"""Data model representing process and utility streams."""
from __future__ import annotations
import warnings
from collections.abc import Mapping
from typing import Any, Optional
import numpy as np
from ._stream import profile as _stream_profile
from ._stream import segments as _stream_segments
from ._stream import thermodynamics as _stream_thermodynamics
from ._stream import value_state as _stream_value_state
from .configuration import tol
from .enums import FluidPhase
from .fluids import validate_coolprop_fluid_name
from .value import Value
_TEMPERATURE_EQUAL_TOL = 1e-12
MaybeVU = Any
[docs]
class Stream:
"""Generic thermal stream used for both process and utility duties.
A :class:`Stream` stores supply/target states together with derived values
such as hot/cold classification, shifted temperature bounds, heat-capacity
flow rate, and simple economic attributes. The same class is reused for
process streams, utilities, and derived net streams created during site-
level aggregation.
"""
_VALUE_UNITS = {
"_t_supply": "degC",
"_t_target": "degC",
"_p_supply": "kPa",
"_p_target": "kPa",
"_h_supply": "kJ/kg",
"_h_target": "kJ/kg",
"_dt_cont": "delta_degC",
"_dt_cont_act": "delta_degC",
"_heat_flow": "kW",
"_htc": "kW/m^2/delta_degC",
"_htr": "m^2*delta_degC/kW",
"_price": "$/MW/h",
"_cost": "$/h",
"_cp": "kW/delta_degC",
"_rcp_prod": "m^2",
"_t_min": "degC",
"_t_max": "degC",
"_t_min_star": "degC",
"_t_max_star": "degC",
}
_CORE_VALUE_ATTRS = (
"_t_supply",
"_t_target",
"_p_supply",
"_p_target",
"_h_supply",
"_h_target",
"_dt_cont",
"_heat_flow",
"_htc",
"_price",
)
_DERIVED_VALUE_ATTRS = (
"_dt_cont_act",
"_t_min",
"_t_max",
"_t_min_star",
"_t_max_star",
"_cp",
"_htr",
"_cost",
"_rcp_prod",
"_t_entr_mean",
)
_PUBLIC_VALUE_ATTRS = {
"supply_temperature": "_t_supply",
"target_temperature": "_t_target",
"supply_pressure": "_p_supply",
"target_pressure": "_p_target",
"supply_enthalpy": "_h_supply",
"target_enthalpy": "_h_target",
"delta_t_contribution": "_dt_cont",
"heat_flow": "_heat_flow",
"heat_transfer_coefficient": "_htc",
"price": "_price",
"effective_delta_t_contribution": "_dt_cont_act",
"minimum_temperature": "_t_min",
"maximum_temperature": "_t_max",
"shifted_minimum_temperature": "_t_min_star",
"shifted_maximum_temperature": "_t_max_star",
"heat_capacity_flowrate": "_cp",
"heat_transfer_resistance": "_htr",
"utility_cost": "_cost",
"resistance_capacity_product": "_rcp_prod",
"entropic_mean_temperature": "_t_entr_mean",
}
_PUBLIC_ATTRS = {
**_PUBLIC_VALUE_ATTRS,
"name": "_name",
"is_process_stream": "_is_process_stream",
"fluid_name": "_fluid_name",
"fluid_phase": "_fluid_phase",
"num_periods": "_num_periods",
"stream_type": "_type",
"is_active": "_active",
"delta_t_contribution_multiplier": "_dt_cont_multiplier",
"delta_t_contribution_multiplier_locked": "_dt_cont_multiplier_locked",
}
_RETIRED_PUBLIC_ATTRS = frozenset(
{
"t_supply",
"t_target",
"p_supply",
"p_target",
"h_supply",
"h_target",
"dt_cont",
"dt_cont_act",
"dt_cont_multiplier",
"dt_cont_multiplier_locked",
"htc",
"htr",
"ut_cost",
"CP",
"rCP",
"t_min",
"t_max",
"t_min_star",
"t_max_star",
"t_entr_mean",
"type",
"active",
}
)
def __setattr__(self, name: str, value: Any) -> None:
if name in self._RETIRED_PUBLIC_ATTRS:
raise AttributeError(
f"Stream has no attribute {name!r}; use the descriptive runtime name."
)
super().__setattr__(name, value)
def __init__(
self,
name: str = "Stream",
supply_temperature: Optional[MaybeVU] = None,
target_temperature: Optional[MaybeVU] = None,
supply_pressure: Optional[MaybeVU] = None,
target_pressure: Optional[MaybeVU] = None,
supply_enthalpy: Optional[MaybeVU] = None,
target_enthalpy: Optional[MaybeVU] = None,
delta_t_contribution: MaybeVU = 0.0,
delta_t_contribution_multiplier: float = 1.0,
heat_flow: MaybeVU = 0.0,
heat_transfer_coefficient: MaybeVU = 1.0,
price: Optional[MaybeVU] = None,
is_process_stream: bool = True,
fluid_name: Optional[str] = None,
fluid_phase: Optional[str | FluidPhase] = None,
segments: list[object] | tuple[object, ...] | None = None,
):
"""Initialise a stream and infer hot/cold classification."""
self._segments: tuple[_StreamSegment, ...] = ()
self._syncing_segments = False
self._name = name
self._is_process_stream = bool(is_process_stream)
self._fluid_name = self._normalise_fluid_name(fluid_name)
self._fluid_phase = self._normalise_fluid_phase(fluid_phase)
self._active = True
self._dt_cont_multiplier_locked = False
self._dt_cont_multiplier = float(delta_t_contribution_multiplier or 1.0)
self._numeric_revision = 0
self._period_ids: dict[str, int] | None = None
self._weights: np.ndarray | None = None
self._num_periods: int | None = None
self._type: str | None = None
self._t_supply: Value | None = None
self._t_target: Value | None = None
self._p_supply: Value | None = None
self._p_target: Value | None = None
self._h_supply: Value | None = None
self._h_target: Value | None = None
self._dt_cont: Value | None = None
self._heat_flow: Value | None = None
self._htc: Value | None = None
self._price: Value | None = None
self._dt_cont_act: Value | None = None
self._t_min: Value | None = None
self._t_max: Value | None = None
self._t_min_star: Value | None = None
self._t_max_star: Value | None = None
self._cp: Value | None = None
self._htr: Value | None = None
self._cost: Value | None = None
self._rcp_prod: Value | None = None
self.set_value_attr(
"supply_temperature", supply_temperature, update_derived=False
)
self.set_value_attr(
"target_temperature", target_temperature, update_derived=False
)
self.set_value_attr("supply_pressure", supply_pressure, update_derived=False)
self.set_value_attr("target_pressure", target_pressure, update_derived=False)
self.set_value_attr("supply_enthalpy", supply_enthalpy, update_derived=False)
self.set_value_attr("target_enthalpy", target_enthalpy, update_derived=False)
self.set_value_attr(
"delta_t_contribution", delta_t_contribution, update_derived=False
)
self.set_value_attr("heat_flow", heat_flow, update_derived=False)
self.set_value_attr(
"heat_transfer_coefficient",
heat_transfer_coefficient,
update_derived=False,
)
self.set_value_attr("price", price, update_derived=False)
self._validate_num_periods()
self._calculate_missing_properties()
self.update_derived_properties()
if segments is not None:
self.replace_segments(segments)
if price is not None:
self.price = price
# === Core Properties ===
@property
def name(self) -> str:
"""Stream name."""
return self._name
@name.setter
def name(self, value: str):
"""Set the display name used for reporting and graph labels."""
self._name = value
@property
def is_process_stream(self) -> bool:
"""Process or utility stream."""
return self._is_process_stream
@is_process_stream.setter
def is_process_stream(self, value: bool):
"""Mark whether the stream is treated as process-side or utility-side."""
self._is_process_stream = value
for segment in self._segments:
segment._is_process_stream = value
@property
def fluid_name(self) -> Optional[str]:
"""CoolProp fluid name or mixture specification."""
return self._fluid_name
@fluid_name.setter
def fluid_name(self, value: Optional[str]):
self._fluid_name = self._normalise_fluid_name(value)
for segment in self._segments:
segment._fluid_name = self._fluid_name
@property
def fluid_phase(self) -> Optional[str]:
"""Optional fluid-phase flag: sol, sle, liq, vle, sve, or gas."""
return self._fluid_phase
@fluid_phase.setter
def fluid_phase(self, value: Optional[str | FluidPhase]):
self._fluid_phase = self._normalise_fluid_phase(value)
for segment in self._segments:
segment._fluid_phase = self._fluid_phase
@property
def segments(self) -> tuple["_StreamSegment", ...]:
"""Ordered immutable view of the stream's piecewise thermal segments."""
return self._segments
@property
def has_segments(self) -> bool:
"""Return whether this physical stream has an explicit thermal profile."""
return bool(self._segments)
@property
def segment_count(self) -> int:
"""Return the number of explicit thermal segments."""
return len(self._segments)
@property
def stream_type(self) -> Optional[str]:
"""Stream type (Hot, Cold, Both)."""
return self._type
@property
def num_periods(self) -> Optional[int]:
"""Number of periods."""
return self._num_periods
@property
def period_ids(self) -> dict[str, int] | None:
return self._period_ids
@property
def weights(self) -> np.ndarray | None:
return self._weights
@property
def supply_temperature(self) -> Optional[Value]:
"""Supply temperature (e.g., degC)."""
return self._t_supply
@supply_temperature.setter
def supply_temperature(self, value):
self.set_value_attr("supply_temperature", value)
@property
def target_temperature(self) -> Optional[Value]:
"""Target temperature (e.g., degC)."""
return self._t_target
@target_temperature.setter
def target_temperature(self, value):
self.set_value_attr("target_temperature", value)
@property
def supply_pressure(self) -> Optional[Value]:
"""Supply pressure (e.g., kPa)."""
return self._p_supply
@supply_pressure.setter
def supply_pressure(self, value):
self.set_value_attr("supply_pressure", value)
@property
def target_pressure(self) -> Optional[Value]:
"""Target pressure (e.g., kPa)."""
return self._p_target
@target_pressure.setter
def target_pressure(self, value):
self.set_value_attr("target_pressure", value)
@property
def supply_enthalpy(self) -> Optional[Value]:
"""Supply enthalpy (e.g., kJ/kg)."""
return self._h_supply
@supply_enthalpy.setter
def supply_enthalpy(self, value):
self.set_value_attr("supply_enthalpy", value)
@property
def target_enthalpy(self) -> Optional[Value]:
"""Target enthalpy (e.g., kJ/kg)."""
return self._h_target
@target_enthalpy.setter
def target_enthalpy(self, value):
self.set_value_attr("target_enthalpy", value)
@property
def delta_t_contribution(self) -> Value:
"""Preserved base delta-T contribution before any zone multiplier."""
return self._dt_cont
@delta_t_contribution.setter
def delta_t_contribution(self, value):
self.set_value_attr("delta_t_contribution", value)
@property
def effective_delta_t_contribution(self) -> Value:
"""Effective delta-T contribution used in shifted-temperature calculations."""
return self._dt_cont_act
@property
def delta_t_contribution_multiplier(self) -> float:
"""Effective delta-T contribution used in shifted-temperature calculations."""
return self._dt_cont_multiplier
@delta_t_contribution_multiplier.setter
def delta_t_contribution_multiplier(self, value: float):
"""Set the effective shifted-temperature contribution in active use."""
if not self._dt_cont_multiplier_locked:
self._dt_cont_multiplier = float(value)
for segment in self._segments:
segment._dt_cont_multiplier = self._dt_cont_multiplier
segment.update_derived_properties()
self._bump_numeric_revision()
self.update_derived_properties()
else:
warnings.warn(
"Attempted to change delta_t_contribution_multiplier, but it is "
"locked. "
"No changes were made."
)
@property
def delta_t_contribution_multiplier_locked(self) -> bool:
"""Whether the delta-T contribution multiplier is locked against changes."""
return self._dt_cont_multiplier_locked
@delta_t_contribution_multiplier_locked.setter
def delta_t_contribution_multiplier_locked(self, value: bool):
"""Lock or unlock the delta-T contribution multiplier."""
self._dt_cont_multiplier_locked = bool(value)
@property
def heat_flow(self) -> Value:
"""Stream heat flow view over a scalar or multiperiod duty value."""
return self._heat_flow
@heat_flow.setter
def heat_flow(self, value):
self.set_value_attr("heat_flow", value)
@property
def heat_transfer_coefficient(self) -> Value:
"""Heat transfer coefficient (e.g., kW/m^2/K)."""
return self._htc
@heat_transfer_coefficient.setter
def heat_transfer_coefficient(self, value):
self.set_value_attr("heat_transfer_coefficient", value)
@property
def heat_transfer_resistance(self) -> Optional[Value]:
"""Heat transfer resistance (e.g., m^2.K/kW)."""
return self._copy_value(self._htr)
@property
def price(self) -> Value:
"""Unit energy price (e.g., $/MWh)."""
return self._copy_value(self._price)
@price.setter
def price(self, value):
self.set_value_attr("price", value)
@property
def utility_cost(self) -> Optional[Value]:
"""Utility cost (e.g., $/y)."""
return self._copy_value(self._cost)
@property
def heat_capacity_flowrate(self) -> Optional[Value]:
"""Heat capacity flowrate (e.g., kW/K)."""
return self._copy_value(self._cp)
@property
def resistance_capacity_product(self) -> Optional[Value]:
"""Resistance-capacity product (1/heat transfer rate)."""
return self._copy_value(self._rcp_prod)
@property
def is_active(self) -> bool:
"""Whether the stream is active in analysis."""
return self._active
@is_active.setter
def is_active(self, value: bool):
"""Activate or deactivate the stream for downstream analysis."""
self._active = bool(value)
for segment in self._segments:
segment._active = self._active
segment._bump_numeric_revision()
self._bump_numeric_revision()
# === Computed Temperature Properties ===
@property
def minimum_temperature(self) -> Optional[Value]:
"""Minimum temperature (supply or target depending on hot/cold)."""
return self._copy_value(self._t_min)
@property
def maximum_temperature(self) -> Optional[Value]:
"""Maximum temperature (supply or target depending on hot/cold)."""
return self._copy_value(self._t_max)
@property
def shifted_minimum_temperature(self) -> Optional[Value]:
"""Shifted minimum temperature."""
return self._copy_value(self._t_min_star)
@property
def shifted_maximum_temperature(self) -> Optional[Value]:
"""Shifted maximum temperature."""
return self._copy_value(self._t_max_star)
@property
def entropic_mean_temperature(self) -> Optional[Value]:
"""Entropic mean temperature of supply and target temperatures."""
return self._copy_value(self._t_entr_mean)
# === Methods ===
def set_value_attr(
self,
attr_name: str,
value: float | Value | np.ndarray | Mapping | None,
update_derived: bool = True,
) -> None:
internal_name = self._resolve_attr_name(attr_name)
if (
self.has_segments
and not self._syncing_segments
and internal_name in {"_dt_cont", "_price"}
):
self._update_all_segments_value_attr(attr_name, value)
return
if (
self.has_segments
and not self._syncing_segments
and internal_name in {"_t_supply", "_t_target", "_heat_flow", "_htc"}
):
raise ValueError(
f"{attr_name!r} is derived for segmented stream {self.name!r}; "
"update a segment or replace the complete profile instead."
)
if value is None:
setattr(self, internal_name, None)
self._bump_numeric_revision()
if update_derived:
self.update_derived_properties()
return
parsed = self._coerce_to_value(value, internal_name)
if parsed.num_periods == 1 and self._num_periods not in (None, 0, 1):
parsed = Value(
np.full(int(self._num_periods), float(parsed.value), dtype=float),
unit=parsed.unit,
)
if self._weights is None or (
len(self._weights) == 1 and len(self._weights) != parsed.num_periods
):
self._num_periods = parsed.num_periods
self._period_ids = {str(i): i for i in range(self._num_periods)}
self._weights = np.ones(self._num_periods, dtype=float)
if len(self._weights) > 1 and len(self._weights) != parsed.num_periods:
raise ValueError("Weights length must match the number of periods.")
owned_value = parsed.to(self._VALUE_UNITS[internal_name])
setattr(self, internal_name, self._read_only_value(owned_value))
self._bump_numeric_revision()
self._validate_num_periods()
if update_derived:
self.update_derived_properties()
@classmethod
def _normalise_fluid_name(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
text = str(value).strip()
if not text:
return None
validate_coolprop_fluid_name(text)
return text
@classmethod
def _normalise_fluid_phase(cls, value: Optional[str | FluidPhase]) -> Optional[str]:
if value is None:
return None
text = str(value).strip().lower()
if not text:
return None
try:
return FluidPhase.from_code_or_description(value).name
except ValueError as exc:
valid = ", ".join(phase.name for phase in FluidPhase)
raise ValueError(f"fluid_phase must be one of: {valid}.") from exc
def set_value_attr_at_idx(
self,
attr_name: str,
value: float | Value | np.ndarray = None,
idx: int = 0,
update_derived: bool = True,
):
internal_name = self._resolve_attr_name(attr_name)
if (
self.has_segments
and not self._syncing_segments
and internal_name in {"_dt_cont", "_price"}
):
self._update_all_segments_value_attr(attr_name, value, idx=idx)
return
if (
self.has_segments
and not self._syncing_segments
and internal_name in {"_t_supply", "_t_target", "_heat_flow", "_htc"}
):
raise ValueError(
f"{attr_name!r} is derived for segmented stream {self.name!r}; "
"update a segment or replace the complete profile instead."
)
if internal_name not in self._CORE_VALUE_ATTRS:
raise ValueError(
f"Attribute '{attr_name}' is not a mutable state property of Stream."
)
current = getattr(self, internal_name)
if current is None:
current = Value(0.0, unit=self._VALUE_UNITS[internal_name])
else:
current = current.mutable_copy()
target_size = self._period_vector_size()
if current.num_periods == 1 and target_size > 1:
current = Value(
np.full(target_size, float(current.value), dtype=float),
unit=current.unit,
)
current[idx if current.num_periods > 1 else 0] = value
self._set_internal_value_attr(
internal_name,
current,
update_derived=update_derived,
)
def _coerce_to_value(self, value, attr_name: str) -> Value | None:
return _stream_value_state.coerce_to_value(
value,
target_unit=self._VALUE_UNITS[attr_name],
)
def _calculate_missing_properties(self) -> None:
"""Calculate any missing core properties from available data."""
completed = _stream_thermodynamics.complete_core_state(
t_supply=self._t_supply,
t_target=self._t_target,
dt_cont=self._dt_cont,
heat_flow=self._heat_flow,
htc=self._htc,
price=self._price,
value_units=self._VALUE_UNITS,
stream_name=self._name,
state_size=self._period_vector_size(),
temperature_equal_tol=_TEMPERATURE_EQUAL_TOL,
)
self._t_supply = completed.t_supply
self._t_target = completed.t_target
self._dt_cont = completed.dt_cont
self._heat_flow = completed.heat_flow
self._htc = completed.htc
self._price = completed.price
self._freeze_owned_values()
def update_derived_properties(self) -> None:
derived = _stream_thermodynamics.derive_stream_state(
t_supply=self._t_supply,
t_target=self._t_target,
dt_cont=self._dt_cont,
dt_cont_multiplier=self._dt_cont_multiplier,
heat_flow=self._heat_flow,
htc=self._htc,
price=self._price,
value_units=self._VALUE_UNITS,
stream_name=self._name,
state_size=self._period_vector_size(),
temperature_equal_tol=_TEMPERATURE_EQUAL_TOL,
)
self._type = derived.stream_type
self._dt_cont_act = derived.dt_cont_act
self._t_min = derived.t_min
self._t_max = derived.t_max
self._t_min_star = derived.t_min_star
self._t_max_star = derived.t_max_star
self._t_entr_mean = derived.t_entr_mean
self._cp = derived.cp
self._htr = derived.htr
self._rcp_prod = derived.rcp_prod
self._cost = derived.cost
self._freeze_owned_values()
self._bump_numeric_revision()
def _validate_num_periods(self):
self._num_periods = _stream_value_state.validate_num_periods(
(getattr(self, attr) for attr in self._CORE_VALUE_ATTRS),
stream_name=self._name,
)
[docs]
def invert(self) -> None:
"""Flip a utility stream into its generating process-stream analogue."""
if self._is_process_stream:
raise ValueError(
"Logic error: Process streams cannot be inverted; only utility "
"streams may be inverted for generation."
)
if self.has_segments:
inverted_segments = []
for segment in reversed(self._segments):
candidate = self._detached_segment(segment)
candidate._t_supply, candidate._t_target = (
candidate._t_target,
candidate._t_supply,
)
candidate._p_supply, candidate._p_target = (
candidate._p_target,
candidate._p_supply,
)
candidate._h_supply, candidate._h_target = (
candidate._h_target,
candidate._h_supply,
)
candidate._is_process_stream = True
candidate.update_derived_properties()
inverted_segments.append(candidate)
self._is_process_stream = True
self.replace_segments(inverted_segments)
return
self._t_supply, self._t_target = self._t_target, self._t_supply
self._p_supply, self._p_target = self._p_target, self._p_supply
self._h_supply, self._h_target = self._h_target, self._h_supply
self._is_process_stream = True
self._bump_numeric_revision()
self.update_derived_properties()
def get_period_index(self, period_id: str | None = None) -> int:
if self._period_ids is None or period_id is None:
return 0
resolved_period_id = str(period_id)
if resolved_period_id not in self._period_ids:
raise ValueError(
f"Unknown period_id {resolved_period_id!r}. "
f"Available periods: {', '.join(self._period_ids.keys())}."
)
return int(self._period_ids[resolved_period_id])
def resolve_attr(self, attr_name: str, period_id: str | None = None):
value = getattr(self, self._resolve_attr_name(attr_name))
if isinstance(value, Value):
return float(value[self.get_period_index(period_id)].value)
return value
def set_attr_for_period(
self,
attr_name: str,
value,
*,
period_id: str | None = None,
) -> None:
self.set_value_attr_at_idx(
attr_name,
value,
idx=self.get_period_index(period_id),
)
def _get_period_context(self) -> tuple[dict[str, int] | None, np.ndarray | None]:
return self._period_ids, self._weights
def set_period_context(
self,
period_ids: dict[str, int] | list[str] | tuple[str, ...] | None,
weights: np.ndarray | list[float] | tuple[float, ...] | None,
num_periods: int | None,
) -> None:
self._period_ids = self._normalise_period_ids(period_ids)
if self._period_ids is None:
self._weights = None
self._num_periods = None
self._bump_numeric_revision()
for segment in self._segments:
segment.set_period_context(None, None, None)
return
self._weights = _stream_value_state.resolve_period_weights(
self._period_ids,
weights,
)
self._num_periods = len(self._period_ids)
self._bump_numeric_revision()
for segment in self._segments:
segment.set_period_context(
period_ids=period_ids,
weights=weights,
num_periods=num_periods,
)
def _update_all_segments_value_attr(
self,
attr_name: str,
value: float | Value | np.ndarray | Mapping | None,
*,
idx: int | None = None,
) -> None:
"""Delegate an all-child value mutation to the transaction owner."""
_stream_segments.update_all_value_attributes(self, attr_name, value, idx=idx)
def _update_segments_transaction(
self,
updates: Mapping[int, Mapping[str, object]],
*,
idx: int | None = None,
) -> None:
"""Delegate sparse child updates to the transaction owner."""
_stream_segments.update_transaction(self, updates, idx=idx)
[docs]
def replace_segments(self, segments) -> None:
"""Normalize and atomically replace the piecewise profile."""
_stream_segments.replace(self, segments, segment_type=_StreamSegment)
[docs]
def update_segment(self, index: int, **changes) -> None:
"""Apply one segment update transactionally and revalidate the profile."""
self.update_segments({index: changes})
[docs]
def update_segments(self, updates: Mapping[int, Mapping[str, object]]) -> None:
"""Atomically apply sparse attribute changes to ordered child segments."""
if not isinstance(updates, Mapping):
raise TypeError("updates must map segment indexes to attribute mappings.")
if not updates:
return
self._update_segments_transaction(updates)
[docs]
@classmethod
def from_temperature_heat_profile(
cls,
*,
name: str,
points,
heat_scale: float = 1.0,
heat_unit: str = "kW",
dt_diff_max: float | None = None,
**stream_kwargs,
) -> "Stream":
"""Build one segmented stream from ordered ``[heat, temperature]`` points."""
specs = _stream_profile.temperature_heat_segment_specs(
name=name,
points=points,
heat_scale=heat_scale,
heat_unit=heat_unit,
dt_diff_max=dt_diff_max,
tolerance=tol,
)
common = dict(stream_kwargs)
segment_kwargs = {
key: common[key]
for key in (
"supply_pressure",
"target_pressure",
"delta_t_contribution",
"delta_t_contribution_multiplier",
"heat_transfer_coefficient",
"price",
"is_process_stream",
"fluid_name",
"fluid_phase",
)
if key in common
}
segments = [
_StreamSegment(
name=spec.name,
supply_temperature=spec.t_supply,
target_temperature=spec.t_target,
heat_flow=spec.heat_flow,
segment_index=spec.segment_index,
**segment_kwargs,
)
for spec in specs
]
return cls(name=name, segments=segments, **common)
@staticmethod
def _detached_segment(segment: "_StreamSegment") -> "_StreamSegment":
return _stream_segments.detached(segment, segment_type=_StreamSegment)
def _validate_segments(self, segments: tuple["_StreamSegment", ...]) -> None:
_stream_segments.validate(self, segments)
def _sync_aggregate_from_segments(self) -> None:
_stream_segments.sync_aggregate(self)
def _bump_numeric_revision(self) -> None:
self._numeric_revision = getattr(self, "_numeric_revision", 0) + 1
def _period_vector_size(self) -> int:
return _stream_value_state.period_vector_size(
getattr(self, attr) for attr in self._CORE_VALUE_ATTRS
)
def _value_array(self, value: Value | None, *, size: int) -> np.ndarray:
return _stream_value_state.value_array(
value,
size=size,
stream_name=self._name,
)
def _build_value(self, magnitudes, *, unit: str) -> Value:
return _stream_value_state.build_value(magnitudes, unit=unit)
def _copy_value(self, value: Value | None) -> Value | None:
return _stream_value_state.copy_value(value)
@staticmethod
def _read_only_value(value: Value | None) -> Value | None:
if value is None:
return None
return value._make_read_only(
"Stream-owned Value is read-only; assign the stream property, call "
"set_value_attr_at_idx, or use update_segment(s)."
)
def _freeze_owned_values(self) -> None:
for attr_name in (*self._CORE_VALUE_ATTRS, *self._DERIVED_VALUE_ATTRS):
value = getattr(self, attr_name, None)
if isinstance(value, Value):
self._read_only_value(value)
def _resolve_attr_name(self, attr_name: str) -> str:
if attr_name in self._PUBLIC_ATTRS:
return self._PUBLIC_ATTRS[attr_name]
raise AttributeError(f"Stream has no attribute {attr_name!r}.")
def _set_internal_value_attr(
self,
internal_name: str,
value: float | Value | np.ndarray | Mapping | None,
*,
update_derived: bool = True,
) -> None:
public_name = next(
(
name
for name, candidate in self._PUBLIC_VALUE_ATTRS.items()
if candidate == internal_name
),
None,
)
if public_name is None:
raise AttributeError(f"Stream has no mutable state {internal_name!r}.")
self.set_value_attr(public_name, value, update_derived=update_derived)
@staticmethod
def _is_period_value_data(value: Mapping) -> bool:
return _stream_value_state.is_period_value_data(value)
@staticmethod
def _normalise_period_ids(
period_ids: dict[str, int] | list[str] | tuple[str, ...] | None,
) -> dict[str, int] | None:
return _stream_value_state.normalise_period_ids(period_ids)
@staticmethod
def _normalise_weights(
weights,
*,
expected_len: int,
) -> np.ndarray | None:
return _stream_value_state.normalise_weights(
weights,
expected_len=expected_len,
)
from ._stream.segment import StreamSegment as _StreamSegment # noqa: E402
__all__ = ["Stream"]