Source code for OpenPinch.analysis.heat_pumps.service

"""Public entrypoint for heat pump and refrigeration targeting."""

from __future__ import annotations

from copy import deepcopy

import numpy as np

from ...analysis.numerics import get_period_index
from ...analysis.targeting.cascade import create_problem_table_with_t_int
from ...contracts.hpr import HeatPumpTargetOutputs
from ...domain.configuration import Configuration, tol
from ...domain.enums import (
    GraphType,
    HeatPumpAndRefrigerationCycle,
    ProblemTableLabel,
    TargetType,
)
from ...domain.problem_table import ProblemTable
from ...domain.targets import (
    DirectHeatPumpTarget,
    DirectRefrigerationTarget,
    IndirectHeatPumpTarget,
    IndirectRefrigerationTarget,
)
from ...domain.zone import Zone
from ..targeting.cascade import (
    get_process_heat_cascade,
    get_utility_heat_cascade,
)
from ._multiperiod.execution import get_multiperiod_hpr_targets
from ._multiperiod.preparation import (
    build_multiperiod_hpr_cases,
    period_case_by_id,
    period_id_for_index,
)
from .common.load_selection import resolve_hpr_target_load
from .common.postprocessing import _get_hpr_residual_utility_summary
from .common.preprocessing import (
    construct_HPRTargetInputs,
)
from .targeting.brayton import (
    optimise_brayton_heat_pump_placement,
)
from .targeting.cascade_carnot import (
    optimise_cascade_carnot_heat_pump_placement,
)
from .targeting.cascade_vapour_compression import (
    optimise_cascade_heat_pump_placement,
)
from .targeting.parallel_carnot import (
    optimise_parallel_carnot_heat_pump_placement,
)
from .targeting.parallel_vapour_compression import (
    optimise_parallel_heat_pump_placement,
)
from .targeting.vapour_compression_mvr import (
    optimise_vapour_compression_mvr_heat_pump_placement,
)

__all__ = [
    "compute_direct_heat_pump_or_refrigeration_target",
    "compute_indirect_heat_pump_or_refrigeration_target",
]


################################################################################
# Public API
################################################################################


[docs] def compute_direct_heat_pump_or_refrigeration_target( zone: Zone, is_heat_pumping: bool, args: dict | None = None, ) -> DirectHeatPumpTarget | DirectRefrigerationTarget | None: """Solve an explicit direct Heat Pump or refrigeration target for one zone.""" if _use_multiperiod_hpr_optimization(zone): return _compute_multiperiod_heat_pump_or_refrigeration_target( zone=zone, is_heat_pumping=is_heat_pumping, is_direct=True, args=args, ) idx, period_id = get_period_index(period_ids=zone.period_ids, args=args) is_refrigeration = not (is_heat_pumping) base_target = zone.targets[TargetType.DI.value] pt = deepcopy(base_target.pt) target_load = resolve_hpr_target_load( H_net_cold=pt[ProblemTableLabel.H_NET_COLD], H_net_hot=pt[ProblemTableLabel.H_NET_HOT], is_heat_pumping=is_heat_pumping, is_refrigeration=is_refrigeration, config=zone.config, period_id=period_id, period_idx=idx, ) if target_load < tol: return None res = _get_hpr_targets( Q_hpr_target=target_load, T_vals=pt[ProblemTableLabel.T], H_hot=pt[ProblemTableLabel.H_NET_HOT], H_cold=pt[ProblemTableLabel.H_NET_COLD], config=zone.config, is_heat_pumping=is_heat_pumping, period_idx=idx, ) pt = _calc_hpr_cascade( pt=pt, res=res, is_T_vals_shifted=True, is_heat_pumping=is_heat_pumping, period_idx=idx, ) general_results = { "zone_name": zone.name, "type": TargetType.DHP.value if is_heat_pumping else TargetType.DR.value, "parent_zone": zone.parent_zone, "config": zone.config, "pt": pt, "graphs": _get_hpr_graphs( pt=pt, is_direct=True, is_heat_pumping=is_heat_pumping, ), "period_id": period_id, "period_idx": idx, } hpr_results = _get_hpr_target_summary(res, zone) util_results = _get_hpr_residual_utility_summary( pt=pt, base_target=base_target, period_idx=idx, is_direct=True, is_heat_pumping=is_heat_pumping, ) model_cls = DirectHeatPumpTarget if is_heat_pumping else DirectRefrigerationTarget return model_cls.model_validate(general_results | hpr_results | util_results)
[docs] def compute_indirect_heat_pump_or_refrigeration_target( zone: Zone, is_heat_pumping: bool, args: dict | None = None, ) -> IndirectHeatPumpTarget | IndirectRefrigerationTarget | None: """Solve an indirect / utility system Heat Pump or refrigeration target.""" if _use_multiperiod_hpr_optimization(zone): return _compute_multiperiod_heat_pump_or_refrigeration_target( zone=zone, is_heat_pumping=is_heat_pumping, is_direct=False, args=args, ) idx, period_id = get_period_index(period_ids=zone.period_ids, args=args) is_refrigeration = not (is_heat_pumping) base_target = zone.targets[TargetType.TS.value] pt = deepcopy(base_target.pt) # Create problem table based on inverted utility streams pt_ut_gen = get_process_heat_cascade( hot_streams=zone.cold_utilities.get_hot_streams(invert_utility=True), cold_streams=zone.hot_utilities.get_cold_streams(invert_utility=True), is_shifted=True, is_full_analysis=True, period_idx=idx, ) # Perform heat pump and/or refrigeration targeting on the correct cascades target_load = resolve_hpr_target_load( H_net_cold=pt_ut_gen[ProblemTableLabel.H_NET_COLD], H_net_hot=pt_ut_gen[ProblemTableLabel.H_NET_HOT], is_heat_pumping=is_heat_pumping, is_refrigeration=is_refrigeration, config=zone.config, period_id=period_id, period_idx=idx, ) if target_load < tol: return None res = _get_hpr_targets( Q_hpr_target=target_load, T_vals=pt_ut_gen[ProblemTableLabel.T], H_hot=pt_ut_gen[ProblemTableLabel.H_NET_HOT], H_cold=pt_ut_gen[ProblemTableLabel.H_NET_COLD], config=zone.config, is_heat_pumping=is_heat_pumping, period_idx=idx, ) pt = _calc_hpr_cascade( pt=pt, res=res, is_T_vals_shifted=True, is_heat_pumping=is_heat_pumping, period_idx=idx, ) general_results = { "zone_name": zone.name, "type": TargetType.IHP.value if is_heat_pumping else TargetType.IR.value, "parent_zone": zone.parent_zone, "config": zone.config, "pt": pt, "graphs": _get_hpr_graphs( pt=pt, is_direct=False, is_heat_pumping=is_heat_pumping, ), "period_id": period_id, "period_idx": idx, } hpr_results = _get_hpr_target_summary(res, zone) util_results = _get_hpr_residual_utility_summary( pt=pt, base_target=base_target, period_idx=idx, is_direct=False, is_heat_pumping=is_heat_pumping, ) model_cls = ( IndirectHeatPumpTarget if is_heat_pumping else IndirectRefrigerationTarget ) return model_cls.model_validate(general_results | hpr_results | util_results)
################################################################################ # Helper functions API ################################################################################ def _use_multiperiod_hpr_optimization(zone: Zone) -> bool: return bool(getattr(zone.config.hpr, "multiperiod_optimization_enabled", False)) def _compute_multiperiod_heat_pump_or_refrigeration_target( *, zone: Zone, is_heat_pumping: bool, is_direct: bool, args: dict | None = None, ) -> ( DirectHeatPumpTarget | DirectRefrigerationTarget | IndirectHeatPumpTarget | IndirectRefrigerationTarget ): idx, period_id = get_period_index(period_ids=zone.period_ids, args=args) selected_period_id = period_id_for_index(zone, idx) cases = build_multiperiod_hpr_cases( zone=zone, is_heat_pumping=is_heat_pumping, is_direct=is_direct, args=args, ) res = get_multiperiod_hpr_targets( period_cases=cases, selected_period_id=selected_period_id, selected_period_idx=idx, ) selected_case = period_case_by_id(cases, selected_period_id) pt = _calc_hpr_cascade( pt=deepcopy(selected_case.base_target.pt), res=res, is_T_vals_shifted=True, is_heat_pumping=is_heat_pumping, period_idx=idx, ) general_results = { "zone_name": zone.name, "type": _hpr_target_type(is_direct=is_direct, is_heat_pumping=is_heat_pumping), "parent_zone": zone.parent_zone, "config": zone.config, "pt": pt, "graphs": _get_hpr_graphs( pt=pt, is_direct=is_direct, is_heat_pumping=is_heat_pumping, ), "period_id": period_id, "period_idx": idx, } hpr_results = _get_hpr_target_summary(res, zone) util_results = _get_hpr_residual_utility_summary( pt=pt, base_target=selected_case.base_target, period_idx=idx, is_direct=is_direct, is_heat_pumping=is_heat_pumping, ) return _hpr_target_model_cls( is_direct=is_direct, is_heat_pumping=is_heat_pumping, ).model_validate(general_results | hpr_results | util_results) def _hpr_target_type(*, is_direct: bool, is_heat_pumping: bool) -> str: if is_direct: return TargetType.DHP.value if is_heat_pumping else TargetType.DR.value return TargetType.IHP.value if is_heat_pumping else TargetType.IR.value def _hpr_target_model_cls(*, is_direct: bool, is_heat_pumping: bool): if is_direct: return DirectHeatPumpTarget if is_heat_pumping else DirectRefrigerationTarget return IndirectHeatPumpTarget if is_heat_pumping else IndirectRefrigerationTarget def _get_hpr_targets( Q_hpr_target: float, T_vals: np.ndarray, H_hot: np.ndarray, H_cold: np.ndarray, config: Configuration, is_heat_pumping: bool, period_idx: int = 0, ) -> HeatPumpTargetOutputs: args = construct_HPRTargetInputs( Q_hpr_target=Q_hpr_target, T_vals=T_vals, H_hot=np.abs(H_hot) * -1, H_cold=np.abs(H_cold), is_heat_pumping=is_heat_pumping, config=config, period_idx=period_idx, debug=False, ) handler = _HP_PLACEMENT_HANDLERS.get(args.hpr_type) if handler is None: raise ValueError("No valid heat pump targeting type selected.") result = handler(args) return HeatPumpTargetOutputs.model_validate(result.to_output_fields()) def _get_hpr_target_summary( res: HeatPumpTargetOutputs, zone: Zone, ) -> dict: return { "hpr_cycle": str(zone.config.hpr.type), "hpr_utility_total": res.utility_tot, "hpr_work": res.w_net, "hpr_external_utility": res.Q_ext, "hpr_ambient_hot": res.Q_amb_hot, "hpr_ambient_cold": res.Q_amb_cold, "hpr_cop": res.cop_h, "hpr_eta_he": res.eta_he, "hpr_operating_cost": res.hpr_operating_cost, "hpr_capital_cost": res.hpr_capital_cost, "hpr_annualized_capital_cost": res.hpr_annualized_capital_cost, "hpr_total_annualized_cost": res.hpr_total_annualized_cost, "hpr_compressor_capital_cost": res.hpr_compressor_capital_cost, "hpr_heat_exchanger_capital_cost": res.hpr_heat_exchanger_capital_cost, "hpr_success": res.success, "hpr_hot_streams": res.hpr_hot_streams, "hpr_cold_streams": res.hpr_cold_streams, "hpr_details": res, } def _get_hpr_graphs( pt: ProblemTable, *, is_direct: bool, is_heat_pumping: bool, ) -> dict: if not is_heat_pumping: return {} if is_direct: return { GraphType.NLP_HP.value: pt.slice( [ ProblemTableLabel.T, ProblemTableLabel.H_NET_HOT, ProblemTableLabel.H_NET_COLD, ProblemTableLabel.H_HOT_HP, ProblemTableLabel.H_COLD_HP, ] ), GraphType.GCC_HP.value: pt.slice( [ ProblemTableLabel.T, ProblemTableLabel.H_NET_W_AIR, ProblemTableLabel.H_NET_HP, ] ), } return { GraphType.SUGCC.value: pt.slice( [ ProblemTableLabel.T, ProblemTableLabel.H_NET_UT, ProblemTableLabel.H_NET_HP, ] ) } def _calc_hpr_cascade( pt: ProblemTable, res: HeatPumpTargetOutputs, is_T_vals_shifted: bool = True, is_heat_pumping: bool = True, period_idx: int | None = None, ) -> ProblemTable: # Add new temperature intervals to the process heat cascade pt_hpr_grid = create_problem_table_with_t_int( streams=( res.hpr_hot_streams + res.hpr_cold_streams + res.amb_streams.get_hot_streams() + res.amb_streams.get_cold_streams() ), is_shifted=is_T_vals_shifted, period_idx=period_idx, ) pt.share_temperature_intervals(pt_hpr_grid) # Ambient air addition to the process stream set pt[ProblemTableLabel.H_NET_W_AIR] = pt[ProblemTableLabel.H_NET_A] if len(res.amb_streams) > 0: pt_air = get_process_heat_cascade( hot_streams=res.amb_streams.get_hot_streams(), cold_streams=res.amb_streams.get_cold_streams(), is_shifted=is_T_vals_shifted, is_full_analysis=True, period_idx=period_idx, ) pt.share_temperature_intervals(pt_air) pt[ProblemTableLabel.H_NET_W_AIR] += pt_air[ProblemTableLabel.H_NET] pt[ProblemTableLabel.H_NET_HOT] += pt_air[ProblemTableLabel.H_NET_HOT] pt[ProblemTableLabel.H_NET_COLD] += pt_air[ProblemTableLabel.H_NET_COLD] # Heat pump or refrigeration cascade hpr_profile = get_utility_heat_cascade( T_int_vals=pt[ProblemTableLabel.T], hot_utilities=res.hpr_hot_streams, cold_utilities=res.hpr_cold_streams, is_shifted=is_T_vals_shifted, period_idx=period_idx, ) hpr_updates = hpr_profile["updates"] if is_heat_pumping: pt.update( T_col=hpr_profile["T_col"], updates={ ProblemTableLabel.H_NET_HP: hpr_updates[ProblemTableLabel.H_NET_UT], ProblemTableLabel.H_HOT_HP: hpr_updates[ProblemTableLabel.H_HOT_UT], ProblemTableLabel.H_COLD_HP: hpr_updates[ProblemTableLabel.H_COLD_UT], }, ) else: pt.update( T_col=hpr_profile["T_col"], updates={ ProblemTableLabel.H_NET_RFRG: hpr_updates[ProblemTableLabel.H_NET_UT], ProblemTableLabel.H_HOT_RFRG: hpr_updates[ProblemTableLabel.H_HOT_UT], ProblemTableLabel.H_COLD_RFRG: hpr_updates[ProblemTableLabel.H_COLD_UT], }, ) return pt _HP_PLACEMENT_HANDLERS = { HeatPumpAndRefrigerationCycle.Brayton.value: optimise_brayton_heat_pump_placement, HeatPumpAndRefrigerationCycle.CascadeCarnot.value: ( optimise_cascade_carnot_heat_pump_placement ), HeatPumpAndRefrigerationCycle.ParallelVapourComp.value: ( optimise_parallel_heat_pump_placement ), HeatPumpAndRefrigerationCycle.CascadeVapourComp.value: ( optimise_cascade_heat_pump_placement ), HeatPumpAndRefrigerationCycle.VapourCompMVR.value: ( optimise_vapour_compression_mvr_heat_pump_placement ), HeatPumpAndRefrigerationCycle.ParallelCarnot.value: ( optimise_parallel_carnot_heat_pump_placement ), }