Source code for OpenPinch.domain.stream

"""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"]