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/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
+ )