OpenPinch 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/__init__.py +2 -0
  2. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/__init__.py +2 -0
  3. openpinch-0.2.4/OpenPinch/classes/_problem/_component_accessor.py +54 -0
  4. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_plot_accessor.py +20 -0
  5. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_target_accessor.py +25 -1
  6. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_target_dispatch.py +19 -16
  7. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_validation.py +62 -64
  8. openpinch-0.2.4/OpenPinch/classes/_stream_collection/_numeric_view.py +81 -0
  9. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/pinch_problem.py +69 -1
  10. openpinch-0.2.4/OpenPinch/classes/process_components.py +45 -0
  11. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/stream.py +117 -62
  12. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/stream_collection.py +45 -1
  13. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/value.py +51 -23
  14. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/zone.py +12 -3
  15. openpinch-0.2.4/OpenPinch/data/notebooks/06_energy_transfer_analysis.ipynb +255 -0
  16. openpinch-0.2.4/OpenPinch/data/notebooks/07_vapour_compression_mvr_cascade_hpr.ipynb +400 -0
  17. openpinch-0.2.4/OpenPinch/data/notebooks/08_direct_gas_stream_mvr.ipynb +439 -0
  18. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/__init__.py +4 -0
  19. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/config.py +1 -1
  20. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/config_metadata.py +3 -0
  21. openpinch-0.2.4/OpenPinch/lib/coolprop_fluids.py +63 -0
  22. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/enums.py +30 -1
  23. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/__init__.py +2 -0
  24. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/hpr.py +5 -1
  25. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/io.py +61 -3
  26. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/reporting.py +2 -0
  27. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/targets.py +18 -0
  28. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/unit_system.py +4 -0
  29. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/__init__.py +6 -0
  30. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/capital_cost_and_area_targeting.py +73 -22
  31. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/gcc_manipulation.py +2 -4
  32. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/graph_data.py +35 -0
  33. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/problem_table_analysis.py +67 -49
  34. openpinch-0.2.4/OpenPinch/services/common/service_orchestration.py +64 -0
  35. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/utility_targeting.py +20 -12
  36. openpinch-0.2.4/OpenPinch/services/components/__init__.py +15 -0
  37. openpinch-0.2.4/OpenPinch/services/components/direct_mvr/__init__.py +21 -0
  38. openpinch-0.2.4/OpenPinch/services/components/direct_mvr/direct_gas_mvr.py +653 -0
  39. openpinch-0.2.4/OpenPinch/services/components/process_mvr.py +681 -0
  40. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/direct_heat_integration/direct_integration_entry.py +58 -66
  41. openpinch-0.2.4/OpenPinch/services/energy_transfer_analysis/__init__.py +15 -0
  42. openpinch-0.2.4/OpenPinch/services/energy_transfer_analysis/energy_transfer_entry.py +731 -0
  43. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/exergy_analysis/__init__.py +4 -0
  44. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/exergy_analysis/exergy_targeting_entry.py +102 -0
  45. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/common/preprocessing.py +4 -0
  46. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/common/shared.py +82 -13
  47. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/heat_pump_and_refrigeration_entry.py +4 -0
  48. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/targeting_services/cascade_vapour_compression.py +15 -8
  49. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/targeting_services/multi_simple_vapour_compression.py +22 -5
  50. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/targeting_services/multi_temperature_carnot.py +1 -1
  51. openpinch-0.2.4/OpenPinch/services/heat_pump_integration/targeting_services/vapour_compression_mvr.py +444 -0
  52. openpinch-0.2.4/OpenPinch/services/heat_pump_integration/unit_models/__init__.py +9 -0
  53. openpinch-0.2.4/OpenPinch/services/heat_pump_integration/unit_models/mechanical_vapour_recompression_cycle.py +716 -0
  54. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/unit_models/vapour_compression_cycle.py +3 -42
  55. openpinch-0.2.4/OpenPinch/services/heat_pump_integration/unit_models/vapour_compression_mvr_cascade.py +504 -0
  56. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/indirect_heat_integration/indirect_integration_entry.py +2 -2
  57. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/input_data_processing/_utility_preparation.py +61 -45
  58. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/input_data_processing/data_preparation.py +33 -35
  59. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/power_cogeneration/__init__.py +3 -1
  60. openpinch-0.2.2/OpenPinch/services/power_cogeneration/power_cogeneration_analysis.py → openpinch-0.2.4/OpenPinch/services/power_cogeneration/power_cogeneration_entry.py +123 -1
  61. openpinch-0.2.4/OpenPinch/services/services_entry.py +233 -0
  62. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/streamlit_webviewer/web_graphing.py +2 -1
  63. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/bb_optimisers/common.py +31 -13
  64. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/blackbox_minimisers.py +8 -2
  65. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/export.py +5 -0
  66. {openpinch-0.2.2 → openpinch-0.2.4}/PKG-INFO +6 -3
  67. {openpinch-0.2.2 → openpinch-0.2.4}/README.md +5 -2
  68. {openpinch-0.2.2 → openpinch-0.2.4}/pyproject.toml +2 -2
  69. openpinch-0.2.2/OpenPinch/services/heat_pump_integration/unit_models/__init__.py +0 -1
  70. openpinch-0.2.2/OpenPinch/services/power_cogeneration_analysis/power_cogeneration_analysis.py +0 -139
  71. openpinch-0.2.2/OpenPinch/services/services_entry.py +0 -430
  72. {openpinch-0.2.2 → openpinch-0.2.4}/.gitignore +0 -0
  73. {openpinch-0.2.2 → openpinch-0.2.4}/LICENSE +0 -0
  74. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/__main__.py +0 -0
  75. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/__init__.py +0 -0
  76. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_loading.py +0 -0
  77. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_output.py +0 -0
  78. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem/_result_extraction.py +0 -0
  79. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem_table/__init__.py +0 -0
  80. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_problem_table/_problem_table_constants.py +0 -0
  81. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_stream_collection/__init__.py +0 -0
  82. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_stream_collection/_helpers.py +0 -0
  83. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_workspace/__init__.py +0 -0
  84. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_workspace/execution.py +0 -0
  85. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_workspace/payloads.py +0 -0
  86. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/_workspace/views.py +0 -0
  87. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/pinch_workspace.py +0 -0
  88. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/classes/problem_table.py +0 -0
  89. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/__init__.py +0 -0
  90. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/notebooks/01_basic_pinch_and_dtcont_sensitivity.ipynb +0 -0
  91. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/notebooks/02_total_site_targets_and_sugcc.ipynb +0 -0
  92. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/notebooks/03_carnot_hpr_comparison.ipynb +0 -0
  93. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/notebooks/04_multistate_targeting_and_state_comparison.ipynb +0 -0
  94. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/notebooks/05_schema_service_and_output_workflows.ipynb +0 -0
  95. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/notebooks/__init__.py +0 -0
  96. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/__init__.py +0 -0
  97. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/basic_pinch.json +0 -0
  98. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/chocolate_factory.json +0 -0
  99. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/crude_preheat_train.json +0 -0
  100. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/crude_preheat_train_multistate.json +0 -0
  101. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/heat_pump_targeting.json +0 -0
  102. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/pulp_mill.json +0 -0
  103. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/zonal_site.json +0 -0
  104. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/data/sample_cases/zonal_site_multistate.json +0 -0
  105. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/problem_table_types.py +0 -0
  106. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/common.py +0 -0
  107. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/graphs.py +0 -0
  108. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/report_units.py +0 -0
  109. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/turbine.py +0 -0
  110. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/lib/schemas/workspace.py +0 -0
  111. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/main.py +0 -0
  112. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/resources.py +0 -0
  113. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/__init__.py +0 -0
  114. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/graph_series_meta.py +0 -0
  115. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/miscellaneous.py +0 -0
  116. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/common/temperature_driving_force.py +0 -0
  117. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/direct_heat_integration/__init__.py +0 -0
  118. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/__init__.py +0 -0
  119. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/common/__init__.py +0 -0
  120. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/common/encoding.py +0 -0
  121. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/common/layout.py +0 -0
  122. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/common/postprocessing.py +0 -0
  123. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/targeting_services/__init__.py +0 -0
  124. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/targeting_services/brayton.py +0 -0
  125. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/targeting_services/multi_simple_carnot.py +0 -0
  126. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/unit_models/brayton_heat_pump.py +0 -0
  127. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/unit_models/cascade_vapour_compression_cycle.py +0 -0
  128. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/heat_pump_integration/unit_models/parallel_vapour_compression_cycles.py +0 -0
  129. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/indirect_heat_integration/__init__.py +0 -0
  130. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/input_data_processing/__init__.py +0 -0
  131. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/input_data_processing/_canonicalization.py +0 -0
  132. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/services/power_cogeneration/unit_models/multi_stage_steam_turbine.py +0 -0
  133. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/__init__.py +0 -0
  134. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/_tabular_input.py +0 -0
  135. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/bb_optimisers/__init__.py +0 -0
  136. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/bb_optimisers/bayesian_optimisation.py +0 -0
  137. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/bb_optimisers/cmaes.py +0 -0
  138. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/bb_optimisers/dual_annealing.py +0 -0
  139. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/bb_optimisers/rbf_surrogate.py +0 -0
  140. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/costing.py +0 -0
  141. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/csv_to_json.py +0 -0
  142. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/decorators.py +0 -0
  143. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/heat_exchanger.py +0 -0
  144. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/input_validation.py +0 -0
  145. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/plots.py +0 -0
  146. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/stream_linearisation.py +0 -0
  147. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/value_resolution.py +0 -0
  148. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/water_properties.py +0 -0
  149. {openpinch-0.2.2 → openpinch-0.2.4}/OpenPinch/utils/wkbook_to_json.py +0 -0
@@ -29,6 +29,7 @@ from .lib import (
29
29
  DirectHeatPumpTarget,
30
30
  DirectIntegrationTarget,
31
31
  DirectRefrigerationTarget,
32
+ EnergyTransferTarget,
32
33
  GetInputOutputData,
33
34
  Graph,
34
35
  GraphCatalogEntry,
@@ -186,6 +187,7 @@ __all__ = [
186
187
  "DirectHeatPumpTarget",
187
188
  "DirectIntegrationTarget",
188
189
  "DirectRefrigerationTarget",
190
+ "EnergyTransferTarget",
189
191
  "HeatPumpTargetBase",
190
192
  "IndirectHeatPumpTarget",
191
193
  "IndirectRefrigerationTarget",
@@ -1,5 +1,6 @@
1
1
  """Private helpers for the :mod:`OpenPinch.classes.pinch_problem` surface."""
2
2
 
3
+ from ._component_accessor import _ComponentAccessorDescriptor
3
4
  from ._loading import (
4
5
  JsonDict,
5
6
  PathLike,
@@ -27,6 +28,7 @@ __all__ = [
27
28
  "_LoadedProblemSource",
28
29
  "PathLike",
29
30
  "_ProblemSourceAdapters",
31
+ "_ComponentAccessorDescriptor",
30
32
  "_PlotAccessorDescriptor",
31
33
  "_TargetAccessorDescriptor",
32
34
  "_validate_problem_semantics",
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Optional
4
+
5
+ if TYPE_CHECKING:
6
+ from ...services.components.process_mvr import ProcessMVRComponent
7
+ from ..pinch_problem import PinchProblem
8
+
9
+
10
+ class _ComponentAccessor:
11
+ """Helper namespace for mutating prepared process models with components."""
12
+
13
+ def __init__(self, problem: "PinchProblem") -> None:
14
+ self._problem = problem
15
+
16
+ def process_mvr(
17
+ self,
18
+ source_streams,
19
+ *,
20
+ mvr_id: Optional[str] = None,
21
+ n_stages: int = 1,
22
+ liquid_injection: bool = True,
23
+ mvr_stage_t_lift: Optional[float] = None,
24
+ mvr_stage_pressure_ratio: Optional[float] = None,
25
+ max_stage_t_sat_lift: Optional[float] = None,
26
+ options: Optional[dict[str, Any]] = None,
27
+ state_id: Optional[str] = None,
28
+ ) -> "ProcessMVRComponent":
29
+ """Add a direct process MVR component to selected hot gas stream(s)."""
30
+ from ...services.components.process_mvr import (
31
+ create_process_mvr_component,
32
+ )
33
+
34
+ return create_process_mvr_component(
35
+ self._problem,
36
+ source_streams=source_streams,
37
+ mvr_id=mvr_id,
38
+ n_stages=n_stages,
39
+ liquid_injection=liquid_injection,
40
+ mvr_stage_t_lift=mvr_stage_t_lift,
41
+ mvr_stage_pressure_ratio=mvr_stage_pressure_ratio,
42
+ max_stage_t_sat_lift=max_stage_t_sat_lift,
43
+ options=options,
44
+ state_id=state_id,
45
+ )
46
+
47
+
48
+ class _ComponentAccessorDescriptor:
49
+ """Descriptor returning a problem-bound component accessor."""
50
+
51
+ def __get__(self, obj: "PinchProblem" | None, owner=None):
52
+ if obj is None:
53
+ return self
54
+ return _ComponentAccessor(obj)
@@ -19,6 +19,9 @@ GraphRecord = Dict[str, Any]
19
19
  GraphPayload = Dict[str, GraphRecord]
20
20
 
21
21
  _GRAPH_TYPE_ALIASES = {
22
+ "etd": GT.ETD.value,
23
+ "energy transfer": GT.ETD.value,
24
+ "energy transfer diagram": GT.ETD.value,
22
25
  "cc": GT.CC.value,
23
26
  "composite": GT.CC.value,
24
27
  "composite curve": GT.CC.value,
@@ -410,6 +413,23 @@ class _PlotAccessor:
410
413
  return_graph_data=return_graph_data,
411
414
  )
412
415
 
416
+ def energy_transfer_diagram(
417
+ self,
418
+ *,
419
+ zone_name: Optional[str] = None,
420
+ index: float = 0,
421
+ show: bool = False,
422
+ return_graph_data: bool = False,
423
+ ):
424
+ """Return the first matching energy-transfer diagram payload or figure."""
425
+ return self._plot_graph(
426
+ zone_name=zone_name,
427
+ graph_type=GT.ETD.value,
428
+ index=index,
429
+ show=show,
430
+ return_graph_data=return_graph_data,
431
+ )
432
+
413
433
 
414
434
  class _PlotAccessorDescriptor:
415
435
  """Non-data descriptor exposing a callable plot accessor on instances."""
@@ -7,6 +7,7 @@ from ...services import (
7
7
  direct_heat_integration_service,
8
8
  direct_heat_pump_service,
9
9
  direct_refrigeration_service,
10
+ energy_transfer_analysis_service,
10
11
  exergy_targeting_service,
11
12
  indirect_heat_integration_service,
12
13
  indirect_heat_pump_service,
@@ -25,7 +26,10 @@ class _TargetAccessor:
25
26
  self._problem = problem
26
27
 
27
28
  def __call__(
28
- self, *, options: Optional[dict[str, Any]] = {}, state_id: Optional[str] = None
29
+ self,
30
+ *,
31
+ options: Optional[dict[str, Any]] = None,
32
+ state_id: Optional[str] = None,
29
33
  ):
30
34
  runtime_options = dict(options or {})
31
35
  if state_id is not None:
@@ -219,6 +223,26 @@ class _TargetAccessor:
219
223
  service_func=exergy_targeting_service,
220
224
  )
221
225
 
226
+ def energy_transfer(
227
+ self,
228
+ *,
229
+ zone_name: Optional[str] = None,
230
+ options: Optional[dict[str, Any]] = None,
231
+ include_subzones: bool = False,
232
+ state_id: Optional[str] = None,
233
+ ) -> BaseTargetModel:
234
+ """Create energy-transfer diagram and heat-surplus/deficit outputs."""
235
+ runtime_options = dict(options or {})
236
+ if state_id is not None:
237
+ runtime_options["state_id"] = state_id
238
+ return self._problem._execute_targeting(
239
+ target_id=TT.ET.value,
240
+ application_zone=zone_name,
241
+ options=runtime_options,
242
+ include_subzones=include_subzones,
243
+ direct_service_func=energy_transfer_analysis_service,
244
+ )
245
+
222
246
 
223
247
  class _TargetAccessorDescriptor:
224
248
  """Non-data descriptor exposing a callable target accessor on instances."""
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
+ from functools import lru_cache
6
7
  from typing import Callable
7
8
 
8
9
  from ...lib.enums import ZT
@@ -31,21 +32,26 @@ def run_targeting_for_zone_and_subzones(
31
32
 
32
33
  def _invoke_service(service_func: Callable, zone: Zone, args: dict | None = None):
33
34
  """Call a targeting callback with backward-compatible argument handling."""
34
- if not isinstance(service_func, Callable):
35
+ if not callable(service_func):
35
36
  return None
36
37
 
37
- if args is None:
38
+ if args is None or not _service_accepts_args(service_func):
38
39
  return service_func(zone)
39
40
 
41
+ return service_func(zone, args)
42
+
43
+
44
+ @lru_cache(maxsize=64)
45
+ def _service_accepts_args(service_func: Callable) -> bool:
46
+ """Return whether a service accepts a second positional ``args`` parameter."""
40
47
  try:
41
48
  signature = inspect.signature(service_func)
42
49
  except TypeError, ValueError:
43
- return service_func(zone, args)
50
+ return True
44
51
 
45
52
  params = list(signature.parameters.values())
46
- accepts_varargs = any(
47
- param.kind == inspect.Parameter.VAR_POSITIONAL for param in params
48
- )
53
+ if any(param.kind == inspect.Parameter.VAR_POSITIONAL for param in params):
54
+ return True
49
55
  positional_params = [
50
56
  param
51
57
  for param in params
@@ -55,10 +61,7 @@ def _invoke_service(service_func: Callable, zone: Zone, args: dict | None = None
55
61
  inspect.Parameter.POSITIONAL_OR_KEYWORD,
56
62
  )
57
63
  ]
58
-
59
- if accepts_varargs or len(positional_params) >= 2:
60
- return service_func(zone, args)
61
- return service_func(zone)
64
+ return len(positional_params) >= 2
62
65
 
63
66
 
64
67
  def _invoke_target_handler(
@@ -95,7 +98,7 @@ def _get_unit_operation_targets(
95
98
  for subzone in zone.subzones.values():
96
99
  if subzone.type == ZT.O.value:
97
100
  if subzone.config.DO_DIRECT_OPERATION_TARGETING:
98
- if isinstance(direct_service_func, Callable):
101
+ if callable(direct_service_func):
99
102
  _invoke_service(direct_service_func, subzone, args)
100
103
  else:
101
104
  raise ValueError(
@@ -103,7 +106,7 @@ def _get_unit_operation_targets(
103
106
  "contain other operation zones."
104
107
  )
105
108
 
106
- if isinstance(direct_service_func, Callable):
109
+ if callable(direct_service_func):
107
110
  _invoke_service(direct_service_func, zone, args)
108
111
 
109
112
  return zone
@@ -142,10 +145,10 @@ def _get_process_targets(
142
145
  )
143
146
 
144
147
  if zone.config.DO_INDIRECT_PROCESS_TARGETING:
145
- if isinstance(indirect_service_func, Callable):
148
+ if callable(indirect_service_func):
146
149
  _invoke_service(indirect_service_func, zone, args)
147
150
 
148
- if isinstance(direct_service_func, Callable):
151
+ if callable(direct_service_func):
149
152
  _invoke_service(direct_service_func, zone, args)
150
153
 
151
154
  return zone
@@ -159,7 +162,7 @@ def _get_site_targets(
159
162
  ):
160
163
  """Run site targeting over the full nested zone hierarchy."""
161
164
 
162
- if isinstance(direct_service_func, Callable):
165
+ if callable(direct_service_func):
163
166
  _invoke_service(direct_service_func, zone, args)
164
167
 
165
168
  if len(zone.subzones) > 0:
@@ -194,7 +197,7 @@ def _get_site_targets(
194
197
  "site, process, and operation zones."
195
198
  )
196
199
 
197
- if isinstance(indirect_service_func, Callable):
200
+ if callable(indirect_service_func):
198
201
  _invoke_service(indirect_service_func, zone, args)
199
202
 
200
203
  return zone
@@ -17,6 +17,48 @@ from ..value import Value
17
17
 
18
18
  ValidationContext = dict[str, list[dict[str, Any]]]
19
19
  _TEMPERATURE_EQUAL_TOL = 1e-12
20
+ _STREAM_VALUE_FIELDS = (
21
+ "t_supply",
22
+ "t_target",
23
+ "p_supply",
24
+ "p_target",
25
+ "h_supply",
26
+ "h_target",
27
+ "heat_flow",
28
+ "dt_cont",
29
+ "htc",
30
+ )
31
+ _STREAM_OPTIONAL_VALUE_FIELDS = (
32
+ "p_supply",
33
+ "p_target",
34
+ "h_supply",
35
+ "h_target",
36
+ "dt_cont",
37
+ "htc",
38
+ )
39
+ _UTILITY_VALUE_FIELDS = (
40
+ "t_supply",
41
+ "t_target",
42
+ "p_supply",
43
+ "p_target",
44
+ "h_supply",
45
+ "h_target",
46
+ "heat_flow",
47
+ "dt_cont",
48
+ "htc",
49
+ "price",
50
+ )
51
+ _UTILITY_OPTIONAL_VALUE_FIELDS = (
52
+ "t_target",
53
+ "p_supply",
54
+ "p_target",
55
+ "h_supply",
56
+ "h_target",
57
+ "heat_flow",
58
+ "dt_cont",
59
+ "htc",
60
+ "price",
61
+ )
20
62
 
21
63
 
22
64
  def validate_problem_inputs(
@@ -136,8 +178,8 @@ def semantic_issues(
136
178
  section="streams",
137
179
  record_index=index,
138
180
  record_label=label,
139
- field_names=("t_supply", "t_target", "heat_flow", "dt_cont", "htc"),
140
- optional_field_names=("dt_cont", "htc"),
181
+ field_names=_STREAM_VALUE_FIELDS,
182
+ optional_field_names=_STREAM_OPTIONAL_VALUE_FIELDS,
141
183
  config=input_unit_config,
142
184
  )
143
185
  issues.extend(stream_value_issues)
@@ -157,21 +199,8 @@ def semantic_issues(
157
199
  section="utilities",
158
200
  record_index=index,
159
201
  record_label=label,
160
- field_names=(
161
- "t_supply",
162
- "t_target",
163
- "heat_flow",
164
- "dt_cont",
165
- "htc",
166
- "price",
167
- ),
168
- optional_field_names=(
169
- "t_target",
170
- "heat_flow",
171
- "dt_cont",
172
- "htc",
173
- "price",
174
- ),
202
+ field_names=_UTILITY_VALUE_FIELDS,
203
+ optional_field_names=_UTILITY_OPTIONAL_VALUE_FIELDS,
175
204
  config=input_unit_config,
176
205
  )
177
206
  issues.extend(utility_value_issues)
@@ -435,51 +464,16 @@ def _validate_stream_record_states(
435
464
  record_label: Optional[str],
436
465
  ) -> list[ValidationIssue]:
437
466
  issues: list[ValidationIssue] = []
438
- issues.extend(
439
- _validate_value_finiteness(
440
- values.get("t_supply"),
441
- section=section,
442
- record_index=record_index,
443
- record_label=record_label,
444
- field_name="t_supply",
445
- )
446
- )
447
- issues.extend(
448
- _validate_value_finiteness(
449
- values.get("t_target"),
450
- section=section,
451
- record_index=record_index,
452
- record_label=record_label,
453
- field_name="t_target",
454
- )
455
- )
456
- issues.extend(
457
- _validate_value_finiteness(
458
- values.get("heat_flow"),
459
- section=section,
460
- record_index=record_index,
461
- record_label=record_label,
462
- field_name="heat_flow",
463
- )
464
- )
465
- issues.extend(
466
- _validate_value_finiteness(
467
- values.get("dt_cont"),
468
- section=section,
469
- record_index=record_index,
470
- record_label=record_label,
471
- field_name="dt_cont",
472
- )
473
- )
474
- issues.extend(
475
- _validate_value_finiteness(
476
- values.get("htc"),
477
- section=section,
478
- record_index=record_index,
479
- record_label=record_label,
480
- field_name="htc",
467
+ for field_name in _STREAM_VALUE_FIELDS:
468
+ issues.extend(
469
+ _validate_value_finiteness(
470
+ values.get(field_name),
471
+ section=section,
472
+ record_index=record_index,
473
+ record_label=record_label,
474
+ field_name=field_name,
475
+ )
481
476
  )
482
- )
483
477
  issues.extend(
484
478
  _validate_non_negative_states(
485
479
  values.get("heat_flow"),
@@ -522,7 +516,7 @@ def _validate_stream_record_states(
522
516
  classifications: dict[str, list[str]] = {
523
517
  ST.Hot.value: [],
524
518
  ST.Cold.value: [],
525
- ST.Both.value: [],
519
+ ST.Neutral.value: [],
526
520
  }
527
521
 
528
522
  num_states = 1
@@ -558,6 +552,10 @@ def _validate_stream_record_states(
558
552
  ),
559
553
  )
560
554
  )
555
+ elif t_supply_value > t_target_value:
556
+ classifications[ST.Hot.value].append(str(idx))
557
+ elif t_supply_value < t_target_value:
558
+ classifications[ST.Cold.value].append(str(idx))
561
559
 
562
560
  active_classes = {
563
561
  stream_type: state_group
@@ -577,7 +575,7 @@ def _validate_stream_record_states(
577
575
  "Stream states must classify consistently. "
578
576
  f"Hot={classifications[ST.Hot.value]}, "
579
577
  f"Cold={classifications[ST.Cold.value]}, "
580
- f"Neutral={classifications[ST.Both.value]}."
578
+ f"Neutral={classifications[ST.Neutral.value]}."
581
579
  ),
582
580
  )
583
581
  )
@@ -593,7 +591,7 @@ def _validate_utility_record_states(
593
591
  record_label: Optional[str],
594
592
  ) -> list[ValidationIssue]:
595
593
  issues: list[ValidationIssue] = []
596
- for field_name in ("t_supply", "t_target", "heat_flow", "dt_cont", "htc", "price"):
594
+ for field_name in _UTILITY_VALUE_FIELDS:
597
595
  issues.extend(
598
596
  _validate_value_finiteness(
599
597
  values.get(field_name),
@@ -0,0 +1,81 @@
1
+ """Numeric snapshot helpers for :class:`StreamCollection` internals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+
9
+ from ...lib.enums import ST
10
+ from ..value import Value
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class StreamCollectionNumericView:
15
+ """Dense numeric snapshot of stream properties for internal kernels."""
16
+
17
+ t_min: np.ndarray
18
+ t_max: np.ndarray
19
+ t_min_star: np.ndarray
20
+ t_max_star: np.ndarray
21
+ cp: np.ndarray
22
+ rcp: np.ndarray
23
+ heat_flow: np.ndarray
24
+ dt_cont: np.ndarray
25
+ is_hot: np.ndarray
26
+ is_cold: np.ndarray
27
+ active: np.ndarray
28
+
29
+
30
+ def build_numeric_view(streams: list[object], idx: int | None = None):
31
+ """Build a dense numeric view from stream objects."""
32
+ n_streams = len(streams)
33
+ t_min = np.empty(n_streams, dtype=float)
34
+ t_max = np.empty(n_streams, dtype=float)
35
+ t_min_star = np.empty(n_streams, dtype=float)
36
+ t_max_star = np.empty(n_streams, dtype=float)
37
+ cp = np.empty(n_streams, dtype=float)
38
+ rcp = np.empty(n_streams, dtype=float)
39
+ heat_flow = np.empty(n_streams, dtype=float)
40
+ dt_cont = np.empty(n_streams, dtype=float)
41
+ is_hot = np.empty(n_streams, dtype=bool)
42
+ is_cold = np.empty(n_streams, dtype=bool)
43
+ active = np.empty(n_streams, dtype=bool)
44
+
45
+ for stream_idx, stream in enumerate(streams):
46
+ t_min[stream_idx] = value_at_idx(stream._t_min, idx)
47
+ t_max[stream_idx] = value_at_idx(stream._t_max, idx)
48
+ t_min_star[stream_idx] = value_at_idx(stream._t_min_star, idx)
49
+ t_max_star[stream_idx] = value_at_idx(stream._t_max_star, idx)
50
+ cp[stream_idx] = value_at_idx(stream._cp, idx)
51
+ rcp[stream_idx] = value_at_idx(stream._rcp_prod, idx)
52
+ heat_flow[stream_idx] = value_at_idx(stream._heat_flow, idx)
53
+ dt_cont[stream_idx] = value_at_idx(stream._dt_cont, idx)
54
+ is_hot[stream_idx] = stream.type == ST.Hot.value
55
+ is_cold[stream_idx] = stream.type == ST.Cold.value
56
+ active[stream_idx] = bool(stream.active)
57
+
58
+ return StreamCollectionNumericView(
59
+ t_min=t_min,
60
+ t_max=t_max,
61
+ t_min_star=t_min_star,
62
+ t_max_star=t_max_star,
63
+ cp=cp,
64
+ rcp=rcp,
65
+ heat_flow=heat_flow,
66
+ dt_cont=dt_cont,
67
+ is_hot=is_hot,
68
+ is_cold=is_cold,
69
+ active=active,
70
+ )
71
+
72
+
73
+ def value_at_idx(value: Value | None, idx: int | None = None) -> float:
74
+ """Return one canonical numeric magnitude from a scalar or stateful value."""
75
+ if value is None:
76
+ return np.nan
77
+ magnitudes = value._quantity.magnitude
78
+ if magnitudes.size == 1:
79
+ return float(magnitudes[0])
80
+ state_idx = 0 if idx is None else int(idx)
81
+ return float(magnitudes[state_idx])
@@ -30,6 +30,7 @@ from ..utils.wkbook_to_json import get_problem_from_excel
30
30
  from ._problem import (
31
31
  JsonDict,
32
32
  PathLike,
33
+ _ComponentAccessorDescriptor,
33
34
  _LoadedProblemSource,
34
35
  _PlotAccessorDescriptor,
35
36
  _ProblemSourceAdapters,
@@ -65,8 +66,10 @@ class PinchProblem:
65
66
  _results: Optional[TargetOutput]
66
67
  _validated_data: Optional[TargetInput]
67
68
  _master_zone: Optional["Zone"]
69
+ _process_components: dict[str, Any]
68
70
  _input_source_kind: str
69
71
  _validation_context: Optional[dict[str, list[dict[str, Any]]]]
72
+ add_component = _ComponentAccessorDescriptor()
70
73
  plot = _PlotAccessorDescriptor()
71
74
  target = _TargetAccessorDescriptor()
72
75
 
@@ -86,6 +89,7 @@ class PinchProblem:
86
89
  self._results = None
87
90
  self._validated_data = None
88
91
  self._master_zone = None
92
+ self._process_components = {}
89
93
  self.results_dir = None
90
94
 
91
95
  if source is not None:
@@ -132,6 +136,7 @@ class PinchProblem:
132
136
  indirect_service_func=indirect_service_func,
133
137
  args=runtime_options,
134
138
  )
139
+ self._attach_process_component_work_targets(zone, runtime_options)
135
140
  self._results = TargetOutput.model_validate(extract_results(zone, state_id=sid))
136
141
  return self._results
137
142
 
@@ -167,6 +172,10 @@ class PinchProblem:
167
172
  direct_service_func(zone, runtime_options)
168
173
  if indirect_service_func is not None:
169
174
  indirect_service_func(zone, runtime_options)
175
+ self._attach_process_component_work_targets(
176
+ execution_master_zone,
177
+ runtime_options,
178
+ )
170
179
  self._results = TargetOutput.model_validate(
171
180
  extract_results(execution_master_zone, state_id=sid)
172
181
  )
@@ -207,6 +216,10 @@ class PinchProblem:
207
216
  else:
208
217
  if service_func is not None:
209
218
  service_func(zone, runtime_options)
219
+ self._attach_process_component_work_targets(
220
+ execution_master_zone,
221
+ runtime_options,
222
+ )
210
223
  self._results = TargetOutput.model_validate(
211
224
  extract_results(execution_master_zone, state_id=sid)
212
225
  )
@@ -273,6 +286,10 @@ class PinchProblem:
273
286
  elif service_func is not None:
274
287
  service_func(zone, runtime_options)
275
288
 
289
+ self._attach_process_component_work_targets(
290
+ execution_master_zone,
291
+ runtime_options,
292
+ )
276
293
  self._results = TargetOutput.model_validate(
277
294
  extract_results(execution_master_zone, state_id=sid)
278
295
  )
@@ -308,6 +325,51 @@ class PinchProblem:
308
325
  return selected_master_zone
309
326
  return selected_master_zone.get_subzone(application_zone)
310
327
 
328
+ def _attach_process_component_work_targets(
329
+ self,
330
+ zone: "Zone",
331
+ runtime_options: Optional[dict[str, Any]],
332
+ ) -> None:
333
+ if not self._process_components:
334
+ return
335
+ state_id = (runtime_options or {}).get("state_id")
336
+ state_idx = (runtime_options or {}).get("idx")
337
+ for current_zone in self._walk_zone_tree(zone):
338
+ component_work = self._process_component_work_for_zone(
339
+ current_zone,
340
+ state_id=state_id,
341
+ state_idx=state_idx,
342
+ )
343
+ for target in current_zone.targets.values():
344
+ if hasattr(target, "process_component_work_target"):
345
+ target.process_component_work_target = component_work
346
+ if (
347
+ component_work > 0.0
348
+ and hasattr(target, "work_target")
349
+ and getattr(target, "work_target", None) is None
350
+ ):
351
+ target.work_target = component_work
352
+
353
+ def _process_component_work_for_zone(
354
+ self,
355
+ zone: "Zone",
356
+ *,
357
+ state_id: str | None,
358
+ state_idx: int | None,
359
+ ) -> float:
360
+ total = 0.0
361
+ for component in self._process_components.values():
362
+ work_for_zone = getattr(component, "work_for_zone", None)
363
+ if work_for_zone is None:
364
+ continue
365
+ total += float(work_for_zone(zone, state_id=state_id, state_idx=state_idx))
366
+ return total
367
+
368
+ def _walk_zone_tree(self, zone: "Zone"):
369
+ yield zone
370
+ for subzone in zone.subzones.values():
371
+ yield from self._walk_zone_tree(subzone)
372
+
311
373
  def _build_execution_master_zone(self) -> "Zone":
312
374
  if self._problem_data is None and self._master_zone is None:
313
375
  raise RuntimeError("No input loaded. Call load(...) first.")
@@ -374,7 +436,7 @@ class PinchProblem:
374
436
  options: Optional[dict[str, Any]],
375
437
  *,
376
438
  zone: "Zone",
377
- ) -> tuple[dict[str, Any], str | None, int]:
439
+ ) -> tuple[dict[str, Any], str | None]:
378
440
  runtime_options = dict(options or {})
379
441
  idx, sid = get_state_index(state_ids=zone.state_ids, args=runtime_options)
380
442
  runtime_options["idx"] = idx
@@ -583,6 +645,11 @@ class PinchProblem:
583
645
  """Return the prepared root zone after a successful ``load()`` pass."""
584
646
  return self._master_zone
585
647
 
648
+ @property
649
+ def process_components(self) -> dict[str, Any]:
650
+ """Memory-only process components applied to the prepared model."""
651
+ return self._process_components
652
+
586
653
  @property
587
654
  def hot_streams(self) -> StreamCollection:
588
655
  """Hot process streams on the root analysis zone."""
@@ -755,6 +822,7 @@ class PinchProblem:
755
822
  """Revalidate, reconstruct the zone tree, and clear cached results."""
756
823
  self._validated_data = self.validate()
757
824
  self._master_zone = self._data_preprocessing()
825
+ self._process_components = {}
758
826
  self._results = None
759
827
  return self._master_zone
760
828