Example of robust, flexible, and extensible model composition using dependency injection via protocols and immutability #2630
Replies: 2 comments
-
|
The example above is simple to emphasize the root concepts. To prove this approach's viability in |
Beta Was this translation helpful? Give feedback.
-
|
As promised, the code below shows an effective way of implementing a cell-temperature-model interface for a PV array model class. Obviously, I cut corners on the docstrings, but I couldn't help but type all the parameters and put unit suffixes on many of them. This isn't 100% polished (one outstanding FIXME, but not related to architecture), but I think the implementation is already very transparent, yet also modular, extensible, and flexible. Note that I use keyword arguments throughout to ensure flexibility (no strict ordering) and extensibility. I think one change for actual implementation in Again, the big win here is that new temperature models can be added without touching the """
Example of a "bare bones" PV array class that uses a flexible interface for the cell
temperature component.
"""
import typing
import numpy.testing
import numpy.typing
class SupportsCellTemperature(typing.Protocol):
"""Interface for photovoltaic cell-temperature models."""
def cell_temperature(
self,
**conditions, # Allow for varied inputs across different tempurature models.
) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
"""Compute cell temperature based on conditions given in inputs."""
... # Method that must be implemented
class RossCellTemperature:
"""Ross cell-temperature model."""
def __init__(self, *, k_W_per_m2: float) -> None:
"""Validate and initialize model."""
self._k_W_per_m2 = k_W_per_m2
@property
def k_W_per_m2(self) -> float:
"""Return the model's k parameter."""
return self._k_W_per_m2
def cell_temperature(
self,
*,
temp_air: numpy.typing.ArrayLike,
poa_global: numpy.typing.ArrayLike,
**_, # Support unused conditions that might be passed.
) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
"""Compute cell temperature based on conditions (SupportsCellTemperature)."""
# Ensure vectorization.
temp_air = numpy.asarray(temp_air)
poa_global = numpy.asarray(poa_global)
return temp_air + self.k_W_per_m2 * poa_global
def ross_from_noct(*, noct: float) -> RossCellTemperature:
"""
Factory for RossCellTemperature from Nominal Operating Cell Temperature (NOCT).
"""
# FIXME I do not follow this unit conversion comment. Seems backwards.
# Factor of 0.1 converts irradiance from W/m2 to mW/cm2.
k_W_per_m2 = 0.1 * (noct - 20.0) / 80.0
return RossCellTemperature(k_W_per_m2=k_W_per_m2)
class FaimanCellTemperature:
"""Faiman cell-temperature model."""
def __init__(
self, *, u0_W_per_m2_K: float = 25.0, u1_W_s_per_m3_K: float = 6.84
) -> None:
"""Validate and initialize model."""
if u0_W_per_m2_K < 0:
raise ValueError(f"u0_W_per_m2_K {u0_W_per_m2_K} is negative")
if u1_W_s_per_m3_K < 0:
raise ValueError(f"u1_W_s_per_m3_K {u1_W_s_per_m3_K} is negative")
self._u0_W_per_m2_K = u0_W_per_m2_K
self._u1_W_s_per_m3_K = u1_W_s_per_m3_K
@property
def u0_W_per_m2_K(self) -> float:
"""Return the model's u0 parameter."""
return self._u0_W_per_m2_K
@property
def u1_W_s_per_m3_K(self) -> float:
"""Return the model's u1 parameter."""
return self._u1_W_s_per_m3_K
def cell_temperature(
self,
*,
temp_air: numpy.typing.ArrayLike,
poa_global: numpy.typing.ArrayLike,
wind_speed: numpy.typing.ArrayLike = 1.0,
**_, # Support unused conditions that might be passed.
) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
"""Compute cell temperature based on conditions (SupportsCellTemperature)."""
# Ensure vectorization.
temp_air = numpy.asarray(temp_air)
poa_global = numpy.asarray(poa_global)
wind_speed = numpy.asarray(wind_speed)
return temp_air + poa_global / (
self.u0_W_per_m2_K + self.u1_W_s_per_m3_K * wind_speed
)
class Array:
"""A "bare bones" photovoltaic array class to demonstrate interface usage."""
def __init__(self, *, cell_temperature_model: SupportsCellTemperature) -> None:
self._cell_temperature_model = cell_temperature_model
@property
def cell_temperature_model(self) -> SupportsCellTemperature:
"""Return the array's cell temperature model."""
return self._cell_temperature_model
def get_cell_temperature(
self,
**conditions,
) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
"""Compute cell temperature based on conditions."""
return self.cell_temperature_model.cell_temperature(**conditions)
if __name__ == "__main__":
# Ross example
ross_cell_temperature_model = RossCellTemperature(k_W_per_m2=0.0342)
pv_array_ross = Array(cell_temperature_model=ross_cell_temperature_model)
# Define conditions relevant to Ross temperature model.
temp_air = numpy.array((25.0, 35.0, 45.0))
poa_global = numpy.array((800.0, 1000.0, 1100.0))
# Solve system at Ross-model conditions.
pv_array_ross_cell_temperature = pv_array_ross.get_cell_temperature(
temp_air=temp_air, poa_global=poa_global
)
print(f"PV array's Ross cell temperature: {pv_array_ross_cell_temperature}")
# Faiman example
faiman_cell_temperature_model = FaimanCellTemperature() # Use defaults.
pv_array_faiman = Array(cell_temperature_model=faiman_cell_temperature_model)
# Define additional conditions relevant to Faiman temperature model.
windspeed = numpy.array((0.0, 1.0, 2.0))
# Solve system at Faiman-model conditions.
pv_array_faiman_cell_temperature = pv_array_faiman.get_cell_temperature(
temp_air=temp_air, poa_global=poa_global, windspeed=windspeed
)
print(f"PV array's Faiman cell temperature: {pv_array_faiman_cell_temperature}")
# By design, we can pass "extra" conditions to Ross model without breaking stuff.
conditions = {
"temp_air": temp_air,
"poa_global": poa_global,
"windspeed": windspeed,
}
# This should NOT raise even though there is an extra parameter in conditions.
numpy.testing.assert_equal(
pv_array_ross_cell_temperature,
pv_array_ross.get_cell_temperature(**conditions),
) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
In the discussion of #2625, the benefits of dependency injection and immutability after validated construction were both discussed in terms of making the composition of models less fragile, easier to maintain and extend, and safer to use. So, here is an example of that.
@ramaroesilva I encourage you to add a
Rectangleclass that implementsCylinderBaseand that is constructed fromlengthandwidthparameters. You should not have to touch theCylinderclass at all in order to use this new "component model" (!), because it meets theCylinderBaseprotocol. Bonus points for a refactor of the existingSquareclass to use the newRectangleclass. I'm happy to provide support and further discussion.Beta Was this translation helpful? Give feedback.
All reactions