OpenPinch 0.2.1__tar.gz → 0.2.2__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 (137) hide show
  1. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/__init__.py +4 -2
  2. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem/__init__.py +4 -0
  3. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem/_output.py +58 -36
  4. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem/_plot_accessor.py +40 -0
  5. openpinch-0.2.2/OpenPinch/classes/_problem/_result_extraction.py +48 -0
  6. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem/_target_accessor.py +21 -0
  7. openpinch-0.2.1/OpenPinch/utils/multiscale_targeting.py → openpinch-0.2.2/OpenPinch/classes/_problem/_target_dispatch.py +11 -57
  8. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem/_validation.py +11 -1
  9. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_stream_collection/_helpers.py +2 -0
  10. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_workspace/views.py +16 -1
  11. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/pinch_problem.py +106 -12
  12. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/stream.py +1 -1
  13. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/stream_collection.py +2 -0
  14. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/value.py +22 -10
  15. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/zone.py +2 -0
  16. openpinch-0.2.2/OpenPinch/data/notebooks/01_basic_pinch_and_dtcont_sensitivity.ipynb +224 -0
  17. openpinch-0.2.2/OpenPinch/data/notebooks/02_total_site_targets_and_sugcc.ipynb +343 -0
  18. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/notebooks/03_carnot_hpr_comparison.ipynb +74 -66
  19. openpinch-0.2.2/OpenPinch/data/notebooks/04_multistate_targeting_and_state_comparison.ipynb +297 -0
  20. openpinch-0.2.2/OpenPinch/data/notebooks/05_schema_service_and_output_workflows.ipynb +284 -0
  21. openpinch-0.2.2/OpenPinch/data/sample_cases/crude_preheat_train_multistate.json +306 -0
  22. openpinch-0.2.2/OpenPinch/data/sample_cases/zonal_site_multistate.json +954 -0
  23. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/__init__.py +2 -2
  24. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/config_metadata.py +2 -0
  25. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/enums.py +5 -8
  26. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/problem_table_types.py +2 -0
  27. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/__init__.py +13 -3
  28. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/common.py +1 -2
  29. openpinch-0.2.2/OpenPinch/lib/schemas/report_units.py +169 -0
  30. openpinch-0.2.2/OpenPinch/lib/schemas/reporting.py +144 -0
  31. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/targets.py +192 -52
  32. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/workspace.py +2 -0
  33. openpinch-0.2.2/OpenPinch/lib/unit_system.py +265 -0
  34. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/__init__.py +6 -0
  35. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/capital_cost_and_area_targeting.py +3 -1
  36. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/gcc_manipulation.py +3 -1
  37. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/graph_data.py +122 -7
  38. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/graph_series_meta.py +18 -1
  39. openpinch-0.2.2/OpenPinch/services/common/miscellaneous.py +166 -0
  40. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/problem_table_analysis.py +8 -11
  41. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/temperature_driving_force.py +3 -1
  42. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/utility_targeting.py +2 -0
  43. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/direct_heat_integration/direct_integration_entry.py +3 -1
  44. openpinch-0.2.2/OpenPinch/services/exergy_analysis/__init__.py +15 -0
  45. openpinch-0.2.2/OpenPinch/services/exergy_analysis/exergy_targeting_entry.py +450 -0
  46. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/common/layout.py +2 -0
  47. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/common/preprocessing.py +2 -1
  48. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/common/shared.py +4 -2
  49. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/heat_pump_and_refrigeration_entry.py +11 -8
  50. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/targeting_services/cascade_vapour_compression.py +2 -0
  51. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/targeting_services/multi_simple_carnot.py +2 -0
  52. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/targeting_services/multi_simple_vapour_compression.py +2 -0
  53. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/indirect_heat_integration/indirect_integration_entry.py +3 -1
  54. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/input_data_processing/_utility_preparation.py +52 -24
  55. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/input_data_processing/data_preparation.py +24 -11
  56. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/power_cogeneration/power_cogeneration_analysis.py +1 -1
  57. openpinch-0.2.2/OpenPinch/services/power_cogeneration_analysis/power_cogeneration_analysis.py +139 -0
  58. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/services_entry.py +95 -5
  59. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/__init__.py +14 -21
  60. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/_tabular_input.py +3 -3
  61. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/blackbox_minimisers.py +2 -0
  62. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/export.py +77 -82
  63. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/heat_exchanger.py +2 -0
  64. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/plots.py +52 -1
  65. openpinch-0.2.2/OpenPinch/utils/value_resolution.py +418 -0
  66. {openpinch-0.2.1 → openpinch-0.2.2}/PKG-INFO +21 -9
  67. {openpinch-0.2.1 → openpinch-0.2.2}/README.md +20 -8
  68. {openpinch-0.2.1 → openpinch-0.2.2}/pyproject.toml +3 -1
  69. openpinch-0.2.1/OpenPinch/data/notebooks/01_basic_pinch_and_dtcont_sensitivity.ipynb +0 -286
  70. openpinch-0.2.1/OpenPinch/data/notebooks/02_total_site_targets_and_sugcc.ipynb +0 -474
  71. openpinch-0.2.1/OpenPinch/lib/schemas/reporting.py +0 -74
  72. openpinch-0.2.1/OpenPinch/services/exergy_analysis/__init__.py +0 -5
  73. openpinch-0.2.1/OpenPinch/services/exergy_analysis/exergy_targeting_entry.py +0 -124
  74. openpinch-0.2.1/OpenPinch/services/heat_pump_integration/targeting_services/brayton.py +0 -209
  75. openpinch-0.2.1/OpenPinch/utils/miscellaneous.py +0 -489
  76. {openpinch-0.2.1 → openpinch-0.2.2}/.gitignore +0 -0
  77. {openpinch-0.2.1 → openpinch-0.2.2}/LICENSE +0 -0
  78. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/__main__.py +0 -0
  79. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/__init__.py +0 -0
  80. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem/_loading.py +0 -0
  81. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem_table/__init__.py +0 -0
  82. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_problem_table/_problem_table_constants.py +0 -0
  83. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_stream_collection/__init__.py +0 -0
  84. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_workspace/__init__.py +0 -0
  85. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_workspace/execution.py +0 -0
  86. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/_workspace/payloads.py +0 -0
  87. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/pinch_workspace.py +0 -0
  88. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/classes/problem_table.py +0 -0
  89. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/__init__.py +0 -0
  90. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/notebooks/__init__.py +0 -0
  91. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/__init__.py +0 -0
  92. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/basic_pinch.json +0 -0
  93. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/chocolate_factory.json +0 -0
  94. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/crude_preheat_train.json +0 -0
  95. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/heat_pump_targeting.json +0 -0
  96. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/pulp_mill.json +0 -0
  97. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/data/sample_cases/zonal_site.json +0 -0
  98. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/config.py +0 -0
  99. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/graphs.py +0 -0
  100. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/hpr.py +0 -0
  101. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/io.py +0 -0
  102. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/lib/schemas/turbine.py +0 -0
  103. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/main.py +0 -0
  104. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/resources.py +0 -0
  105. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/common/__init__.py +0 -0
  106. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/direct_heat_integration/__init__.py +0 -0
  107. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/__init__.py +0 -0
  108. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/common/__init__.py +0 -0
  109. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/common/encoding.py +0 -0
  110. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/common/postprocessing.py +0 -0
  111. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/targeting_services/__init__.py +0 -0
  112. {openpinch-0.2.1/OpenPinch/services/heat_pump_integration/cycles → openpinch-0.2.2/OpenPinch/services/heat_pump_integration/targeting_services}/brayton.py +0 -0
  113. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/targeting_services/multi_temperature_carnot.py +0 -0
  114. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/unit_models/__init__.py +0 -0
  115. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/unit_models/brayton_heat_pump.py +0 -0
  116. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/unit_models/cascade_vapour_compression_cycle.py +0 -0
  117. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/unit_models/parallel_vapour_compression_cycles.py +0 -0
  118. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/heat_pump_integration/unit_models/vapour_compression_cycle.py +0 -0
  119. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/indirect_heat_integration/__init__.py +0 -0
  120. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/input_data_processing/__init__.py +0 -0
  121. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/input_data_processing/_canonicalization.py +0 -0
  122. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/power_cogeneration/__init__.py +0 -0
  123. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/services/power_cogeneration/unit_models/multi_stage_steam_turbine.py +0 -0
  124. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/streamlit_webviewer/web_graphing.py +0 -0
  125. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/bb_optimisers/__init__.py +0 -0
  126. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/bb_optimisers/bayesian_optimisation.py +0 -0
  127. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/bb_optimisers/cmaes.py +0 -0
  128. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/bb_optimisers/common.py +0 -0
  129. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/bb_optimisers/dual_annealing.py +0 -0
  130. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/bb_optimisers/rbf_surrogate.py +0 -0
  131. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/costing.py +0 -0
  132. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/csv_to_json.py +0 -0
  133. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/decorators.py +0 -0
  134. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/input_validation.py +0 -0
  135. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/stream_linearisation.py +0 -0
  136. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/water_properties.py +0 -0
  137. {openpinch-0.2.1 → openpinch-0.2.2}/OpenPinch/utils/wkbook_to_json.py +0 -0
@@ -1,5 +1,7 @@
1
1
  """OpenPinch public API."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import warnings as _warnings
4
6
 
5
7
  from . import lib
@@ -55,6 +57,7 @@ from .lib import (
55
57
  MaybeVU,
56
58
  NonLinearStream,
57
59
  PayloadRecordView,
60
+ PinchTemp,
58
61
  PinchWorkspaceBundle,
59
62
  ProblemTableColumnKey,
60
63
  ProblemTableColumnUpdates,
@@ -82,7 +85,6 @@ from .lib import (
82
85
  TargetOutput,
83
86
  TargetResults,
84
87
  TargetType,
85
- TempPinch,
86
88
  THSchema,
87
89
  TotalProcessTarget,
88
90
  TotalSiteTarget,
@@ -178,7 +180,7 @@ __all__ = [
178
180
  "ZoneTreeSchema",
179
181
  "HeatUtility",
180
182
  "TargetResults",
181
- "TempPinch",
183
+ "PinchTemp",
182
184
  "AnyTargetModel",
183
185
  "BaseTargetModel",
184
186
  "DirectHeatPumpTarget",
@@ -14,7 +14,9 @@ from ._output import (
14
14
  locate_summary_row,
15
15
  )
16
16
  from ._plot_accessor import _PlotAccessorDescriptor
17
+ from ._result_extraction import extract_results
17
18
  from ._target_accessor import _TargetAccessorDescriptor
19
+ from ._target_dispatch import run_targeting_for_zone_and_subzones
18
20
  from ._validation import (
19
21
  _validate_problem_semantics,
20
22
  format_schema_validation_error,
@@ -30,8 +32,10 @@ __all__ = [
30
32
  "_validate_problem_semantics",
31
33
  "build_graph_payload",
32
34
  "build_problem_summary_frame",
35
+ "extract_results",
33
36
  "format_schema_validation_error",
34
37
  "load_problem_source",
35
38
  "locate_summary_row",
36
39
  "prepare_in_memory_problem_source",
40
+ "run_targeting_for_zone_and_subzones",
37
41
  ]
@@ -6,8 +6,8 @@ from typing import Any, Optional
6
6
 
7
7
  import pandas as pd
8
8
 
9
+ from ...lib.schemas.report_units import split_report_value
9
10
  from ...utils.export import build_summary_dataframe
10
- from ...utils.miscellaneous import get_value
11
11
 
12
12
 
13
13
  def build_problem_summary_frame(
@@ -21,40 +21,55 @@ def build_problem_summary_frame(
21
21
 
22
22
  rows = []
23
23
  for target in results.targets:
24
+ idx = getattr(target, "idx", None)
25
+ qh_value, qh_unit = split_report_value(target.Qh, idx=idx)
26
+ qc_value, qc_unit = split_report_value(target.Qc, idx=idx)
27
+ qr_value, qr_unit = split_report_value(target.Qr, idx=idx)
28
+ hot_pinch_value, hot_pinch_unit = split_report_value(
29
+ target.pinch_temp.hot_temp,
30
+ idx=idx,
31
+ )
32
+ cold_pinch_value, cold_pinch_unit = split_report_value(
33
+ target.pinch_temp.cold_temp,
34
+ idx=idx,
35
+ )
24
36
  rows.append(
25
37
  {
26
38
  "Target": target.name,
27
39
  "State ID": getattr(target, "state_id", None),
28
- "Hot Utility Target": maybe_get_value(
29
- target.Qh, state_id=getattr(target, "state_id", None)
40
+ "Hot Utility Target": format_res(
41
+ value=qh_value,
42
+ unit=qh_unit,
30
43
  ),
31
- "Cold Utility Target": maybe_get_value(
32
- target.Qc, state_id=getattr(target, "state_id", None)
44
+ "Cold Utility Target": format_res(
45
+ value=qc_value,
46
+ unit=qc_unit,
33
47
  ),
34
- "Heat Recovery": maybe_get_value(
35
- target.Qr, state_id=getattr(target, "state_id", None)
48
+ "Heat Recovery": format_res(
49
+ value=qr_value,
50
+ unit=qr_unit,
36
51
  ),
37
- "Hot Pinch": maybe_get_value(
38
- target.temp_pinch.hot_temp,
39
- state_id=getattr(target, "state_id", None),
52
+ "Hot Pinch": format_res(
53
+ value=hot_pinch_value,
54
+ unit=hot_pinch_unit,
40
55
  ),
41
- "Cold Pinch": maybe_get_value(
42
- target.temp_pinch.cold_temp,
43
- state_id=getattr(target, "state_id", None),
56
+ "Cold Pinch": format_res(
57
+ value=cold_pinch_value,
58
+ unit=cold_pinch_unit,
44
59
  ),
45
60
  "Hot Utilities": ", ".join(
46
- format_utility(
47
- utility.name,
48
- utility.heat_flow,
49
- state_id=getattr(target, "state_id", None),
61
+ format_res(
62
+ name=utility.name,
63
+ value=utility.heat_flow,
64
+ idx=idx,
50
65
  )
51
66
  for utility in target.hot_utilities
52
67
  ),
53
68
  "Cold Utilities": ", ".join(
54
- format_utility(
55
- utility.name,
56
- utility.heat_flow,
57
- state_id=getattr(target, "state_id", None),
69
+ format_res(
70
+ name=utility.name,
71
+ value=utility.heat_flow,
72
+ idx=idx,
58
73
  )
59
74
  for utility in target.cold_utilities
60
75
  ),
@@ -106,20 +121,27 @@ def build_graph_payload(results: Any) -> Optional[dict[str, Any]]:
106
121
  }
107
122
 
108
123
 
109
- def maybe_get_value(value: Any, state_id: str | None = None) -> Any:
110
- """Return the scalar value for OpenPinch value objects."""
111
- if value is None:
112
- return None
113
- return get_value(value, state_id=state_id)
114
-
115
-
116
- def format_utility(
117
- name: str,
118
- heat_flow: Any,
119
- state_id: str | None = None,
124
+ def format_res(
125
+ name: str | None = None,
126
+ value: Any | None = None,
127
+ idx: int | None = None,
128
+ unit: str | None = None,
120
129
  ) -> str:
121
130
  """Render one utility summary item."""
122
- value = maybe_get_value(heat_flow, state_id=state_id)
123
- if value is None:
124
- return f"{name}: n/a"
125
- return f"{name}: {value:.2f}"
131
+ val, unt = split_report_value(value, idx=idx)
132
+ if unt is None:
133
+ unt = unit
134
+ if val is None:
135
+ if name:
136
+ return f"{name}: n/a"
137
+ else:
138
+ return "n/a"
139
+ if unt is None:
140
+ if name:
141
+ return f"{name}: {float(val):.2f}"
142
+ else:
143
+ return f"{float(val):.2f}"
144
+ if name:
145
+ return f"{name}: {float(val):.2f} {unt}"
146
+ else:
147
+ return f"{float(val):.2f} {unt}"
@@ -36,6 +36,9 @@ _GRAPH_TYPE_ALIASES = {
36
36
  "gcc": GT.GCC.value,
37
37
  "grand composite": GT.GCC.value,
38
38
  "grand composite curve": GT.GCC.value,
39
+ "gcc_x": GT.GCC_X.value,
40
+ "exergy gcc": GT.GCC_X.value,
41
+ "exergetic grand composite curve": GT.GCC_X.value,
39
42
  "nlc": GT.NLP.value,
40
43
  "net load": GT.NLP.value,
41
44
  "net load curve": GT.NLP.value,
@@ -47,6 +50,9 @@ _GRAPH_TYPE_ALIASES = {
47
50
  "net_load curves": GT.NLP.value,
48
51
  "net_load profile": GT.NLP.value,
49
52
  "net_load profiles": GT.NLP.value,
53
+ "nlp_x": GT.NLP_X.value,
54
+ "exergy nlp": GT.NLP_X.value,
55
+ "exergetic net load profiles": GT.NLP_X.value,
50
56
  "tsp": GT.TSP.value,
51
57
  "total site": GT.TSP.value,
52
58
  "total site profiles": GT.TSP.value,
@@ -302,6 +308,23 @@ class _PlotAccessor:
302
308
  return_graph_data=return_graph_data,
303
309
  )
304
310
 
311
+ def exergetic_grand_composite_curve(
312
+ self,
313
+ *,
314
+ zone_name: Optional[str] = None,
315
+ index: float = 0,
316
+ show: bool = False,
317
+ return_graph_data: bool = False,
318
+ ):
319
+ """Return the first matching exergetic GCC output or figure."""
320
+ return self._plot_graph(
321
+ zone_name=zone_name,
322
+ graph_type=GT.GCC_X.value,
323
+ index=index,
324
+ show=show,
325
+ return_graph_data=return_graph_data,
326
+ )
327
+
305
328
  def grand_composite_curve_with_heat_pump(
306
329
  self,
307
330
  *,
@@ -336,6 +359,23 @@ class _PlotAccessor:
336
359
  return_graph_data=return_graph_data,
337
360
  )
338
361
 
362
+ def exergetic_net_load_profiles(
363
+ self,
364
+ *,
365
+ zone_name: Optional[str] = None,
366
+ index: float = 0,
367
+ show: bool = False,
368
+ return_graph_data: bool = False,
369
+ ):
370
+ """Return the first matching exergetic NLP payload or figure."""
371
+ return self._plot_graph(
372
+ zone_name=zone_name,
373
+ graph_type=GT.NLP_X.value,
374
+ index=index,
375
+ show=show,
376
+ return_graph_data=return_graph_data,
377
+ )
378
+
339
379
  def total_site_profiles(
340
380
  self,
341
381
  *,
@@ -0,0 +1,48 @@
1
+ """Helpers for converting solved zone trees into serializable result payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+
7
+ from ...services.common.graph_data import get_output_graph_data
8
+ from ..stream import Stream
9
+ from ..stream_collection import StreamCollection
10
+ from ..zone import Zone
11
+
12
+ __all__ = ["extract_results"]
13
+
14
+
15
+ def extract_results(zone: Zone, state_id: str | None = None) -> dict:
16
+ """Serialise solved targets, generated utilities, and graph payloads."""
17
+ return {
18
+ "name": zone.name,
19
+ "state_id": state_id,
20
+ "targets": _get_report(zone, state_id=state_id),
21
+ "utilities": _get_utilities(zone),
22
+ "graphs": get_output_graph_data(zone),
23
+ }
24
+
25
+
26
+ def _get_report(zone: Zone, state_id: str | None = None) -> dict:
27
+ """Create the report payload from one zone and all nested subzones."""
28
+ targets: List[dict] = []
29
+
30
+ for target in zone.targets.values():
31
+ target_payload = target.serialize_json()
32
+ if state_id is not None:
33
+ target_payload["state_id"] = state_id
34
+ targets.append(target_payload)
35
+
36
+ if len(zone.subzones) > 0:
37
+ for subzone in zone.subzones.values():
38
+ targets.extend(_get_report(subzone, state_id=state_id))
39
+
40
+ return targets
41
+
42
+
43
+ def _get_utilities(zone: Zone) -> StreamCollection:
44
+ """Get any default utilities generated during the analysis."""
45
+ utilities: StreamCollection = zone.hot_utilities + zone.cold_utilities
46
+ default_hu: Stream = next((u for u in utilities if u.name == "HU"), None)
47
+ default_cu: Stream = next((u for u in utilities if u.name == "CU"), None)
48
+ return [default_hu, default_cu]
@@ -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
+ exergy_targeting_service,
10
11
  indirect_heat_integration_service,
11
12
  indirect_heat_pump_service,
12
13
  indirect_refrigeration_service,
@@ -198,6 +199,26 @@ class _TargetAccessor:
198
199
  direct_service_func=area_cost_targeting_service,
199
200
  )
200
201
 
202
+ def exergy(
203
+ self,
204
+ *,
205
+ zone_name: Optional[str] = None,
206
+ options: Optional[dict[str, Any]] = None,
207
+ include_subzones: bool = False,
208
+ state_id: Optional[str] = None,
209
+ ) -> BaseTargetModel:
210
+ """Run exergy targeting on the first compatible base target family."""
211
+ runtime_options = dict(options or {})
212
+ runtime_options["DO_EXERGY_TARGETING"] = True
213
+ if state_id is not None:
214
+ runtime_options["state_id"] = state_id
215
+ return self._problem._execute_exergy_targeting(
216
+ application_zone=zone_name,
217
+ options=runtime_options,
218
+ include_subzones=include_subzones,
219
+ service_func=exergy_targeting_service,
220
+ )
221
+
201
222
 
202
223
  class _TargetAccessorDescriptor:
203
224
  """Non-data descriptor exposing a callable target accessor on instances."""
@@ -1,21 +1,17 @@
1
- """Recursive dispatch helpers for zone-scale targeting and result extraction."""
1
+ """Recursive dispatch helpers for zone-scale targeting across subzones."""
2
+
3
+ from __future__ import annotations
2
4
 
3
5
  import inspect
4
- from typing import Callable, List
6
+ from typing import Callable
5
7
 
6
- from ..classes.stream import Stream
7
- from ..classes.stream_collection import StreamCollection
8
- from ..classes.zone import Zone
9
- from ..lib.enums import ZT
10
- from ..services.common.graph_data import get_output_graph_data
8
+ from ...lib.enums import ZT
9
+ from ..zone import Zone
11
10
 
12
- __all__ = [
13
- "get_targets_for_zone_and_sub_zones",
14
- "extract_results",
15
- ]
11
+ __all__ = ["run_targeting_for_zone_and_subzones"]
16
12
 
17
13
 
18
- def get_targets_for_zone_and_sub_zones(
14
+ def run_targeting_for_zone_and_subzones(
19
15
  zone: Zone,
20
16
  direct_service_func: Callable = None,
21
17
  indirect_service_func: Callable = None,
@@ -28,17 +24,6 @@ def get_targets_for_zone_and_sub_zones(
28
24
  return handler(zone, direct_service_func, indirect_service_func, args)
29
25
 
30
26
 
31
- def extract_results(zone: Zone, state_id: str | None = None) -> dict:
32
- """Serialise solved targets, generated utilities, and graph payloads."""
33
- return {
34
- "name": zone.name,
35
- "state_id": state_id,
36
- "targets": _get_report(zone, state_id=state_id),
37
- "utilities": _get_utilities(zone),
38
- "graphs": get_output_graph_data(zone),
39
- }
40
-
41
-
42
27
  ################################################################################
43
28
  # Helper functions
44
29
  ################################################################################
@@ -172,16 +157,11 @@ def _get_site_targets(
172
157
  indirect_service_func: Callable = None,
173
158
  args: dict | None = None,
174
159
  ):
175
- """Targets heat integration using Total Site Anlysis,
176
- by systematically analysing individual zones and then performing
177
- site-level indirect integration through the utility system.
178
- """
160
+ """Run site targeting over the full nested zone hierarchy."""
179
161
 
180
- # Totally integrated analysis for a site zone
181
162
  if isinstance(direct_service_func, Callable):
182
163
  _invoke_service(direct_service_func, zone, args)
183
164
 
184
- # Targets sub-zone energy requirements
185
165
  if len(zone.subzones) > 0:
186
166
  for subzone in zone.subzones.values():
187
167
  if subzone.type == ZT.O.value:
@@ -214,7 +194,6 @@ def _get_site_targets(
214
194
  "site, process, and operation zones."
215
195
  )
216
196
 
217
- # Calculates indirect targets based on different approaches
218
197
  if isinstance(indirect_service_func, Callable):
219
198
  _invoke_service(indirect_service_func, zone, args)
220
199
 
@@ -227,7 +206,7 @@ def _get_community_targets(
227
206
  indirect_service_func: Callable = None,
228
207
  args: dict | None = None,
229
208
  ):
230
- """Targets a Community Zone."""
209
+ """Target a community zone by dispatching each site child."""
231
210
  for subzone in zone.subzones.values():
232
211
  subzone = _invoke_target_handler(
233
212
  _get_site_targets,
@@ -245,7 +224,7 @@ def _get_regional_targets(
245
224
  indirect_service_func: Callable = None,
246
225
  args: dict | None = None,
247
226
  ):
248
- """Targets a Regional Zone."""
227
+ """Target a regional zone by dispatching each community child."""
249
228
  for subzone in zone.subzones.values():
250
229
  subzone = _invoke_target_handler(
251
230
  _get_community_targets,
@@ -257,31 +236,6 @@ def _get_regional_targets(
257
236
  return zone
258
237
 
259
238
 
260
- def _get_report(zone: Zone, state_id: str | None = None) -> dict:
261
- """Creates the database summary of zone targets."""
262
- targets: List[dict] = []
263
-
264
- for target in zone.targets.values():
265
- target_payload = target.serialize_json()
266
- if state_id is not None:
267
- target_payload["state_id"] = state_id
268
- targets.append(target_payload)
269
-
270
- if len(zone.subzones) > 0:
271
- for subzone in zone.subzones.values():
272
- targets.extend(_get_report(subzone, state_id=state_id))
273
-
274
- return targets
275
-
276
-
277
- def _get_utilities(zone: Zone) -> StreamCollection:
278
- """Gets a list of any default utilities generated during the analysis."""
279
- utilities: StreamCollection = zone.hot_utilities + zone.cold_utilities
280
- default_hu: Stream = next((u for u in utilities if u.name == "HU"), None)
281
- default_cu: Stream = next((u for u in utilities if u.name == "CU"), None)
282
- return [default_hu, default_cu]
283
-
284
-
285
239
  _TARGET_HANDLERS = {
286
240
  ZT.R.value: _get_regional_targets,
287
241
  ZT.C.value: _get_community_targets,
@@ -12,6 +12,7 @@ from pydantic import ValidationError
12
12
  from ...lib.enums import ST
13
13
  from ...lib.schemas.io import TargetInput
14
14
  from ...lib.schemas.workspace import ValidationIssue, ValidationReport
15
+ from ...lib.unit_system import standardise_input_value
15
16
  from ..value import Value
16
17
 
17
18
  ValidationContext = dict[str, list[dict[str, Any]]]
@@ -116,6 +117,8 @@ def semantic_issues(
116
117
  ) -> list[ValidationIssue]:
117
118
  """Return semantic validation issues for one validated problem definition."""
118
119
  issues: list[ValidationIssue] = []
120
+ options = problem_inputs.options if isinstance(problem_inputs.options, dict) else {}
121
+ input_unit_config = {"INPUT_UNITS": options.get("INPUT_UNITS", {})}
119
122
 
120
123
  if len(problem_inputs.streams) == 0:
121
124
  issues.append(
@@ -135,6 +138,7 @@ def semantic_issues(
135
138
  record_label=label,
136
139
  field_names=("t_supply", "t_target", "heat_flow", "dt_cont", "htc"),
137
140
  optional_field_names=("dt_cont", "htc"),
141
+ config=input_unit_config,
138
142
  )
139
143
  issues.extend(stream_value_issues)
140
144
  issues.extend(
@@ -168,6 +172,7 @@ def semantic_issues(
168
172
  "htc",
169
173
  "price",
170
174
  ),
175
+ config=input_unit_config,
171
176
  )
172
177
  issues.extend(utility_value_issues)
173
178
  issues.extend(
@@ -365,6 +370,7 @@ def _coerce_validation_values(
365
370
  record_label: Optional[str],
366
371
  field_names: tuple[str, ...],
367
372
  optional_field_names: tuple[str, ...] = (),
373
+ config: dict[str, dict[str, Any]] | None = None,
368
374
  ) -> tuple[
369
375
  dict[str, Value | None],
370
376
  dict[str, tuple[list[str] | None, np.ndarray | None]],
@@ -382,7 +388,11 @@ def _coerce_validation_values(
382
388
  values[field_name] = None
383
389
  continue
384
390
  try:
385
- values[field_name] = Value(raw_value)
391
+ values[field_name] = standardise_input_value(
392
+ raw_value,
393
+ field_name=field_name,
394
+ config=config,
395
+ )
386
396
  except (TypeError, ValueError) as exc:
387
397
  values[field_name] = None
388
398
  issues.append(
@@ -1,5 +1,7 @@
1
1
  """Helper utilities for stream collection operations."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import pickle
4
6
  from typing import Tuple
5
7
 
@@ -42,6 +42,10 @@ _SUMMARY_METRIC_COLUMNS = [
42
42
  ]
43
43
 
44
44
 
45
+ def _unit_column(metric: str) -> str:
46
+ return f"{metric} (unit)"
47
+
48
+
45
49
  def invalid_variant_view(
46
50
  *,
47
51
  variant_name: str,
@@ -161,6 +165,7 @@ def summary_cards(frame: pd.DataFrame) -> list[SummaryCard]:
161
165
  target_name=target_name,
162
166
  label=metric,
163
167
  value=json_safe(row.get(metric)),
168
+ unit=maybe_string(row.get(_unit_column(metric))),
164
169
  )
165
170
  )
166
171
  return cards
@@ -276,6 +281,13 @@ def summary_metric_deltas(
276
281
  for metric in _SUMMARY_METRIC_COLUMNS:
277
282
  base_value = json_safe(base_row.get(metric))
278
283
  variant_value = json_safe(variant_row.get(metric))
284
+ unit = maybe_string(
285
+ base_row.get(_unit_column(metric))
286
+ or variant_row.get(_unit_column(metric))
287
+ )
288
+ units_match = base_row.get(_unit_column(metric)) == variant_row.get(
289
+ _unit_column(metric)
290
+ )
279
291
  deltas.append(
280
292
  VariantMetricDelta(
281
293
  base_variant=base_name,
@@ -285,7 +297,10 @@ def summary_metric_deltas(
285
297
  metric=metric,
286
298
  base_value=base_value,
287
299
  variant_value=variant_value,
288
- delta=numeric_delta(base_value, variant_value),
300
+ unit=unit,
301
+ delta=numeric_delta(base_value, variant_value)
302
+ if units_match
303
+ else None,
289
304
  )
290
305
  )
291
306
  return deltas