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.
- processpi/__init__.py +1 -0
- processpi/calculations/__init__.py +3 -0
- processpi/calculations/base.py +55 -0
- processpi/calculations/engine.py +69 -0
- processpi/calculations/fluids/__init__.py +13 -0
- processpi/calculations/fluids/cool_brook_white.py +49 -0
- processpi/calculations/fluids/flow_type.py +37 -0
- processpi/calculations/fluids/optimium_pipe_dia.py +45 -0
- processpi/calculations/fluids/pressure_drop_darcy.py +25 -0
- processpi/calculations/fluids/pressure_drop_fanning.py +25 -0
- processpi/calculations/fluids/pump_power.py +34 -0
- processpi/calculations/fluids/reynolds_number.py +24 -0
- processpi/calculations/fluids/velocity.py +36 -0
- processpi/calculations/heat_transfer/__init__.py +4 -0
- processpi/calculations/heat_transfer/conduction_heat_loss.py +26 -0
- processpi/calculations/heat_transfer/convection_heat_loss.py +23 -0
- processpi/calculations/heat_transfer/heat_exchanger_area.py +26 -0
- processpi/calculations/mass_transfer/__init__.py +3 -0
- processpi/calculations/mass_transfer/distillation_stage_count.py +346 -0
- processpi/calculations/mass_transfer/drying_rate.py +132 -0
- processpi/calculations/reaction_engineering/__init__.py +4 -0
- processpi/calculations/reaction_engineering/catalyst_activity.py +109 -0
- processpi/calculations/reaction_engineering/reaction_rate.py +143 -0
- processpi/calculations/reaction_engineering/residence_time.py +80 -0
- processpi/calculations/thermodynamics/__init__.py +4 -0
- processpi/calculations/thermodynamics/enthalpy_change.py +23 -0
- processpi/calculations/thermodynamics/entropy_change.py +25 -0
- processpi/calculations/thermodynamics/heat_of_vaporization.py +20 -0
- processpi/components/__init__.py +5 -0
- processpi/components/aceticacid.py +19 -0
- processpi/components/acetone.py +20 -0
- processpi/components/acrylicacid.py +19 -0
- processpi/components/air.py +20 -0
- processpi/components/ammonia.py +19 -0
- processpi/components/base.py +85 -0
- processpi/components/benzene.py +40 -0
- processpi/components/benzoicacid.py +19 -0
- processpi/components/bromine.py +19 -0
- processpi/components/butane.py +19 -0
- processpi/components/carbondioxide.py +19 -0
- processpi/components/carbonmonoxide.py +17 -0
- processpi/components/carbontetrachloride.py +17 -0
- processpi/components/chlorine.py +17 -0
- processpi/components/chlorobenzene.py +17 -0
- processpi/components/chloroform.py +17 -0
- processpi/components/chloromethane.py +17 -0
- processpi/components/cyanogen.py +17 -0
- processpi/components/cyclohexene.py +17 -0
- processpi/components/ethane.py +17 -0
- processpi/components/ethanol.py +20 -0
- processpi/components/ethylacetate.py +17 -0
- processpi/components/methanol.py +20 -0
- processpi/components/toluene.py +22 -0
- processpi/components/water.py +24 -0
- processpi/pipelines/__init__.py +0 -0
- processpi/pipelines/base.py +76 -0
- processpi/pipelines/design_rules.py +91 -0
- processpi/pipelines/engine.py +718 -0
- processpi/pipelines/equipment.py +32 -0
- processpi/pipelines/fittings.py +76 -0
- processpi/pipelines/insulation.py +0 -0
- processpi/pipelines/materials.py +102 -0
- processpi/pipelines/network.py +277 -0
- processpi/pipelines/pipelineresults.py +95 -0
- processpi/pipelines/pipes.py +84 -0
- processpi/pipelines/pumps.py +83 -0
- processpi/pipelines/selection.py +89 -0
- processpi/pipelines/standards.py +279 -0
- processpi/pipelines/vessel.py +34 -0
- processpi/units/__init__.py +38 -0
- processpi/units/area.py +51 -0
- processpi/units/base.py +57 -0
- processpi/units/density.py +47 -0
- processpi/units/diameter.py +58 -0
- processpi/units/dimensionless.py +42 -0
- processpi/units/heat_flux.py +46 -0
- processpi/units/heat_of_vaporization.py +47 -0
- processpi/units/heat_transfer_coefficient.py +45 -0
- processpi/units/length.py +47 -0
- processpi/units/mass.py +50 -0
- processpi/units/mass_flowrate.py +50 -0
- processpi/units/pressure.py +57 -0
- processpi/units/specific_heat.py +47 -0
- processpi/units/strings.py +31 -0
- processpi/units/temperature.py +51 -0
- processpi/units/thermal_conductivity.py +45 -0
- processpi/units/thermal_resistance.py +46 -0
- processpi/units/velocity.py +45 -0
- processpi/units/viscosity.py +78 -0
- processpi/units/volume.py +50 -0
- processpi/units/volumetric_flowrate.py +52 -0
- processpi-0.0.0.dist-info/METADATA +5 -0
- processpi-0.0.0.dist-info/RECORD +96 -0
- processpi-0.0.0.dist-info/WHEEL +5 -0
- processpi-0.0.0.dist-info/licenses/LICENSE +21 -0
- 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"]
|
processpi/units/area.py
ADDED
|
@@ -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}"
|
processpi/units/base.py
ADDED
|
@@ -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}"
|