OpenPyTEA 3.1.0__tar.gz → 3.2.0__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.
- {openpytea-3.1.0/src/OpenPyTEA.egg-info → openpytea-3.2.0}/PKG-INFO +1 -1
- {openpytea-3.1.0 → openpytea-3.2.0}/pyproject.toml +1 -1
- {openpytea-3.1.0 → openpytea-3.2.0/src/OpenPyTEA.egg-info}/PKG-INFO +1 -1
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/io.py +108 -37
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_composite_equipment.py +30 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_run_tea.py +47 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/LICENSE +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/README.md +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/setup.cfg +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/OpenPyTEA.egg-info/SOURCES.txt +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/OpenPyTEA.egg-info/dependency_links.txt +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/OpenPyTEA.egg-info/entry_points.txt +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/OpenPyTEA.egg-info/requires.txt +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/OpenPyTEA.egg-info/top_level.txt +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/__init__.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/analysis.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/cli.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/data/cepci_values.csv +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/data/cost_correlations.csv +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/equipment.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/helpers.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/plant.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/src/openpytea/plotting.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_analysis.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_cli.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_equipment.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_plant.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_plotting.py +0 -0
- {openpytea-3.1.0 → openpytea-3.2.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenPyTEA
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: OpenPyTEA: An open-source Python toolkit for techno-economic assessment of process plants with economic sensitivity and uncertainty evaluation
|
|
5
5
|
Maintainer-email: "Panji B. Tamarona" <P.B.Tamarona@tudelft.nl>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "OpenPyTEA"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.2.0"
|
|
4
4
|
description = "OpenPyTEA: An open-source Python toolkit for techno-economic assessment of process plants with economic sensitivity and uncertainty evaluation"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenPyTEA
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: OpenPyTEA: An open-source Python toolkit for techno-economic assessment of process plants with economic sensitivity and uncertainty evaluation
|
|
5
5
|
Maintainer-email: "Panji B. Tamarona" <P.B.Tamarona@tudelft.nl>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -5,7 +5,7 @@ from importlib.metadata import version
|
|
|
5
5
|
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
7
|
|
|
8
|
-
from openpytea.equipment import Equipment
|
|
8
|
+
from openpytea.equipment import Equipment, CompositeEquipment
|
|
9
9
|
from openpytea.plant import Plant
|
|
10
10
|
from openpytea.analysis import (
|
|
11
11
|
direct_costs_data,
|
|
@@ -76,6 +76,11 @@ def load_equipment_config(filepath):
|
|
|
76
76
|
material (falls back to 'Carbon steel'), target_year=2024
|
|
77
77
|
- Optional keys: 'type', 'material', 'num_units', 'purchased_cost',
|
|
78
78
|
'cost_year', 'cost_func', 'target_year'
|
|
79
|
+
- An entry with a 'components' list (of equipment entries, nesting
|
|
80
|
+
allowed) builds a CompositeEquipment; it requires 'name' and
|
|
81
|
+
'process_type' and accepts 'category' (default 'Composite'),
|
|
82
|
+
'type', 'installation', 'purchased_cost', 'cost_year',
|
|
83
|
+
'target_year' and 'num_units' (default 1)
|
|
79
84
|
"""
|
|
80
85
|
data = _read_json(filepath)
|
|
81
86
|
return _build_equipment_list(data)
|
|
@@ -99,45 +104,87 @@ def _build_equipment_list(data):
|
|
|
99
104
|
"'equipment' must be a list of equipment entries."
|
|
100
105
|
)
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
return [
|
|
108
|
+
_build_equipment(entry, f"#{i}")
|
|
109
|
+
for i, entry in enumerate(data["equipment"], start=1)
|
|
110
|
+
]
|
|
103
111
|
|
|
104
|
-
for i, entry in enumerate(data["equipment"], start=1):
|
|
105
|
-
if not isinstance(entry, dict):
|
|
106
|
-
raise ValueError(
|
|
107
|
-
f"Equipment entry #{i} must be a dictionary."
|
|
108
|
-
)
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
# Installation-factor overrides accepted by Equipment and CompositeEquipment
|
|
114
|
+
_FACTOR_KEYS = (
|
|
115
|
+
"erection_factor", "piping_factor", "instrumentation_factor",
|
|
116
|
+
"electrical_factor", "civil_factor", "structural_factor",
|
|
117
|
+
"lagging_factor", "material_factor",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_equipment(entry, label, target_year=2024):
|
|
122
|
+
"""
|
|
123
|
+
Build one Equipment, or a CompositeEquipment when the entry has a
|
|
124
|
+
'components' list (built recursively; components without their own
|
|
125
|
+
'target_year' inherit the composite's).
|
|
126
|
+
"""
|
|
127
|
+
if not isinstance(entry, dict):
|
|
128
|
+
raise ValueError(f"Equipment entry {label} must be a dictionary.")
|
|
129
|
+
|
|
130
|
+
target_year = entry.get("target_year", target_year)
|
|
131
|
+
factors = {k: entry[k] for k in _FACTOR_KEYS if k in entry}
|
|
132
|
+
|
|
133
|
+
if "components" in entry:
|
|
134
|
+
missing = [k for k in ("name", "process_type") if k not in entry]
|
|
112
135
|
if missing:
|
|
113
136
|
raise ValueError(
|
|
114
|
-
f"Equipment entry
|
|
137
|
+
f"Equipment entry {label} is missing required keys: {missing}"
|
|
115
138
|
)
|
|
116
|
-
|
|
117
|
-
if "purchased_cost" not in entry and "param" not in entry:
|
|
139
|
+
if not isinstance(entry["components"], list):
|
|
118
140
|
raise ValueError(
|
|
119
|
-
f"
|
|
120
|
-
f"or 'purchased_cost'."
|
|
141
|
+
f"'components' of equipment entry {label} must be a list."
|
|
121
142
|
)
|
|
122
|
-
|
|
123
|
-
# ponytail: copies keys by hand; Equipment(**{'param': 0.0, **entry})
|
|
124
|
-
eq = Equipment(
|
|
143
|
+
return CompositeEquipment(
|
|
125
144
|
name=entry["name"],
|
|
126
|
-
param=entry.get("param", 0.0),
|
|
127
145
|
process_type=entry["process_type"],
|
|
128
|
-
|
|
146
|
+
components=[
|
|
147
|
+
_build_equipment(sub, f"{label}.{j}", target_year)
|
|
148
|
+
for j, sub in enumerate(entry["components"], start=1)
|
|
149
|
+
],
|
|
150
|
+
category=entry.get("category", "Composite"),
|
|
129
151
|
type=entry.get("type"),
|
|
130
|
-
|
|
131
|
-
num_units=entry.get("num_units"),
|
|
152
|
+
installation=entry.get("installation", "component"),
|
|
132
153
|
purchased_cost=entry.get("purchased_cost"),
|
|
133
154
|
cost_year=entry.get("cost_year"),
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
target_year=target_year,
|
|
156
|
+
num_units=entry.get("num_units", 1),
|
|
157
|
+
**factors,
|
|
136
158
|
)
|
|
137
159
|
|
|
138
|
-
|
|
160
|
+
required = ["name", "process_type", "category"]
|
|
161
|
+
missing = [k for k in required if k not in entry]
|
|
162
|
+
if missing:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Equipment entry {label} is missing required keys: {missing}"
|
|
165
|
+
)
|
|
139
166
|
|
|
140
|
-
|
|
167
|
+
if "purchased_cost" not in entry and "param" not in entry:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Equipment entry {label} must define either 'param' "
|
|
170
|
+
f"or 'purchased_cost'."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# ponytail: copies keys by hand; Equipment(**{'param': 0.0, **entry})
|
|
174
|
+
return Equipment(
|
|
175
|
+
name=entry["name"],
|
|
176
|
+
param=entry.get("param", 0.0),
|
|
177
|
+
process_type=entry["process_type"],
|
|
178
|
+
category=entry["category"],
|
|
179
|
+
type=entry.get("type"),
|
|
180
|
+
material=entry.get("material"),
|
|
181
|
+
num_units=entry.get("num_units"),
|
|
182
|
+
purchased_cost=entry.get("purchased_cost"),
|
|
183
|
+
cost_year=entry.get("cost_year"),
|
|
184
|
+
cost_func=entry.get("cost_func"),
|
|
185
|
+
target_year=target_year,
|
|
186
|
+
**factors,
|
|
187
|
+
)
|
|
141
188
|
|
|
142
189
|
|
|
143
190
|
def load_plant_config(filepath, equipment_list):
|
|
@@ -555,6 +602,10 @@ def run_openpytea(config_path, output_dir=None):
|
|
|
555
602
|
return _run_analyses(equipment_list, plant, config, output_dir)
|
|
556
603
|
|
|
557
604
|
|
|
605
|
+
# Analyses whose function takes an ``additional_capex`` argument
|
|
606
|
+
_ADDITIONAL_CAPEX_ANALYSES = {"fixed_capital", "tornado", "monte_carlo"}
|
|
607
|
+
|
|
608
|
+
|
|
558
609
|
def _run_analyses(equipment_list, plant, analysis_cfg, output_dir):
|
|
559
610
|
"""
|
|
560
611
|
Run the configured analyses for a loaded plant/equipment pair and
|
|
@@ -602,7 +653,10 @@ def _run_analyses(equipment_list, plant, analysis_cfg, output_dir):
|
|
|
602
653
|
if save_json or save_plots:
|
|
603
654
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
604
655
|
|
|
605
|
-
plant
|
|
656
|
+
# One switch so the exported plant results and the analyses agree on
|
|
657
|
+
# whether additional capex counts; an analysis' own args still win
|
|
658
|
+
additional_capex = analysis_block.get("additional_capex", False)
|
|
659
|
+
plant.calculate_all(additional_capex=additional_capex)
|
|
606
660
|
|
|
607
661
|
results = {}
|
|
608
662
|
|
|
@@ -621,15 +675,20 @@ def _run_analyses(equipment_list, plant, analysis_cfg, output_dir):
|
|
|
621
675
|
cfg = analysis_block.get(key, {})
|
|
622
676
|
if cfg.get("run"):
|
|
623
677
|
args = cfg.get("args", {})
|
|
678
|
+
if key in _ADDITIONAL_CAPEX_ANALYSES:
|
|
679
|
+
args = {"additional_capex": additional_capex, **args}
|
|
624
680
|
results[key] = func(plant, **args)
|
|
625
681
|
|
|
626
682
|
sens_cfg = analysis_block.get("sensitivity", {})
|
|
683
|
+
sens_plot_args = {}
|
|
627
684
|
if sens_cfg.get("run", False):
|
|
628
685
|
results["sensitivity"] = {}
|
|
629
686
|
for i, case in enumerate(sens_cfg.get("cases", []), start=1):
|
|
630
687
|
name = case.get("name", f"case_{i}")
|
|
631
|
-
args =
|
|
688
|
+
args = {"additional_capex": additional_capex,
|
|
689
|
+
**case.get("args", {})}
|
|
632
690
|
results["sensitivity"][name] = sensitivity_data(plant, **args)
|
|
691
|
+
sens_plot_args[name] = case.get("plot_args", {})
|
|
633
692
|
|
|
634
693
|
if save_json:
|
|
635
694
|
export_equipment_results(
|
|
@@ -667,13 +726,18 @@ def _run_analyses(equipment_list, plant, analysis_cfg, output_dir):
|
|
|
667
726
|
("cash_flow", plot_cash_flow),
|
|
668
727
|
):
|
|
669
728
|
if key in results:
|
|
670
|
-
|
|
729
|
+
plot_args = analysis_block[key].get("plot_args", {})
|
|
730
|
+
_save(plot_fn(results[key], show=False, **plot_args)[0], key)
|
|
671
731
|
|
|
672
732
|
for name, data in results.get("sensitivity", {}).items():
|
|
673
|
-
|
|
733
|
+
plot_args = sens_plot_args.get(name, {})
|
|
734
|
+
_save(plot_sensitivity(data, show=False, **plot_args)[0],
|
|
735
|
+
f"sensitivity_{name}")
|
|
674
736
|
|
|
675
737
|
if "tornado" in results:
|
|
676
|
-
|
|
738
|
+
plot_args = analysis_block["tornado"].get("plot_args", {})
|
|
739
|
+
_save(plot_tornado(results["tornado"], show=False, **plot_args)[0],
|
|
740
|
+
"tornado")
|
|
677
741
|
|
|
678
742
|
if "monte_carlo" in results:
|
|
679
743
|
mc_results = results["monte_carlo"]
|
|
@@ -704,17 +768,24 @@ def _run_analyses(equipment_list, plant, analysis_cfg, output_dir):
|
|
|
704
768
|
mc_results,
|
|
705
769
|
metric=metric_name,
|
|
706
770
|
show=False,
|
|
771
|
+
**mc_cfg.get("plot_args", {}),
|
|
707
772
|
)
|
|
708
773
|
_save(fig, f"monte_carlo_{metric_name.lower()}")
|
|
709
774
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
775
|
+
inputs_cfg = mc_cfg.get("plot_inputs", False)
|
|
776
|
+
if inputs_cfg:
|
|
777
|
+
# true, or a dict of plot_monte_carlo_inputs kwargs
|
|
778
|
+
plot_args = inputs_cfg if isinstance(inputs_cfg, dict) else {}
|
|
779
|
+
out = plot_monte_carlo_inputs(
|
|
780
|
+
mc_results, show=False, **plot_args
|
|
781
|
+
)
|
|
782
|
+
category = plot_args.get("category", "both")
|
|
783
|
+
groups = (
|
|
784
|
+
("process", "economic") if category == "both"
|
|
785
|
+
else (category,)
|
|
713
786
|
)
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
("economic", fig_economic),
|
|
717
|
-
):
|
|
787
|
+
# out is (fig, axes) per group, flattened
|
|
788
|
+
for group_name, fig in zip(groups, out[::2]):
|
|
718
789
|
if fig is None:
|
|
719
790
|
continue # no inputs sampled for this group
|
|
720
791
|
_save(fig, f"monte_carlo_inputs_{group_name}")
|
|
@@ -218,3 +218,33 @@ def test_composite_in_plant_and_direct_costs_data(psa):
|
|
|
218
218
|
expanded = direct_costs_data(plant, expand_composites=True)["components"][0]
|
|
219
219
|
assert set(expanded) == {"PSA / Vessel", "PSA / Adsorbent", "Pump"}
|
|
220
220
|
assert sum(expanded.values()) == pytest.approx(sum(lumped.values()))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_json_builds_nested_composite(psa):
|
|
224
|
+
from openpytea.io import _build_equipment_list
|
|
225
|
+
|
|
226
|
+
def part(name, cost, process_type, category, **kw):
|
|
227
|
+
return {"name": name, "purchased_cost": cost, "cost_year": 2024,
|
|
228
|
+
"process_type": process_type, "category": category, **kw}
|
|
229
|
+
|
|
230
|
+
entry = {
|
|
231
|
+
"name": "Train", "process_type": "Fluids", "num_units": 2,
|
|
232
|
+
"components": [
|
|
233
|
+
{"name": "PSA", "process_type": "Fluids",
|
|
234
|
+
"category": "Adsorption", "type": "PSA",
|
|
235
|
+
"components": [
|
|
236
|
+
part("Vessel", 10_000, "Fluids", "Pressure vessels",
|
|
237
|
+
num_units=4),
|
|
238
|
+
part("Adsorbent", 5_000, "Solids",
|
|
239
|
+
"Packings & adsorbents"),
|
|
240
|
+
]},
|
|
241
|
+
],
|
|
242
|
+
}
|
|
243
|
+
(train,) = _build_equipment_list({"equipment": [entry]})
|
|
244
|
+
|
|
245
|
+
assert isinstance(train, CompositeEquipment)
|
|
246
|
+
assert train.category == "Composite"
|
|
247
|
+
built_psa = train.components[0]
|
|
248
|
+
assert isinstance(built_psa, CompositeEquipment)
|
|
249
|
+
assert built_psa.direct_cost == pytest.approx(psa.direct_cost)
|
|
250
|
+
assert train.direct_cost == pytest.approx(2 * psa.direct_cost)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
2
4
|
from openpytea import run_tea, run_openpytea
|
|
3
5
|
|
|
4
6
|
|
|
@@ -286,3 +288,48 @@ def test_run_tea_python_api_honors_config_directory(tmp_path):
|
|
|
286
288
|
)
|
|
287
289
|
|
|
288
290
|
assert (config_dir / "Test Plant_analysis_results.json").exists()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_run_openpytea_passes_factors_capex_and_plot_args(tmp_path):
|
|
294
|
+
equipment = _equipment_data()
|
|
295
|
+
equipment["equipment"][1]["piping_factor"] = 0.0
|
|
296
|
+
plant = _plant_data()
|
|
297
|
+
plant["plant"]["additional_capex_years"] = [5]
|
|
298
|
+
plant["plant"]["additional_capex_cost"] = [50_000]
|
|
299
|
+
analysis = {
|
|
300
|
+
"analysis": {
|
|
301
|
+
"additional_capex": True,
|
|
302
|
+
"fixed_capital": {"run": True, "plot_args": {"figsize": [2, 2]}},
|
|
303
|
+
"monte_carlo": {
|
|
304
|
+
"run": True,
|
|
305
|
+
"args": {"num_samples": 100, "batch_size": 20},
|
|
306
|
+
"metric": ["LCOP"],
|
|
307
|
+
"plot_args": {"bins": 5},
|
|
308
|
+
"plot_inputs": {"category": "economic"},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
"output": {"save_json": True, "save_plots": True},
|
|
312
|
+
}
|
|
313
|
+
config_path = tmp_path / "config.json"
|
|
314
|
+
config_path.write_text(
|
|
315
|
+
json.dumps({**equipment, **plant, **analysis}), encoding="utf-8"
|
|
316
|
+
)
|
|
317
|
+
out = tmp_path / "results"
|
|
318
|
+
run_openpytea(config_path=config_path, output_dir=out)
|
|
319
|
+
|
|
320
|
+
eq = json.loads((out / "Test Plant_equipment_results.json").read_text())
|
|
321
|
+
pump = eq["equipment"][1]
|
|
322
|
+
reactor = eq["equipment"][0]
|
|
323
|
+
# Same purchased cost and process type, only the pump drops piping
|
|
324
|
+
assert pump["direct_cost"] / pump["purchased_cost"] < (
|
|
325
|
+
reactor["direct_cost"] / reactor["purchased_cost"]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
from openpytea.io import _build_equipment_list, _build_plant
|
|
329
|
+
expected = _build_plant(plant, _build_equipment_list(equipment))
|
|
330
|
+
expected.calculate_all(additional_capex=True)
|
|
331
|
+
plant_res = json.loads((out / "Test Plant_plant_results.json").read_text())
|
|
332
|
+
assert plant_res["metrics"]["roi"] == pytest.approx(expected.roi)
|
|
333
|
+
|
|
334
|
+
assert (out / "Test Plant_monte_carlo_inputs_economic.png").exists()
|
|
335
|
+
assert not (out / "Test Plant_monte_carlo_inputs_process.png").exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|