OpenPyTEA 1.2.0__py3-none-any.whl
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/__init__.py +24 -0
- openpytea/analysis.py +2127 -0
- openpytea/data/cepci_values.csv +36 -0
- openpytea/data/cost_correlations.csv +166 -0
- openpytea/equipment.py +432 -0
- openpytea/plant.py +1821 -0
- openpytea-1.2.0.dist-info/METADATA +300 -0
- openpytea-1.2.0.dist-info/RECORD +11 -0
- openpytea-1.2.0.dist-info/WHEEL +5 -0
- openpytea-1.2.0.dist-info/licenses/LICENSE +21 -0
- openpytea-1.2.0.dist-info/top_level.txt +1 -0
openpytea/equipment.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
# --- Fixed CSV data sources ---
|
|
5
|
+
from importlib.resources import files, as_file
|
|
6
|
+
|
|
7
|
+
data_dir = files("openpytea.data")
|
|
8
|
+
|
|
9
|
+
with as_file(
|
|
10
|
+
data_dir / "cepci_values.csv"
|
|
11
|
+
) as CEPCI_CSV_PATH:
|
|
12
|
+
CEPCI_DF = pd.read_csv(CEPCI_CSV_PATH).set_index("year")
|
|
13
|
+
|
|
14
|
+
with as_file(
|
|
15
|
+
data_dir / "cost_correlations.csv"
|
|
16
|
+
) as COST_DB_PATH:
|
|
17
|
+
COST_DB_DF = pd.read_csv(COST_DB_PATH)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def inflation_adjustment(
|
|
21
|
+
equipment_cost, cost_year, target_year=2024
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Adjust equipment cost from one year to another using
|
|
25
|
+
the Chemical Engineering Plant Cost Index (CEPCI).
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
equipment_cost : float
|
|
30
|
+
The cost of the equipment in the cost_year.
|
|
31
|
+
cost_year : int
|
|
32
|
+
The year in which the equipment_cost is valued.
|
|
33
|
+
Must be available in CEPCI_DF.
|
|
34
|
+
target_year : int, optional
|
|
35
|
+
The year to adjust the cost to. Default is 2024.
|
|
36
|
+
Must be available in CEPCI_DF.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
float
|
|
41
|
+
The inflation-adjusted equipment cost in the target_year.
|
|
42
|
+
|
|
43
|
+
Raises
|
|
44
|
+
------
|
|
45
|
+
ValueError
|
|
46
|
+
If cost_year is not available in CEPCI_DF.
|
|
47
|
+
ValueError
|
|
48
|
+
If target_year is not available in CEPCI_DF.
|
|
49
|
+
|
|
50
|
+
Examples
|
|
51
|
+
--------
|
|
52
|
+
>>> adjusted_cost = inflation_adjustment(10000, 2020, 2024)
|
|
53
|
+
>>> print(adjusted_cost)
|
|
54
|
+
12345.67
|
|
55
|
+
"""
|
|
56
|
+
if cost_year not in CEPCI_DF.index:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"CEPCI not available for year {cost_year}"
|
|
59
|
+
)
|
|
60
|
+
if target_year not in CEPCI_DF.index:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"CEPCI not available for target year {target_year}"
|
|
63
|
+
)
|
|
64
|
+
return float(equipment_cost) * (
|
|
65
|
+
CEPCI_DF.loc[target_year, "cepci"]
|
|
66
|
+
/ CEPCI_DF.loc[cost_year, "cepci"]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CostCorrelationDB:
|
|
71
|
+
"""
|
|
72
|
+
A database interface for equipment cost correlations.
|
|
73
|
+
This class manages cost estimation correlations for equipment based on
|
|
74
|
+
size/capacity parameters. It supports multiple correlation forms
|
|
75
|
+
(power-law, quad log-log) and handles equipment parallelization when
|
|
76
|
+
capacity limits are exceeded.
|
|
77
|
+
Attributes:
|
|
78
|
+
df (pd.DataFrame): DataFrame containing cost correlation data with
|
|
79
|
+
columns including: key, category, type, form, s_lower, s_upper,
|
|
80
|
+
upper_parallel, a, b, n, k1, k2, k3, cost_year, and other parameters.
|
|
81
|
+
Methods:
|
|
82
|
+
__init__(df: pd.DataFrame) -> None:
|
|
83
|
+
Initialize the database with a cost correlation DataFrame.
|
|
84
|
+
Normalizes column names to lowercase and converts numeric columns.
|
|
85
|
+
_parallelize(s: float, cap: float | None) -> tuple[int, float]:
|
|
86
|
+
Calculate number of parallel units and adjusted size when capacity
|
|
87
|
+
is exceeded.
|
|
88
|
+
Args:
|
|
89
|
+
s: Equipment size/capacity.
|
|
90
|
+
cap: Unit capacity limit. If None, no parallelization occurs.
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of (number_of_units, adjusted_size_per_unit).
|
|
93
|
+
evaluate(key: str, s: float) -> tuple[float, int, int]:
|
|
94
|
+
Calculate purchased equipment cost based on
|
|
95
|
+
correlation key and size.
|
|
96
|
+
Args:
|
|
97
|
+
key: Unique identifier for the cost correlation.
|
|
98
|
+
s: Equipment size/capacity parameter.
|
|
99
|
+
Returns:
|
|
100
|
+
Tuple of (total_cost, number_of_units, cost_year).
|
|
101
|
+
Raises:
|
|
102
|
+
KeyError: If correlation key not found in database.
|
|
103
|
+
ValueError: If size is below lower bound or
|
|
104
|
+
form is unsupported.
|
|
105
|
+
key_for_category_type(eq_category: str, type: str | None)
|
|
106
|
+
-> str | None:
|
|
107
|
+
Look up correlation key by equipment category and optional type.
|
|
108
|
+
Args:
|
|
109
|
+
eq_category: Equipment category name.
|
|
110
|
+
type: Equipment type (optional).
|
|
111
|
+
Returns:
|
|
112
|
+
Correlation key if found, None otherwise.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, df=COST_DB_DF):
|
|
116
|
+
df.columns = [c.strip().lower() for c in df.columns]
|
|
117
|
+
for col in [
|
|
118
|
+
"s_lower",
|
|
119
|
+
"s_upper",
|
|
120
|
+
"upper_parallel",
|
|
121
|
+
"a",
|
|
122
|
+
"b",
|
|
123
|
+
"n",
|
|
124
|
+
"s0",
|
|
125
|
+
"c0",
|
|
126
|
+
"f",
|
|
127
|
+
"cost_year",
|
|
128
|
+
]:
|
|
129
|
+
if col in df.columns:
|
|
130
|
+
df[col] = pd.to_numeric(
|
|
131
|
+
df[col], errors="coerce"
|
|
132
|
+
)
|
|
133
|
+
df["form"] = df["form"].str.lower()
|
|
134
|
+
self.df = df
|
|
135
|
+
|
|
136
|
+
def _parallelize(self, s: float, cap: float | None):
|
|
137
|
+
if pd.notna(cap) and s > cap:
|
|
138
|
+
units = int(np.ceil(s / cap))
|
|
139
|
+
return units, s / units
|
|
140
|
+
return 1, s
|
|
141
|
+
|
|
142
|
+
def evaluate(self, key: str, s: float):
|
|
143
|
+
row = self.df.loc[self.df["key"] == key]
|
|
144
|
+
if row.empty:
|
|
145
|
+
raise KeyError(
|
|
146
|
+
f"Correlation key not found in CSV: {key}"
|
|
147
|
+
)
|
|
148
|
+
r = row.iloc[0].to_dict()
|
|
149
|
+
|
|
150
|
+
s_lower = r.get("s_lower")
|
|
151
|
+
s_upper = r.get("s_upper")
|
|
152
|
+
cap = (
|
|
153
|
+
r.get("upper_parallel")
|
|
154
|
+
if pd.notna(r.get("upper_parallel"))
|
|
155
|
+
else s_upper
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if pd.notna(s_lower) and s < s_lower:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"s={s} below lower bound {s_lower} for key '{key}'"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
units, s_adj = self._parallelize(s, cap)
|
|
164
|
+
form = r.get("form", "linear")
|
|
165
|
+
year = int(r["cost_year"])
|
|
166
|
+
|
|
167
|
+
if form == "power-law":
|
|
168
|
+
a, b, n = r["a"], r["b"], r["n"]
|
|
169
|
+
ce = a + b * (s_adj**n)
|
|
170
|
+
purchased = ce * units
|
|
171
|
+
|
|
172
|
+
elif form == "quad log-log":
|
|
173
|
+
K1, K2, K3 = r["k1"], r["k3"], r["k3"]
|
|
174
|
+
|
|
175
|
+
logS = np.log10(s_adj)
|
|
176
|
+
logCe = K1 + K2 * logS + K3 * (logS**2)
|
|
177
|
+
|
|
178
|
+
ce = 10**logCe
|
|
179
|
+
purchased = ce * units
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Unsupported form '{form}' for key '{key}'"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return float(purchased), int(units), year
|
|
187
|
+
|
|
188
|
+
def key_for_category_type(
|
|
189
|
+
self, eq_category: str, type: str | None
|
|
190
|
+
):
|
|
191
|
+
|
|
192
|
+
t = eq_category.lower()
|
|
193
|
+
st = type.lower() if type else ""
|
|
194
|
+
df = self.df
|
|
195
|
+
|
|
196
|
+
if "category" not in df.columns:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
cand = df[df["category"].str.lower() == t]
|
|
200
|
+
if "type" in df.columns:
|
|
201
|
+
cand = cand[
|
|
202
|
+
cand["type"].fillna("").str.lower() == st
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
if cand.empty:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
# ✅ Return the first match (take the first listed in the CSV)
|
|
209
|
+
return cand.iloc[0]["key"]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Equipment:
|
|
213
|
+
"""
|
|
214
|
+
Equipment cost estimation class for process equipment.
|
|
215
|
+
This class manages the cost calculation of process equipment
|
|
216
|
+
based on process type, material, and equipment parameters.
|
|
217
|
+
It supports both direct cost input and calculated costs
|
|
218
|
+
based on correlations from a cost database.
|
|
219
|
+
Attributes:
|
|
220
|
+
process_factors (dict):
|
|
221
|
+
Dictionary of process type factors affecting cost calculation.
|
|
222
|
+
Keys are process types ("Solids", "Fluids", "Mixed", "Electrical").
|
|
223
|
+
Values are dicts with factors: fer, fp, fi, fel, fc, fs, fl.
|
|
224
|
+
material_factors (dict): Dictionary of material type multipliers.
|
|
225
|
+
Maps material names to cost multiplication factors (1.0 to 1.7).
|
|
226
|
+
Args:
|
|
227
|
+
name (str): Equipment identifier/name.
|
|
228
|
+
param (float):
|
|
229
|
+
Equipment parameter (size, capacity) for cost correlation lookup.
|
|
230
|
+
process_type (str): Type of process
|
|
231
|
+
("Solids", "Fluids", "Mixed", or "Electrical").
|
|
232
|
+
category (str): Equipment category for database lookup.
|
|
233
|
+
type (str | None): Equipment sub-type for database lookup.
|
|
234
|
+
Default is None.
|
|
235
|
+
material (str): Material of construction.
|
|
236
|
+
Default is "Carbon steel".
|
|
237
|
+
num_units (int | None): Number of identical units.
|
|
238
|
+
Default is None (set to 1 if purchased_cost provided).
|
|
239
|
+
purchased_cost (float | None): Direct purchased cost input.
|
|
240
|
+
If provided, param is ignored. Default is None.
|
|
241
|
+
cost_year (int | None): Year of the purchased_cost quote.
|
|
242
|
+
Default is None.
|
|
243
|
+
cost_func (str | None): Explicit cost correlation key from database.
|
|
244
|
+
Default is None (auto-resolved).
|
|
245
|
+
target_year (int): Target year for inflation adjustment.
|
|
246
|
+
Default is 2024.
|
|
247
|
+
Methods:
|
|
248
|
+
_resolve_key() -> str: Resolves the cost correlation key
|
|
249
|
+
from database or explicit input.
|
|
250
|
+
_calc_purchased_cost() -> float: Calculates purchased cost
|
|
251
|
+
using database correlation.
|
|
252
|
+
calculate_direct_cost() -> float: Calculates total direct cost
|
|
253
|
+
including process and material factors.
|
|
254
|
+
__str__() -> str: Returns formatted string representation of equipment
|
|
255
|
+
specifications and costs.
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError:
|
|
258
|
+
If process_type or material not found in factor dictionaries.
|
|
259
|
+
KeyError:
|
|
260
|
+
If category/type combination not found in database
|
|
261
|
+
and cost_func not specified.
|
|
262
|
+
️"""
|
|
263
|
+
|
|
264
|
+
process_factors = {
|
|
265
|
+
"Solids": {
|
|
266
|
+
"fer": 0.6,
|
|
267
|
+
"fp": 0.2,
|
|
268
|
+
"fi": 0.2,
|
|
269
|
+
"fel": 0.15,
|
|
270
|
+
"fc": 0.2,
|
|
271
|
+
"fs": 0.1,
|
|
272
|
+
"fl": 0.05,
|
|
273
|
+
},
|
|
274
|
+
"Fluids": {
|
|
275
|
+
"fer": 0.3,
|
|
276
|
+
"fp": 0.8,
|
|
277
|
+
"fi": 0.3,
|
|
278
|
+
"fel": 0.2,
|
|
279
|
+
"fc": 0.3,
|
|
280
|
+
"fs": 0.2,
|
|
281
|
+
"fl": 0.1,
|
|
282
|
+
},
|
|
283
|
+
"Mixed": {
|
|
284
|
+
"fer": 0.5,
|
|
285
|
+
"fp": 0.6,
|
|
286
|
+
"fi": 0.3,
|
|
287
|
+
"fel": 0.2,
|
|
288
|
+
"fc": 0.3,
|
|
289
|
+
"fs": 0.2,
|
|
290
|
+
"fl": 0.1,
|
|
291
|
+
},
|
|
292
|
+
"Electrical": {
|
|
293
|
+
"fer": 0.4,
|
|
294
|
+
"fp": 0.1,
|
|
295
|
+
"fi": 0.7,
|
|
296
|
+
"fel": 0.7,
|
|
297
|
+
"fc": 0.2,
|
|
298
|
+
"fs": 0.1,
|
|
299
|
+
"fl": 0.1,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
material_factors = {
|
|
304
|
+
"Carbon steel": 1.0,
|
|
305
|
+
"Aluminum": 1.07,
|
|
306
|
+
"Bronze": 1.07,
|
|
307
|
+
"Cast steel": 1.1,
|
|
308
|
+
"304 stainless steel": 1.3,
|
|
309
|
+
"316 stainless steel": 1.3,
|
|
310
|
+
"321 stainless steel": 1.5,
|
|
311
|
+
"Hastelloy C": 1.55,
|
|
312
|
+
"Monel": 1.65,
|
|
313
|
+
"Nickel": 1.7,
|
|
314
|
+
"Inconel": 1.7,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
def __init__(
|
|
318
|
+
self,
|
|
319
|
+
name: str,
|
|
320
|
+
param: float,
|
|
321
|
+
process_type: str,
|
|
322
|
+
category: str,
|
|
323
|
+
type: str | None = None,
|
|
324
|
+
material: str = "Carbon steel",
|
|
325
|
+
num_units: int | None = None,
|
|
326
|
+
purchased_cost: float | None = None,
|
|
327
|
+
cost_year: int | None = None,
|
|
328
|
+
cost_func: (
|
|
329
|
+
str | None
|
|
330
|
+
) = None, # explicit correlation key
|
|
331
|
+
target_year: int = 2024,
|
|
332
|
+
):
|
|
333
|
+
|
|
334
|
+
self.name = name
|
|
335
|
+
self.process_type = process_type
|
|
336
|
+
self.material = material
|
|
337
|
+
self.param = (
|
|
338
|
+
None if purchased_cost is not None else param
|
|
339
|
+
)
|
|
340
|
+
self.category = category
|
|
341
|
+
self.type = type
|
|
342
|
+
self.num_units = num_units
|
|
343
|
+
self.cost_year = (
|
|
344
|
+
cost_year if cost_year is not None else None
|
|
345
|
+
)
|
|
346
|
+
self.target_year = target_year
|
|
347
|
+
self._cost_func = cost_func
|
|
348
|
+
self._db = (
|
|
349
|
+
CostCorrelationDB()
|
|
350
|
+
) # always loads from the fixed CSV file
|
|
351
|
+
|
|
352
|
+
if purchased_cost is not None:
|
|
353
|
+
self.purchased_cost = purchased_cost
|
|
354
|
+
if cost_year is not None:
|
|
355
|
+
self.purchased_cost = inflation_adjustment(
|
|
356
|
+
purchased_cost,
|
|
357
|
+
cost_year,
|
|
358
|
+
target_year=self.target_year,
|
|
359
|
+
)
|
|
360
|
+
if self.num_units is None:
|
|
361
|
+
self.num_units = 1
|
|
362
|
+
else:
|
|
363
|
+
self.purchased_cost = (
|
|
364
|
+
self._calc_purchased_cost()
|
|
365
|
+
)
|
|
366
|
+
self.direct_cost = (
|
|
367
|
+
self.calculate_direct_cost()
|
|
368
|
+
) # your existing method
|
|
369
|
+
|
|
370
|
+
def _resolve_key(self) -> str:
|
|
371
|
+
|
|
372
|
+
if self._cost_func:
|
|
373
|
+
return self._cost_func
|
|
374
|
+
|
|
375
|
+
key = self._db.key_for_category_type(
|
|
376
|
+
self.category, self.type
|
|
377
|
+
)
|
|
378
|
+
if key is None:
|
|
379
|
+
raise KeyError(
|
|
380
|
+
f"No CSV correlation matches category='{self.category}', "
|
|
381
|
+
f"type='{self.type}'. "
|
|
382
|
+
f"Add a row to the CSV or specify cost_func manually."
|
|
383
|
+
)
|
|
384
|
+
return key
|
|
385
|
+
|
|
386
|
+
def _calc_purchased_cost(self) -> float:
|
|
387
|
+
key = self._resolve_key()
|
|
388
|
+
s = self.param
|
|
389
|
+
purchased, units, year = self._db.evaluate(key, s)
|
|
390
|
+
self.num_units = self.num_units or units
|
|
391
|
+
self.cost_year = year
|
|
392
|
+
return inflation_adjustment(
|
|
393
|
+
purchased, year, target_year=self.target_year
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def calculate_direct_cost(self) -> float:
|
|
397
|
+
|
|
398
|
+
if self.process_type not in self.process_factors:
|
|
399
|
+
raise ValueError(
|
|
400
|
+
f"Process type not found: {self.process_type}"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if self.material not in self.material_factors:
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"Material not found: {self.material}"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
factors = self.process_factors[self.process_type]
|
|
409
|
+
fm = self.material_factors[self.material]
|
|
410
|
+
|
|
411
|
+
self.direct_cost = self.purchased_cost * (
|
|
412
|
+
(1 + factors["fp"]) * fm
|
|
413
|
+
+ (
|
|
414
|
+
factors["fer"]
|
|
415
|
+
+ factors["fel"]
|
|
416
|
+
+ factors["fi"]
|
|
417
|
+
+ factors["fc"]
|
|
418
|
+
+ factors["fs"]
|
|
419
|
+
+ factors["fl"]
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
return self.direct_cost
|
|
423
|
+
|
|
424
|
+
def __str__(self) -> str:
|
|
425
|
+
return (
|
|
426
|
+
f"Name={self.name}, "
|
|
427
|
+
f"Category={self.category}, Sub-type={self.type}, "
|
|
428
|
+
f"Material={self.material}, Process Type={self.process_type}, "
|
|
429
|
+
f"Parameter={self.param}, Number of units={self.num_units}, "
|
|
430
|
+
f"Purchased Cost={self.purchased_cost}, "
|
|
431
|
+
f"Direct Cost={self.direct_cost})"
|
|
432
|
+
)
|