processpi 0.0.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.
Files changed (96) hide show
  1. processpi/__init__.py +1 -0
  2. processpi/calculations/__init__.py +3 -0
  3. processpi/calculations/base.py +55 -0
  4. processpi/calculations/engine.py +69 -0
  5. processpi/calculations/fluids/__init__.py +13 -0
  6. processpi/calculations/fluids/cool_brook_white.py +49 -0
  7. processpi/calculations/fluids/flow_type.py +37 -0
  8. processpi/calculations/fluids/optimium_pipe_dia.py +45 -0
  9. processpi/calculations/fluids/pressure_drop_darcy.py +25 -0
  10. processpi/calculations/fluids/pressure_drop_fanning.py +25 -0
  11. processpi/calculations/fluids/pump_power.py +34 -0
  12. processpi/calculations/fluids/reynolds_number.py +24 -0
  13. processpi/calculations/fluids/velocity.py +36 -0
  14. processpi/calculations/heat_transfer/__init__.py +4 -0
  15. processpi/calculations/heat_transfer/conduction_heat_loss.py +26 -0
  16. processpi/calculations/heat_transfer/convection_heat_loss.py +23 -0
  17. processpi/calculations/heat_transfer/heat_exchanger_area.py +26 -0
  18. processpi/calculations/mass_transfer/__init__.py +3 -0
  19. processpi/calculations/mass_transfer/distillation_stage_count.py +346 -0
  20. processpi/calculations/mass_transfer/drying_rate.py +132 -0
  21. processpi/calculations/reaction_engineering/__init__.py +4 -0
  22. processpi/calculations/reaction_engineering/catalyst_activity.py +109 -0
  23. processpi/calculations/reaction_engineering/reaction_rate.py +143 -0
  24. processpi/calculations/reaction_engineering/residence_time.py +80 -0
  25. processpi/calculations/thermodynamics/__init__.py +4 -0
  26. processpi/calculations/thermodynamics/enthalpy_change.py +23 -0
  27. processpi/calculations/thermodynamics/entropy_change.py +25 -0
  28. processpi/calculations/thermodynamics/heat_of_vaporization.py +20 -0
  29. processpi/components/__init__.py +5 -0
  30. processpi/components/aceticacid.py +19 -0
  31. processpi/components/acetone.py +20 -0
  32. processpi/components/acrylicacid.py +19 -0
  33. processpi/components/air.py +20 -0
  34. processpi/components/ammonia.py +19 -0
  35. processpi/components/base.py +85 -0
  36. processpi/components/benzene.py +40 -0
  37. processpi/components/benzoicacid.py +19 -0
  38. processpi/components/bromine.py +19 -0
  39. processpi/components/butane.py +19 -0
  40. processpi/components/carbondioxide.py +19 -0
  41. processpi/components/carbonmonoxide.py +17 -0
  42. processpi/components/carbontetrachloride.py +17 -0
  43. processpi/components/chlorine.py +17 -0
  44. processpi/components/chlorobenzene.py +17 -0
  45. processpi/components/chloroform.py +17 -0
  46. processpi/components/chloromethane.py +17 -0
  47. processpi/components/cyanogen.py +17 -0
  48. processpi/components/cyclohexene.py +17 -0
  49. processpi/components/ethane.py +17 -0
  50. processpi/components/ethanol.py +20 -0
  51. processpi/components/ethylacetate.py +17 -0
  52. processpi/components/methanol.py +20 -0
  53. processpi/components/toluene.py +22 -0
  54. processpi/components/water.py +24 -0
  55. processpi/pipelines/__init__.py +0 -0
  56. processpi/pipelines/base.py +76 -0
  57. processpi/pipelines/design_rules.py +91 -0
  58. processpi/pipelines/engine.py +718 -0
  59. processpi/pipelines/equipment.py +32 -0
  60. processpi/pipelines/fittings.py +76 -0
  61. processpi/pipelines/insulation.py +0 -0
  62. processpi/pipelines/materials.py +102 -0
  63. processpi/pipelines/network.py +277 -0
  64. processpi/pipelines/pipelineresults.py +95 -0
  65. processpi/pipelines/pipes.py +84 -0
  66. processpi/pipelines/pumps.py +83 -0
  67. processpi/pipelines/selection.py +89 -0
  68. processpi/pipelines/standards.py +279 -0
  69. processpi/pipelines/vessel.py +34 -0
  70. processpi/units/__init__.py +38 -0
  71. processpi/units/area.py +51 -0
  72. processpi/units/base.py +57 -0
  73. processpi/units/density.py +47 -0
  74. processpi/units/diameter.py +58 -0
  75. processpi/units/dimensionless.py +42 -0
  76. processpi/units/heat_flux.py +46 -0
  77. processpi/units/heat_of_vaporization.py +47 -0
  78. processpi/units/heat_transfer_coefficient.py +45 -0
  79. processpi/units/length.py +47 -0
  80. processpi/units/mass.py +50 -0
  81. processpi/units/mass_flowrate.py +50 -0
  82. processpi/units/pressure.py +57 -0
  83. processpi/units/specific_heat.py +47 -0
  84. processpi/units/strings.py +31 -0
  85. processpi/units/temperature.py +51 -0
  86. processpi/units/thermal_conductivity.py +45 -0
  87. processpi/units/thermal_resistance.py +46 -0
  88. processpi/units/velocity.py +45 -0
  89. processpi/units/viscosity.py +78 -0
  90. processpi/units/volume.py +50 -0
  91. processpi/units/volumetric_flowrate.py +52 -0
  92. processpi-0.0.0.dist-info/METADATA +5 -0
  93. processpi-0.0.0.dist-info/RECORD +96 -0
  94. processpi-0.0.0.dist-info/WHEEL +5 -0
  95. processpi-0.0.0.dist-info/licenses/LICENSE +21 -0
  96. processpi-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,83 @@
1
+ from typing import Optional, Dict, Union
2
+ from .base import PipelineBase
3
+ from ..units import Power, Pressure
4
+ from .standards import PUMP_EFFICIENCIES
5
+
6
+
7
+ class Pump(PipelineBase):
8
+ """
9
+ Represents a pump in a pipeline system.
10
+
11
+ Attributes:
12
+ pump_type (str): Type of pump (e.g., 'centrifugal', 'positive_displacement').
13
+ flow_rate (float): Volumetric flow rate (m³/s).
14
+ head (float): Pump head (m), calculated automatically from pressures if provided.
15
+ density (float): Fluid density (kg/m³). Default = 1000 (water).
16
+ efficiency (float): Pump efficiency (0 < η ≤ 1). Loaded from standards if available.
17
+ inlet_pressure (Pressure): Inlet pressure to the pump.
18
+ outlet_pressure (Pressure): Outlet pressure from the pump.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ pump_type: str,
24
+ flow_rate: float,
25
+ head: Optional[float] = None,
26
+ density: float = 1000,
27
+ efficiency: Optional[float] = None,
28
+ inlet_pressure: Optional[Pressure] = None,
29
+ outlet_pressure: Optional[Pressure] = None,
30
+ ):
31
+ self.pump_type = pump_type
32
+ self.flow_rate = flow_rate
33
+ self.density = density
34
+ self.inlet_pressure = inlet_pressure
35
+ self.outlet_pressure = outlet_pressure
36
+
37
+ # Calculate head from pressure difference if available
38
+ if outlet_pressure and inlet_pressure:
39
+ dp_pa = (outlet_pressure.to("Pa").value - inlet_pressure.to("Pa").value)
40
+ self.head = dp_pa / (density * 9.81)
41
+ else:
42
+ self.head = head or 0.0 # Default head to 0 if neither pressures nor head provided
43
+
44
+ self.efficiency = efficiency or PUMP_EFFICIENCIES.get(pump_type, 0.7)
45
+
46
+ def hydraulic_power(self) -> float:
47
+ """
48
+ Calculate the hydraulic power required by the pump.
49
+ Formula:
50
+ P_h = ρ * g * Q * H (W)
51
+ """
52
+ g = 9.81 # m/s²
53
+ return self.density * g * self.flow_rate * self.head
54
+
55
+ def brake_power(self) -> float:
56
+ """
57
+ Calculate the brake power required considering efficiency.
58
+ Formula:
59
+ P_b = P_h / η (W)
60
+ """
61
+ return self.hydraulic_power() / self.efficiency if self.efficiency > 0 else 0.0
62
+
63
+ def to_dict(self) -> Dict[str, Union[str, float]]:
64
+ """
65
+ Export pump properties and calculations as a dictionary.
66
+ """
67
+ return {
68
+ "pump_type": self.pump_type,
69
+ "flow_rate_m3s": self.flow_rate,
70
+ "density_kgm3": self.density,
71
+ "efficiency": self.efficiency,
72
+ "head_m": self.head,
73
+ "hydraulic_power_W": self.hydraulic_power(),
74
+ "brake_power_W": self.brake_power(),
75
+ "inlet_pressure_Pa": self.inlet_pressure.to("Pa").value if self.inlet_pressure else None,
76
+ "outlet_pressure_Pa": self.outlet_pressure.to("Pa").value if self.outlet_pressure else None,
77
+ }
78
+
79
+ def calculate(self):
80
+ """
81
+ Perform calculations and return results.
82
+ """
83
+ return self.to_dict()
@@ -0,0 +1,89 @@
1
+ # processpi/pipelines/selection.py
2
+
3
+ """
4
+ Pipeline Material Selection Module
5
+
6
+ This module provides logic for selecting appropriate
7
+ pipeline materials based on design conditions such as
8
+ temperature, pressure, and corrosion requirements.
9
+
10
+ Uses data from materials.py
11
+ """
12
+
13
+ from typing import Dict, Any, List
14
+ from .materials import MATERIAL_PROPERTIES
15
+
16
+
17
+ class MaterialSelector:
18
+ """
19
+ Class for selecting suitable pipeline materials.
20
+ """
21
+
22
+ def __init__(self, design_temp: float, design_pressure: float, corrosive: bool = False):
23
+ """
24
+ Initialize material selector with design parameters.
25
+
26
+ Args:
27
+ design_temp (float): Design temperature in °C.
28
+ design_pressure (float): Design pressure in bar.
29
+ corrosive (bool): Whether the fluid is corrosive.
30
+ """
31
+ self.design_temp = design_temp
32
+ self.design_pressure = design_pressure
33
+ self.corrosive = corrosive
34
+
35
+ def filter_materials(self) -> List[str]:
36
+ """
37
+ Filter materials that can withstand design conditions.
38
+
39
+ Returns:
40
+ list: Suitable material codes.
41
+ """
42
+ suitable = []
43
+ for material, props in MATERIAL_PROPERTIES.items():
44
+ max_temp = props.get("max_temp", 0)
45
+ allowable_stress = props.get("allowable_stress", 0)
46
+
47
+ # Simple selection logic
48
+ if self.design_temp <= max_temp:
49
+ # Convert MPa stress to bar (1 MPa = 10 bar approx)
50
+ if self.design_pressure <= allowable_stress * 10:
51
+ if self.corrosive:
52
+ if "SS" in material or material in ["HDPE", "CPVC"]:
53
+ suitable.append(material)
54
+ else:
55
+ suitable.append(material)
56
+ return suitable
57
+
58
+ def recommend_material(self) -> Dict[str, Any]:
59
+ """
60
+ Recommend the most suitable material based on design conditions.
61
+
62
+ Returns:
63
+ dict: Recommended material data.
64
+ """
65
+ suitable = self.filter_materials()
66
+
67
+ if not suitable:
68
+ return {"message": "No suitable material found for given conditions."}
69
+
70
+ # Prefer stainless steel in corrosive service
71
+ if self.corrosive:
72
+ for pref in ["SS316", "SS304", "HDPE", "CPVC"]:
73
+ if pref in suitable:
74
+ return {"material": pref, **MATERIAL_PROPERTIES[pref]}
75
+
76
+ # Otherwise pick carbon steel first if allowed
77
+ if "CS" in suitable:
78
+ return {"material": "CS", **MATERIAL_PROPERTIES["CS"]}
79
+
80
+ # Fallback: return the first suitable option
81
+ mat = suitable[0]
82
+ return {"material": mat, **MATERIAL_PROPERTIES[mat]}
83
+
84
+
85
+ # Example usage
86
+ if __name__ == "__main__":
87
+ selector = MaterialSelector(design_temp=150, design_pressure=15, corrosive=True)
88
+ recommendation = selector.recommend_material()
89
+ print("Recommended Material:", recommendation)
@@ -0,0 +1,279 @@
1
+ # processpi/pipelines/standards.py
2
+ from ..units import *
3
+
4
+ """
5
+ Pipeline Standards Module
6
+ -------------------------
7
+ Contains standard reference data for process pipelines:
8
+ - Pipe nominal sizes
9
+ - Schedules and wall thickness
10
+ - Internal diameter lookup
11
+ - Material roughness values
12
+ - Recommended fluid velocities
13
+ """
14
+
15
+ from typing import Dict, Tuple, Optional, Union, List
16
+
17
+ # --------------------------
18
+ # 🔹 Roughness (mm)
19
+ # --------------------------
20
+ ROUGHNESS: Dict[str, float] = {
21
+ "CS": 0.045, # Carbon Steel
22
+ "SS": 0.015, # Stainless Steel
23
+ "PVC": 0.0015, # Polyvinyl Chloride
24
+ "Copper": 0.0015,
25
+ "Concrete": 0.3,
26
+ "Glass": 0.001,
27
+ "Other": 0.05, # Default
28
+ }
29
+
30
+ # --------------------------
31
+ # 🔹 Pipe Standard Sizes (in)
32
+ # --------------------------
33
+
34
+ STANDARD_SIZES = [Diameter(0.25,"in"),
35
+ Diameter(0.5,"in"),
36
+ Diameter(0.75,"in"),
37
+ Diameter(1,"in"),
38
+ Diameter(1.5,"in"),
39
+ Diameter(2,"in"),
40
+ Diameter(2.5,"in"),
41
+ Diameter(3,"in"),
42
+ Diameter(4,"in"),
43
+ Diameter(5,"in"),
44
+ Diameter(6,"in"),
45
+ Diameter(8,"in"),
46
+ Diameter(10,"in"),
47
+ Diameter(12,"in"),
48
+ Diameter(14,"in"),
49
+ Diameter(15,"in"),
50
+ Diameter(20,"in"),
51
+ Diameter(25,"in"),
52
+ Diameter(30,"in"),
53
+ Diameter(35,"in"),
54
+ Diameter(40,"in"),
55
+ Diameter(45,"in"),
56
+ Diameter(50,"in")]
57
+
58
+ # --------------------------
59
+ # 🔹 Pipe Size Database
60
+ # Nominal Diameter (mm) → { Schedule → (Wall Thickness mm, ID mm) }
61
+ # --------------------------
62
+ PIPE_SCHEDULES: Dict[Diameter, Dict[str, Tuple[Length, Diameter]]] = {
63
+ Diameter(0.25,"in"): { # 1/4"
64
+ "40": (Length(2.77,"mm"), Diameter(6.35,"mm")),
65
+ "80": (Length(3.02,"mm"), Diameter(5.49,"mm")),
66
+ },
67
+ Diameter(0.5,"in"): { # 1/2"
68
+ "40": (Length(2.77,"mm"), Diameter(15.8,"mm")),
69
+ "80": (Length(3.73,"mm"), Diameter(13.8,"mm")),
70
+ },
71
+ Diameter(0.75,"in"): { # 3/4"
72
+ "40": (Length(2.87,"mm"), Diameter(20.9,"mm")),
73
+ "80": (Length(3.91,"mm"), Diameter(18.8,"mm")),
74
+ },
75
+ Diameter(1,"in"): { # 1"
76
+ "40": (Length(3.38,"mm"), Diameter(26.6,"mm")),
77
+ "80": (Length(4.55,"mm"), Diameter(24.3,"mm")),
78
+ },
79
+ Diameter(1.5,"in"): { # 1-1/2"
80
+ "40": (Length(3.68,"mm"), Diameter(40.9,"mm")),
81
+ "80": (Length(5.08,"mm"), Diameter(37.5,"mm")),
82
+ },
83
+ Diameter(2,"in"): { # 2"
84
+ "40": (Length(3.91,"mm"), Diameter(52.5,"mm")),
85
+ "80": (Length(5.54,"mm"), Diameter(49.3,"mm")),
86
+ },
87
+ Diameter(2.5,"in"): { # 2-1/2"
88
+ "40": (Length(5.16,"mm"), Diameter(62.7,"mm")),
89
+ "80": (Length(7.01,"mm"), Diameter(59.5,"mm")),
90
+ },
91
+ Diameter(3,"in"): { # 3"
92
+ "40": (Length(5.49,"mm"), Diameter(77.9,"mm")),
93
+ "80": (Length(7.62,"mm"), Diameter(73.7,"mm")),
94
+ },
95
+ Diameter(4,"in"): { # 4"
96
+ "40": (Length(6.02,"mm"), Diameter(102.3,"mm")),
97
+ "80": (Length(8.56,"mm"), Diameter(97.2,"mm")),
98
+ },
99
+ Diameter(5,"in"): { # 5"
100
+ "40": (Length(6.55,"mm"), Diameter(127.3,"mm")),
101
+ "80": (Length(9.53,"mm"), Diameter(121.9,"mm")),
102
+ },
103
+ Diameter(6,"in"): { # 6"
104
+ "40": (Length(7.11,"mm"), Diameter(154.1,"mm")),
105
+ "80": (Length(10.97,"mm"), Diameter(146.3,"mm")),
106
+ },
107
+ Diameter(8,"in"): { # 8"
108
+ "40": (Length(8.18,"mm"), Diameter(202.7,"mm")),
109
+ "80": (Length(12.70,"mm"), Diameter(193.7,"mm")),
110
+ },
111
+ Diameter(10,"in"): { # 10"
112
+ "40": (Length(9.27,"mm"), Diameter(254.5,"mm")),
113
+ "80": (Length(15.09,"mm"), Diameter(242.8,"mm")),
114
+ },
115
+ Diameter(12,"in"): { # 12"
116
+ "40": (Length(10.31,"mm"), Diameter(303.2,"mm")),
117
+ "80": (Length(17.48,"mm"), Diameter(289.1,"mm")),
118
+ },
119
+ Diameter(14,"in"): { # 14"
120
+ "40": (Length(11.13,"mm"), Diameter(333.4,"mm")),
121
+ "80": (Length(19.05,"mm"), Diameter(318.5,"mm")),
122
+ },
123
+ Diameter(15,"in"): { # 15"
124
+ "40": (Length(11.91,"mm"), Diameter(359.1,"mm")),
125
+ "80": (Length(19.05,"mm"), Diameter(338.9,"mm")),
126
+ },
127
+ Diameter(20,"in"): { # 20"
128
+ "40": (Length(12.70,"mm"), Diameter(508.0,"mm")),
129
+ "80": (Length(23.01,"mm"), Diameter(482.6,"mm")),
130
+ },
131
+ Diameter(25,"in"): { # 25"
132
+ "40": (Length(15.88,"mm"), Diameter(635.0,"mm")),
133
+ "80": (Length(24.61,"mm"), Diameter(584.2,"mm")),
134
+ },
135
+ Diameter(30,"in"): { # 30"
136
+ "40": (Length(15.88,"mm"), Diameter(762.0,"mm")),
137
+ "80": (Length(27.78,"mm"), Diameter(730.0,"mm")),
138
+ },
139
+ Diameter(35,"in"): { # 35"
140
+ "40": (Length(19.05,"mm"), Diameter(889.0,"mm")),
141
+ "80": (Length(28.58,"mm"), Diameter(850.0,"mm")),
142
+ },
143
+ Diameter(40,"in"): { # 40"
144
+ "40": (Length(19.05,"mm"), Diameter(1016.0,"mm")),
145
+ "80": (Length(31.75,"mm"), Diameter(965.0,"mm")),
146
+ },
147
+ Diameter(45,"in"): { # 45"
148
+ "40": (Length(19.05,"mm"), Diameter(1143.0,"mm")),
149
+ "80": (Length(31.75,"mm"), Diameter(1092.0,"mm")),
150
+ },
151
+ Diameter(50,"in"): { # 50"
152
+ "40": (Length(19.05,"mm"), Diameter(1270.0,"mm")),
153
+ "80": (Length(31.75,"mm"), Diameter(1219.0,"mm")),
154
+ },
155
+ }
156
+
157
+
158
+ # --------------------------
159
+ # 🔹 Recommended Fluid Velocities (m/s)
160
+ # --------------------------
161
+ RECOMMENDED_VELOCITIES: Dict[str, Union[Tuple[float, float], float]] = {
162
+ "Water - Pump suction": (0.3, 1.5),
163
+ "Water - Pump discharge": (2.0, 3.0),
164
+ "Water - Average service": (1.0, 2.5),
165
+
166
+ "Steam - 0 to 2 atm g, saturated": (20, 30),
167
+ "Steam - 2 to 10 atm g, saturated": (30, 50),
168
+ "Steam - Superheated <10 atm g": (20, 50),
169
+ "Steam - Superheated >10 atm g": (30, 75),
170
+
171
+ "Vacuum lines": (100, 125),
172
+
173
+ "Air - 0 to 2 atm g": 20,
174
+ "Air - >2 atm g or above": 30,
175
+
176
+ "Ammonia - Liquid": 1.8,
177
+ "Ammonia - Gas": 30,
178
+
179
+ "Organic liquids and oils": (1.8, 2.0),
180
+ "Natural gas": (25, 35),
181
+
182
+ "Chlorine - Liquid": 1.5,
183
+ "Chlorine - Gas": (10, 25),
184
+
185
+ "Hydrochloric acid - Liquid": 1.5,
186
+ "Hydrochloric acid - Gas": 10,
187
+
188
+ "Inorganic liquids": (1.2, 1.8),
189
+ "Inorganic gases and vapours": (15, 30),
190
+ }
191
+ # --------------------------
192
+ # 🔹 Equivalent Lengths Equivalent Length / Diameter
193
+ # --------------------------
194
+
195
+ EQUIVALENT_LENGTHS = {
196
+ "gate_valve_open": (7, 10), # fully open
197
+ "gate_valve_3_4_closed": (800, 1100),
198
+ "gate_valve_half_closed": (190, 290),
199
+ "globe_valve_open": (330, 480),
200
+ "angle_valve_open": (165, 220),
201
+ "plug_valve_open": 18,
202
+ "elbow_90_standard": 30,
203
+ "elbow_45_long": 5.8,
204
+ "elbow_45_short": 8.0,
205
+ "return_bend_medium": (39, 56),
206
+ "coupling_union": 0, # negligible
207
+ "tee_straight": 22,
208
+ }
209
+ # --------------------------
210
+ # 🔹 K Factors
211
+ # --------------------------
212
+
213
+ K_FACTORS = {
214
+ "gate_valve_open": 0.15, # fully open
215
+ "gate_valve_3_4_closed": 17.0,
216
+ "gate_valve_half_closed": 9.5,
217
+ "globe_valve_open": 10.0,
218
+ "angle_valve_open": 5.0,
219
+ "plug_valve_open": 0.4,
220
+ "elbow_90_standard": 0.9,
221
+ "elbow_45_long": 0.35,
222
+ "elbow_45_short": 0.4,
223
+ "return_bend_medium": 2.2,
224
+ "coupling_union": 0.08,
225
+ "tee_straight": 0.6,
226
+ "tee_branch": 1.8, # flow through branch
227
+ "sudden_contraction": 0.42,
228
+ "sudden_expansion": 1.0, # (1 - (d/D)^2)^2, here avg
229
+ "entrance_sharp": 0.5,
230
+ "entrance_rounded": 0.04,
231
+ "exit": 1.0,
232
+ }
233
+
234
+ # --------------------------
235
+ # 🔹 Utility Functions
236
+ # --------------------------
237
+ def get_internal_diameter(
238
+ nominal_diameter: Diameter, schedule: str
239
+ ):
240
+ """Returns internal diameter for a given nominal diameter and schedule."""
241
+ if nominal_diameter not in PIPE_SCHEDULES:
242
+ return None
243
+ return PIPE_SCHEDULES[nominal_diameter].get(schedule, (None, None))[1]
244
+
245
+
246
+ def get_thickness(
247
+ nominal_diameter: int, schedule: str
248
+ ) -> Optional[float]:
249
+ """Returns wall thickness for a given nominal diameter and schedule."""
250
+ if nominal_diameter not in PIPE_SCHEDULES:
251
+ return None
252
+ return PIPE_SCHEDULES[nominal_diameter].get(schedule, (None, None))[0]
253
+
254
+
255
+ def get_roughness(material: str) -> float:
256
+ """Returns roughness for given material. Defaults if not found."""
257
+ return ROUGHNESS.get(material, ROUGHNESS["Other"])
258
+
259
+
260
+ def get_recommended_velocity(service: str) -> Optional[Union[float, Tuple[float, float]]]:
261
+ """
262
+ Returns recommended velocity (m/s) for a given service type.
263
+ Can be a single value or a range (tuple).
264
+ """
265
+ return RECOMMENDED_VELOCITIES.get(service, None)
266
+
267
+ def get_nearest_diameter(calculated_diameter: Diameter) -> Diameter:
268
+ """
269
+ Returns the nearest standard diameter for a given calculated diameter.
270
+ if calculated_diameter not in STANDARD_SIZES:
271
+ return calculated_diameter
272
+
273
+ """
274
+
275
+ # Find the nearest standard diameter
276
+ nearest = min(STANDARD_SIZES, key=lambda x: abs(x.value - calculated_diameter.value))
277
+ #print(f"Nearest standard diameter for {calculated_diameter.value} mm is {nearest.value} mm")
278
+ #print(type(nearest))
279
+ return nearest
@@ -0,0 +1,34 @@
1
+ # processpi/pipelines/vessel.py
2
+
3
+ class Vessel:
4
+ """
5
+ Vessel represents a storage or process vessel in the pipeline network.
6
+ It can have one or multiple inlet and outlet connections.
7
+ """
8
+ def __init__(self, name: str, volume: float = 0.0, pressure: float = 0.0, temperature: float = 0.0):
9
+ """
10
+ Initialize a Vessel.
11
+
12
+ Args:
13
+ name (str): Name of the vessel.
14
+ volume (float): Vessel capacity (m³), optional.
15
+ pressure (float): Internal pressure (bar), optional.
16
+ temperature (float): Internal temperature (°C), optional.
17
+ """
18
+ self.name = name
19
+ self.volume = volume
20
+ self.pressure = pressure
21
+ self.temperature = temperature
22
+ self.inlet_nodes = []
23
+ self.outlet_nodes = []
24
+
25
+ def add_inlet(self, node):
26
+ """Attach an inlet node to the vessel."""
27
+ self.inlet_nodes.append(node)
28
+
29
+ def add_outlet(self, node):
30
+ """Attach an outlet node to the vessel."""
31
+ self.outlet_nodes.append(node)
32
+
33
+ def __repr__(self):
34
+ return f"Vessel({self.name}, Volume={self.volume} m³, P={self.pressure} bar, T={self.temperature}°C)"
@@ -0,0 +1,38 @@
1
+ from .length import Length
2
+ from .velocity import Velocity
3
+ from .density import Density
4
+ from .viscosity import Viscosity
5
+ from .temperature import Temperature
6
+ from .dimensionless import Dimensionless
7
+ from .volumetric_flowrate import VolumetricFlowRate
8
+ from .mass_flowrate import MassFlowRate
9
+ from .diameter import Diameter
10
+ from .pressure import Pressure
11
+ from .volume import Volume
12
+ from .area import Area
13
+ from .mass import Mass
14
+ from .specific_heat import SpecificHeat
15
+ from .thermal_conductivity import ThermalConductivity
16
+ from .heat_transfer_coefficient import HeatTransferCoefficient
17
+ from .thermal_resistance import ThermalResistance
18
+ from .heat_flux import HeatFlux
19
+ from .heat_of_vaporization import HeatOfVaporization
20
+ __all__ = ["Length",
21
+ "Velocity",
22
+ "Density",
23
+ "Viscosity",
24
+ "Temperature",
25
+ "VolumetricFlowRate",
26
+ "MassFlowRate",
27
+ "Diameter",
28
+ "Pressure",
29
+ "Volume",
30
+ "Area",
31
+ "Mass",
32
+ "SpecificHeat",
33
+ "ThermalConductivity",
34
+ "HeatTransferCoefficient",
35
+ "ThermalResistance",
36
+ "HeatFlux",
37
+ "HeatOfVaporization",
38
+ "Dimensionless"]
@@ -0,0 +1,51 @@
1
+ from .base import Variable
2
+
3
+ class Area(Variable):
4
+ """
5
+ Represents an Area quantity.
6
+ Default SI unit: Square Meter (m2).
7
+
8
+ Example:
9
+ a1 = Area(100, "cm2")
10
+ a2 = Area(0.5, "m2")
11
+ """
12
+
13
+ _conversion = {
14
+ "m2": 1,
15
+ "cm2": 0.0001,
16
+ "mm2": 0.000001,
17
+ "km2": 1_000_000,
18
+ "in2": 0.00064516,
19
+ "ft2": 0.092903,
20
+ "yd2": 0.836127,
21
+ "acre": 4046.86,
22
+ "ha": 10000
23
+ }
24
+
25
+ def __init__(self, value, units="m2"):
26
+ if value < 0:
27
+ raise ValueError("Area must be non-negative")
28
+ if units not in self._conversion:
29
+ raise TypeError(f"{units} is not a valid unit for Area")
30
+ base_value = round(value * self._conversion[units], 6)
31
+ super().__init__(base_value, "m2")
32
+ self.original_value = value
33
+ self.original_unit = units
34
+
35
+ def to(self, target_unit):
36
+ if target_unit not in self._conversion:
37
+ raise TypeError(f"{target_unit} is not a valid unit for Area")
38
+ converted_value = self.value / self._conversion[target_unit]
39
+ return Area(round(converted_value, 6), target_unit)
40
+
41
+ def __add__(self, other):
42
+ if not isinstance(other, Area):
43
+ raise TypeError("Addition only supported between Area instances")
44
+ total = self.value + other.value
45
+ return Area(round(total, 6), "m2")
46
+
47
+ def __eq__(self, other):
48
+ return isinstance(other, Area) and self.value == other.value
49
+
50
+ def __repr__(self):
51
+ return f"{self.original_value} {self.original_unit}"
@@ -0,0 +1,57 @@
1
+ class Variable:
2
+ """
3
+ A generic physical variable with value and units.
4
+ Should be subclassed for specific physical types.
5
+ """
6
+
7
+ def __init__(self, value: float, units: str):
8
+ self.value = value
9
+ self.units = units
10
+
11
+ def __str__(self):
12
+ return f"{self.value} {self.units}"
13
+
14
+ def __repr__(self):
15
+ return f"<{self.__class__.__name__}: {self.value} {self.units}>"
16
+
17
+ def __eq__(self, other):
18
+ return (
19
+ isinstance(other, self.__class__) and
20
+ self.to_base() == other.to_base()
21
+ )
22
+
23
+ def __ne__(self, other):
24
+ return not self.__eq__(other)
25
+
26
+ def __lt__(self, other):
27
+ if not isinstance(other, self.__class__):
28
+ return NotImplemented
29
+ return self.to_base() < other.to_base()
30
+
31
+ def __le__(self, other):
32
+ if not isinstance(other, self.__class__):
33
+ return NotImplemented
34
+ return self.to_base() <= other.to_base()
35
+
36
+ def __gt__(self, other):
37
+ if not isinstance(other, self.__class__):
38
+ return NotImplemented
39
+ return self.to_base() > other.to_base()
40
+
41
+ def __ge__(self, other):
42
+ if not isinstance(other, self.__class__):
43
+ return NotImplemented
44
+ return self.to_base() >= other.to_base()
45
+
46
+ def __hash__(self):
47
+ # Use tuple of class name + normalized value (in base units) for immutability
48
+ return hash((self.__class__.__name__, self.to_base()))
49
+
50
+ def to(self, target_units: str):
51
+ raise NotImplementedError("Override this method in subclass.")
52
+
53
+ def to_base(self):
54
+ raise NotImplementedError("Override this method in subclass.")
55
+
56
+ def from_base(self, base_value: float, target_units: str):
57
+ raise NotImplementedError("Override this method in subclass.")
@@ -0,0 +1,47 @@
1
+ from .base import Variable
2
+
3
+ class Density(Variable):
4
+ """
5
+ Represents a Density quantity.
6
+ Default SI unit: kilograms per cubic meter (kg/m³).
7
+
8
+ Example:
9
+ d1 = Density(1000) # 1000 kg/m³
10
+ d2 = Density(1, "g/cm³") # 1 gram per cubic centimeter
11
+ """
12
+
13
+ _conversion = {
14
+ "kg/m3": 1,
15
+ "g/cm3": 1000,
16
+ "g/mL": 1000,
17
+ "lb/ft3": 16.0185,
18
+ "lb/in3": 27679.9
19
+ }
20
+
21
+ def __init__(self, value, units="kg/m3"):
22
+ if value < 0:
23
+ raise ValueError("Density must be a non-negative value")
24
+ if units not in self._conversion:
25
+ raise ValueError(f"{units} is not a valid unit for Density")
26
+ base_value = round(value * self._conversion[units], 6)
27
+ super().__init__(base_value, "kg/m3") # Always store as base unit (kg/m³)
28
+ self.original_value = value
29
+ self.original_unit = units
30
+
31
+ def to(self, target_unit):
32
+ if target_unit not in self._conversion:
33
+ raise ValueError(f"{target_unit} is not a valid unit for Density")
34
+ converted_value = self.value / self._conversion[target_unit]
35
+ return Density(converted_value, target_unit)
36
+
37
+ def __add__(self, other):
38
+ if not isinstance(other, Density):
39
+ raise TypeError("Addition only allowed between Density objects")
40
+ result_in_base = self.value + other.value
41
+ return Density(result_in_base / self._conversion[self.original_unit], self.original_unit)
42
+
43
+ def __eq__(self, other):
44
+ return isinstance(other, Density) and self.value == other.value
45
+
46
+ def __repr__(self):
47
+ return f"{self.original_value} {self.original_unit}"