"""Construct validated :class:`Zone` trees and attach stream/utility data."""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
from ....contracts.input import StreamSchema, UtilitySchema, ZoneTreeSchema
from ....contracts.units import standardise_input_value
from ....domain._value.resolution import resolve_value_array
from ....domain.configuration import tol
from ....domain.enums import StreamType
from ....domain.stream import Stream
from ....domain.stream_collection import StreamCollection
from ....domain.zone import Zone
from . import canonicalization as _canonicalization
from .segments import _create_segmented_process_stream
from .utilities import (
_get_hot_and_cold_utilities,
_set_utilities_for_zone_and_subzones,
)
__all__ = ["prepare_problem"]
[docs]
def prepare_problem(
streams: Optional[List[StreamSchema]] = None,
utilities: Optional[List[UtilitySchema]] = None,
options: Optional[Dict[str, Any]] = None,
project_name: str = "Site",
zone_tree: ZoneTreeSchema = None,
) -> Zone:
"""Build the top-level zone hierarchy for analysis."""
streams = [] if streams is None else list(streams)
utilities = [] if utilities is None else list(utilities)
top_zone_name, top_zone_identifier = _canonicalization._get_validated_zone_info(
zone_tree,
project_name,
)
config = _canonicalization._build_zone_config(
options=options,
top_zone_name=top_zone_name,
top_zone_identifier=top_zone_identifier,
)
zone_tree, streams, utilities, config = _canonicalization._validate_input_data(
zone_tree=zone_tree,
streams=streams,
utilities=utilities,
config=config,
)
master_zone = Zone(
name=config.problem.top_zone_name,
type=config.problem.top_zone_identifier,
config=config,
)
master_zone = _canonicalization._create_nested_zones(
parent_zone=master_zone,
zone_tree=zone_tree,
config=master_zone.config,
)
prepared_streams, process_zone_paths = _build_prepared_stream_collection(
master_zone=master_zone,
streams=sorted(streams, key=lambda stream: stream.name),
utilities=utilities,
)
master_zone = _assign_process_streams_to_subzones(
master_zone=master_zone,
process_streams=prepared_streams.get_process_streams(),
process_zone_paths=process_zone_paths,
)
master_zone.import_hot_and_cold_streams_from_sub_zones()
master_zone = _set_utilities_for_zone_and_subzones(
zone=master_zone,
hot_utilities=prepared_streams.get_hot_utility_streams(),
cold_utilities=prepared_streams.get_cold_utility_streams(),
)
master_zone = _canonicalization._apply_zone_dt_cont_multiplier(
parent_zone=master_zone,
zone_tree=zone_tree,
)
return master_zone
def _build_prepared_stream_collection(
master_zone: Zone,
streams: List[StreamSchema],
utilities: List[UtilitySchema],
) -> tuple[StreamCollection, Dict[str, str]]:
"""Build one canonical collection of prepared process and utility streams."""
process_streams = StreamCollection()
process_streams.set_period_context(
period_ids=master_zone.period_ids,
weights=master_zone.weights,
num_periods=master_zone.num_periods,
)
process_zone_paths: Dict[str, str] = {}
for stream_schema in streams:
zone = master_zone.get_subzone(stream_schema.zone)
if zone is None:
raise ValueError(
f"Validated stream '{stream_schema.name}' could not resolve zone "
f"'{stream_schema.zone}'."
)
stream_obj = _create_process_stream(
stream=stream_schema,
zone=zone,
)
stream_key = _build_process_stream_key(
zone_path=zone.address,
stream_obj=stream_obj,
)
resolved_key = process_streams.add(
stream=stream_obj,
key=stream_key,
prevent_overwrite=True,
)
process_zone_paths[resolved_key] = zone.address
hu_t_min, cu_t_max = _find_extreme_process_temperatures(
hot_streams=process_streams.get_hot_process_streams(),
cold_streams=process_streams.get_cold_process_streams(),
)
utility_streams = _get_hot_and_cold_utilities(
utilities=utilities,
hu_t_min=hu_t_min,
cu_t_max=cu_t_max,
config=master_zone.config,
dt_cont_multiplier=master_zone.dt_cont_multiplier,
)
prepared_streams = process_streams + utility_streams
return prepared_streams, process_zone_paths
def _assign_process_streams_to_subzones(
master_zone: Zone,
process_streams: StreamCollection,
process_zone_paths: Dict[str, str],
) -> Zone:
"""Attach prepared process streams to their owning zones by reference."""
for stream_key, stream_obj in process_streams.items():
zone_path = process_zone_paths.get(stream_key)
if zone_path is None:
raise RuntimeError(
f"Prepared process stream '{stream_key}' is missing a zone mapping."
)
zone = master_zone.get_subzone(zone_path)
if zone is None:
raise ValueError(
f"Prepared process stream '{stream_obj.name}' could not resolve zone "
f"'{zone_path}'."
)
if stream_obj.stream_type == StreamType.Hot.value:
zone.hot_streams.add(stream_obj, key=stream_key, prevent_overwrite=False)
elif stream_obj.stream_type == StreamType.Cold.value:
zone.cold_streams.add(
stream_obj,
key=stream_key,
prevent_overwrite=False,
)
else:
raise ValueError(
f"Process stream '{stream_obj.name}' must classify as Hot or Cold, "
f"got '{stream_obj.stream_type}'."
)
return master_zone
def _validate_stream_temperatures(stream: StreamSchema):
"""Validate that supply and target temperatures align with stream type."""
t_supply = resolve_value_array(stream.t_supply)
t_target = resolve_value_array(stream.t_target)
heat_flow = resolve_value_array(stream.heat_flow)
if np.all((abs(t_supply - t_target) < tol) * (heat_flow != 0.0)):
raise ValueError(
f"Process stream '{stream.name}' must classify as Hot or Cold."
)
def _create_process_stream(stream: StreamSchema, zone: Zone) -> Stream:
"""Create a process :class:`Stream` from one validated schema record."""
if stream.segments is not None or stream.profile is not None:
return _create_segmented_process_stream(stream, zone)
_validate_stream_temperatures(stream)
stream_obj = Stream(
name=stream.name,
supply_temperature=standardise_input_value(
stream.t_supply,
field_name="t_supply",
config=zone.config,
),
target_temperature=standardise_input_value(
stream.t_target,
field_name="t_target",
config=zone.config,
),
supply_pressure=standardise_input_value(
stream.p_supply,
field_name="p_supply",
config=zone.config,
),
target_pressure=standardise_input_value(
stream.p_target,
field_name="p_target",
config=zone.config,
),
supply_enthalpy=standardise_input_value(
stream.h_supply,
field_name="h_supply",
config=zone.config,
),
target_enthalpy=standardise_input_value(
stream.h_target,
field_name="h_target",
config=zone.config,
),
heat_flow=standardise_input_value(
stream.heat_flow,
field_name="heat_flow",
config=zone.config,
),
delta_t_contribution=standardise_input_value(
stream.dt_cont,
field_name="dt_cont",
config=zone.config,
),
delta_t_contribution_multiplier=zone.dt_cont_multiplier,
heat_transfer_coefficient=standardise_input_value(
stream.htc,
field_name="htc",
config=zone.config,
),
is_process_stream=True,
fluid_name=stream.fluid_name,
fluid_phase=stream.fluid_phase,
)
return stream_obj
def _build_process_stream_key(zone_path: str, stream_obj: Stream) -> str:
"""Build a stable canonical key for one prepared process stream."""
return ".".join([zone_path, stream_obj.name])
def _find_extreme_process_temperatures(
hot_streams: StreamCollection,
cold_streams: StreamCollection,
) -> Tuple[float, float]:
"""Find cold-stream high and hot-stream low target temperatures."""
if len(hot_streams) == 0 and len(cold_streams) == 0:
return 20, 20
hu_t_min: float = None
cu_t_max: float = None
stream: Stream
for stream in hot_streams:
thermal_segments = stream.segments or (stream,)
for segment in thermal_segments:
segment_min = segment.shifted_minimum_temperature.min
if cu_t_max is None or cu_t_max > segment_min:
cu_t_max = segment_min
for stream in cold_streams:
thermal_segments = stream.segments or (stream,)
for segment in thermal_segments:
segment_max = segment.shifted_maximum_temperature.max
if hu_t_min is None or hu_t_min < segment_max:
hu_t_min = segment_max
if hu_t_min is None:
hu_t_min = cu_t_max
if cu_t_max is None:
cu_t_max = hu_t_min
return float(hu_t_min), float(cu_t_max)