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,32 @@
1
+
2
+ class Equipment:
3
+ """
4
+ Generic inline process equipment with inlet, outlet, and pressure drop.
5
+ Can represent filters, heat exchangers, valves, or other unit operations.
6
+ """
7
+ def __init__(self, name: str, pressure_drop: float = 0.0, description: str = ""):
8
+ """
9
+ Initialize Equipment.
10
+
11
+ Args:
12
+ name (str): Equipment name.
13
+ pressure_drop (float): Pressure drop across equipment (bar).
14
+ description (str): Optional description (e.g., 'Heat Exchanger', 'Filter').
15
+ """
16
+ self.name = name
17
+ self.pressure_drop = pressure_drop
18
+ self.description = description
19
+ self.inlet_nodes = []
20
+ self.outlet_nodes = []
21
+
22
+ def add_inlet(self, node):
23
+ """Attach an inlet node to the equipment."""
24
+ self.inlet_nodes.append(node)
25
+
26
+ def add_outlet(self, node):
27
+ """Attach an outlet node to the equipment."""
28
+ self.outlet_nodes.append(node)
29
+
30
+ def __repr__(self):
31
+ return (f"Equipment({self.name}, ΔP={self.pressure_drop} bar, "
32
+ f"Desc='{self.description}')")
@@ -0,0 +1,76 @@
1
+ # processpi/pipelines/fittings.py
2
+
3
+ from typing import Optional, Dict, Union
4
+ from .base import PipelineBase
5
+ from ..units import *
6
+ from .standards import EQUIVALENT_LENGTHS, K_FACTORS
7
+
8
+
9
+ class Fitting(PipelineBase):
10
+ """
11
+ Represents a pipeline fitting or valve with either equivalent length or K-factor.
12
+
13
+ Attributes:
14
+ fitting_type (str): Type of fitting/valve (e.g., 'gate_valve_open', 'globe_valve').
15
+ diameter (float): Inside diameter of the pipe (mm).
16
+ quantity (int): Number of fittings of this type.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ fitting_type: str,
22
+ diameter: Diameter,
23
+ quantity: int = 1,
24
+ ):
25
+ self.fitting_type = fitting_type
26
+ self.diameter = diameter
27
+ self.quantity = quantity
28
+
29
+ # Load equivalent length factor if available
30
+ self.le_factor = EQUIVALENT_LENGTHS.get(fitting_type, None)
31
+
32
+ # Load K-factor if available
33
+ self.K = K_FACTORS.get(fitting_type, None)
34
+
35
+ def equivalent_length(self) -> Optional[float]:
36
+ """
37
+ Calculate equivalent length of the fitting(s) in meters.
38
+
39
+ Returns:
40
+ float: Equivalent length (m) or None if not defined.
41
+ """
42
+ if self.le_factor is None:
43
+ return None
44
+ dj_m = self.diameter / 1000 # mm → m
45
+ return self.le_factor * dj_m * self.quantity
46
+
47
+ def total_K(self) -> Optional[float]:
48
+ """
49
+ Calculate total K-factor for the fitting(s).
50
+
51
+ Returns:
52
+ float: Total K-factor or None if not defined.
53
+ """
54
+ if self.K is None:
55
+ return None
56
+ return self.K * self.quantity
57
+
58
+ def to_dict(self) -> Dict[str, Union[str, float, None]]:
59
+ """
60
+ Export fitting properties as dictionary for reporting.
61
+ """
62
+ return {
63
+ "fitting_type": self.fitting_type,
64
+ "diameter_mm": self.diameter,
65
+ "quantity": self.quantity,
66
+ "le_factor": self.le_factor,
67
+ "equivalent_length_m": self.equivalent_length(),
68
+ "K": self.K,
69
+ "total_K": self.total_K(),
70
+ }
71
+
72
+ def calculate(self):
73
+ """
74
+ Perform the pipeline calculation (placeholder).
75
+ """
76
+ return self.to_dict()
File without changes
@@ -0,0 +1,102 @@
1
+ # processpi/pipelines/materials.py
2
+
3
+ """
4
+ Pipeline Materials Specifications and Properties
5
+
6
+ This module contains physical and mechanical properties of
7
+ pipeline materials commonly used in process industries.
8
+
9
+ All values are representative and may vary depending on
10
+ standards (ASME, ASTM, ISO) and manufacturer data.
11
+ """
12
+
13
+ from typing import Dict, Any
14
+
15
+
16
+ # Dictionary of material properties
17
+ MATERIAL_PROPERTIES: Dict[str, Dict[str, Any]] = {
18
+ "CS": { # Carbon Steel
19
+ "density": 7850, # kg/m³
20
+ "thermal_conductivity": 54, # W/m·K
21
+ "roughness": 0.045, # mm
22
+ "max_temp": 425, # °C (approximate)
23
+ "allowable_stress": 138, # MPa
24
+ "notes": "Standard carbon steel pipes, widely used in process piping."
25
+ },
26
+ "SS304": { # Stainless Steel 304
27
+ "density": 8000,
28
+ "thermal_conductivity": 16.2,
29
+ "roughness": 0.015,
30
+ "max_temp": 870,
31
+ "allowable_stress": 137,
32
+ "notes": "Good corrosion resistance, general chemical service."
33
+ },
34
+ "SS316": { # Stainless Steel 316
35
+ "density": 8000,
36
+ "thermal_conductivity": 16.3,
37
+ "roughness": 0.015,
38
+ "max_temp": 925,
39
+ "allowable_stress": 137,
40
+ "notes": "Better corrosion resistance than SS304, widely used in chemical plants."
41
+ },
42
+ "PVC": { # Polyvinyl Chloride
43
+ "density": 1380,
44
+ "thermal_conductivity": 0.19,
45
+ "roughness": 0.0015,
46
+ "max_temp": 60,
47
+ "allowable_stress": 13.8,
48
+ "notes": "Lightweight, used in water and low-pressure service."
49
+ },
50
+ "CPVC": { # Chlorinated Polyvinyl Chloride
51
+ "density": 1500,
52
+ "thermal_conductivity": 0.14,
53
+ "roughness": 0.0015,
54
+ "max_temp": 90,
55
+ "allowable_stress": 17,
56
+ "notes": "Higher temperature resistance than PVC."
57
+ },
58
+ "HDPE": { # High Density Polyethylene
59
+ "density": 950,
60
+ "thermal_conductivity": 0.42,
61
+ "roughness": 0.007,
62
+ "max_temp": 60,
63
+ "allowable_stress": 5,
64
+ "notes": "Corrosion resistant, flexible, low-pressure applications."
65
+ },
66
+ "Copper": {
67
+ "density": 8960,
68
+ "thermal_conductivity": 401,
69
+ "roughness": 0.0015,
70
+ "max_temp": 200,
71
+ "allowable_stress": 70,
72
+ "notes": "Excellent thermal conductivity, used in heat exchangers."
73
+ }
74
+ }
75
+
76
+
77
+ def get_material_property(material: str, prop: str) -> Any:
78
+ """
79
+ Get a specific property of a pipeline material.
80
+
81
+ Args:
82
+ material (str): Material code (e.g., 'CS', 'SS316', 'PVC').
83
+ prop (str): Property name (e.g., 'density', 'roughness').
84
+
85
+ Returns:
86
+ Any: Property value if found, else None.
87
+ """
88
+ return MATERIAL_PROPERTIES.get(material, {}).get(prop)
89
+
90
+
91
+ def get_material_data(material: str) -> Dict[str, Any]:
92
+ """
93
+ Get all properties for a given pipeline material.
94
+
95
+ Args:
96
+ material (str): Material code.
97
+
98
+ Returns:
99
+ dict: Dictionary of material properties.
100
+ """
101
+ return MATERIAL_PROPERTIES.get(material, {})
102
+
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+ from typing import List, Dict, Union, Optional, Any
3
+
4
+ from .pipes import Pipe
5
+ from .fittings import Fitting
6
+ from .vessel import Vessel
7
+ from .pump import Pump
8
+ from .equipment import Equipment
9
+
10
+
11
+ class Node:
12
+ """Node represents a junction or endpoint in the pipeline network."""
13
+ def __init__(self, name: str, elevation: float = 0.0):
14
+ self.name = name
15
+ self.elevation = elevation
16
+
17
+ def __repr__(self):
18
+ return f"Node({self.name}, elevation={self.elevation})"
19
+
20
+
21
+ # A branch in a parallel block can be either a PipelineNetwork or a single element
22
+ Branch = Union["PipelineNetwork", Pipe, Fitting, Pump, Vessel, Equipment]
23
+
24
+
25
+ class PipelineNetwork:
26
+ """
27
+ Framework for defining a process pipeline network consisting of pipes,
28
+ fittings, pumps, vessels, equipment, and sub-networks.
29
+
30
+ This version supports both node/edge construction AND composable
31
+ series/parallel blocks compatible with the PipelineEngine:
32
+ - connection_type: "series" (default) or "parallel"
33
+ - elements: a list of elements or child PipelineNetwork objects
34
+ """
35
+
36
+ # ---------------- Init ----------------
37
+ def __init__(self, name: str, connection_type: Optional[str] = "series"):
38
+ if connection_type not in (None, "series", "parallel"):
39
+ raise ValueError("connection_type must be 'series' or 'parallel' (or None which defaults to 'series').")
40
+ self.name = name
41
+ self.nodes: Dict[str, Node] = {}
42
+ self.elements: List[Branch] = []
43
+ self.connection_type: str = connection_type or "series"
44
+
45
+ # ---------------- Convenience constructors (engine-friendly) -------------
46
+ @staticmethod
47
+ def series(name: str, *elements: Branch) -> "PipelineNetwork":
48
+ """Create a series block populated with elements."""
49
+ net = PipelineNetwork(name=name, connection_type="series")
50
+ net.elements.extend(elements)
51
+ return net
52
+
53
+ @staticmethod
54
+ def parallel(name: str, *branches: Branch) -> "PipelineNetwork":
55
+ """Create a parallel block populated with branch elements or child networks."""
56
+ net = PipelineNetwork(name=name, connection_type="parallel")
57
+ net.elements.extend(branches)
58
+ return net
59
+
60
+ # ---------------- Composable builders ------------------------------------
61
+ def add(self, *elements: Branch) -> "PipelineNetwork":
62
+ """Append one or more elements to the current block (series or parallel)."""
63
+ self.elements.extend(elements)
64
+ return self
65
+
66
+ def add_series(self, *elements: Branch) -> "PipelineNetwork":
67
+ """
68
+ Add a series group. If the current block is series, append directly.
69
+ If current block is parallel, wrap as a child series network (a branch).
70
+ """
71
+ if self.connection_type == "series":
72
+ self.elements.extend(elements)
73
+ return self
74
+ series_child = PipelineNetwork(name=f"{self.name}-series-{len(self.elements)+1}", connection_type="series")
75
+ series_child.elements.extend(elements)
76
+ self.elements.append(series_child)
77
+ return self
78
+
79
+ def add_parallel(self, *branches: Branch) -> "PipelineNetwork":
80
+ """
81
+ Add a parallel group. If current block is parallel, append as new branches.
82
+ If current block is series, wrap as a child parallel network (one element).
83
+ """
84
+ if self.connection_type == "parallel":
85
+ self.elements.extend(branches)
86
+ return self
87
+ par_child = PipelineNetwork(name=f"{self.name}-parallel-{len(self.elements)+1}", connection_type="parallel")
88
+ par_child.elements.extend(branches)
89
+ self.elements.append(par_child)
90
+ return self
91
+
92
+ # ---------------- Node Management ----------------
93
+ def add_node(self, name: str, elevation: float = 0.0) -> Node:
94
+ """Add a node (junction) to the network."""
95
+ if name in self.nodes:
96
+ raise ValueError(f"Node '{name}' already exists in the network.")
97
+ node = Node(name, elevation)
98
+ self.nodes[name] = node
99
+ return node
100
+
101
+ def get_node(self, name: str) -> Node:
102
+ """Fetch an existing node by name."""
103
+ if name not in self.nodes:
104
+ raise KeyError(f"Node '{name}' does not exist in the network.")
105
+ return self.nodes[name]
106
+
107
+ # ---------------- Edge Management ----------------
108
+ def add_edge(self, component: Union[Pipe, Pump, Vessel, Equipment], start_node: str, end_node: str = None):
109
+ """
110
+ Add a connection (edge) between nodes with strict validation.
111
+ Edges added here are automatically appended to this block's elements.
112
+ """
113
+ if start_node not in self.nodes:
114
+ raise ValueError(f"Start node '{start_node}' not found in network.")
115
+
116
+ # Prevent start=end self-loops
117
+ if end_node and start_node == end_node:
118
+ raise ValueError(f"Cannot create self-loop on node '{start_node}'.")
119
+
120
+ if isinstance(component, (Pipe, Pump)):
121
+ if not end_node or end_node not in self.nodes:
122
+ raise ValueError(f"End node '{end_node}' not found in network.")
123
+ component.start_node = self.nodes[start_node]
124
+ component.end_node = self.nodes[end_node]
125
+
126
+ elif isinstance(component, (Vessel, Equipment)):
127
+ if not end_node or end_node not in self.nodes:
128
+ raise ValueError(f"Outlet node '{end_node}' not found in network.")
129
+ component.add_inlet(self.nodes[start_node])
130
+ component.add_outlet(self.nodes[end_node])
131
+
132
+ else:
133
+ raise TypeError(f"Unsupported component type '{type(component).__name__}'.")
134
+
135
+ self.elements.append(component)
136
+
137
+ # ---------------- Element Management ----------------
138
+ def add_fitting(self, fitting: Fitting, at_node: str):
139
+ """Add a fitting at a specific node."""
140
+ if at_node not in self.nodes:
141
+ raise ValueError(f"Node '{at_node}' must exist in the network.")
142
+ fitting.node = self.nodes[at_node]
143
+ self.elements.append(fitting)
144
+
145
+ def add_subnetwork(self, subnetwork: "PipelineNetwork", connection_type: str):
146
+ """
147
+ Add a subnetwork (series or parallel) as a child element.
148
+ This preserves compatibility with existing code that constructs subnetworks separately.
149
+ """
150
+ if connection_type not in ["series", "parallel"]:
151
+ raise ValueError("connection_type must be 'series' or 'parallel'")
152
+ if subnetwork is self:
153
+ raise ValueError("Cannot add the network as a subnetwork of itself.")
154
+ subnetwork.connection_type = connection_type
155
+ self.elements.append(subnetwork)
156
+
157
+ # ---------------- Validation ----------------
158
+ def validate(self):
159
+ """Check for common network errors and raise descriptive exceptions."""
160
+ errors = []
161
+
162
+ # Collect connected node names from elements in this block only
163
+ connected = set()
164
+ for elem in self.elements:
165
+ if isinstance(elem, (Pipe, Pump)):
166
+ if getattr(elem, "start_node", None) and getattr(elem, "end_node", None):
167
+ connected.update([elem.start_node.name, elem.end_node.name])
168
+ elif isinstance(elem, (Vessel, Equipment)):
169
+ for n in getattr(elem, "inlet_nodes", []) or []:
170
+ connected.add(n.name)
171
+ for n in getattr(elem, "outlet_nodes", []) or []:
172
+ connected.add(n.name)
173
+ elif isinstance(elem, PipelineNetwork):
174
+ # recurse into child blocks for validation too
175
+ try:
176
+ elem.validate()
177
+ except ValueError as e:
178
+ errors.append(f"In subnetwork '{elem.name}': {e}")
179
+
180
+ for node_name in self.nodes:
181
+ if node_name not in connected:
182
+ errors.append(f"Node '{node_name}' is not connected to any component.")
183
+
184
+ # in PipelineNetwork.validate()
185
+ for elem in self.elements:
186
+ if isinstance(elem, Fitting):
187
+ if not isinstance(elem.diameter, Diameter):
188
+ errors.append(f"Fitting '{elem.fitting_type}' requires 'diameter' as Diameter unit.")
189
+ if elem.total_K() is None and elem.equivalent_length() is None:
190
+ errors.append(f"Fitting '{elem.fitting_type}' has neither K nor equivalent length in standards.")
191
+ if elem.__class__.__name__ == "Pump":
192
+ if (getattr(elem, "head", 0) in (None, 0)) and not (getattr(elem, "inlet_pressure", None) and getattr(elem, "outlet_pressure", None)):
193
+ errors.append("Pump requires either (inlet & outlet pressures) or a non-zero head.")
194
+ if elem.__class__.__name__ == "Equipment":
195
+ if getattr(elem, "pressure_drop", 0.0) is None:
196
+ errors.append(f"Equipment '{getattr(elem, 'name', 'equipment')}' requires 'pressure_drop' (bar).")
197
+
198
+
199
+ if errors:
200
+ raise ValueError("Network validation failed:\n" + "\n".join(errors))
201
+ return True
202
+
203
+ # ---------------- Description ----------------
204
+ def describe(self, level: int = 0) -> str:
205
+ """Return a string representation of the network hierarchy."""
206
+ indent = " " * level
207
+ desc = f"{indent}Network: {self.name} (connection: {self.connection_type})\n"
208
+ for node in self.nodes.values():
209
+ desc += f"{indent} Node: {node.name}, Elevation={node.elevation} m\n"
210
+ for element in self.elements:
211
+ if isinstance(element, Pipe):
212
+ desc += (
213
+ f"{indent} Pipe: {getattr(element, 'nominal_diameter', 'NA')} mm, "
214
+ f"{getattr(element, 'length', 'NA')} m, {getattr(element, 'material', 'NA')}, "
215
+ f"from {element.start_node.name} → {element.end_node.name}\n"
216
+ )
217
+ elif isinstance(element, Pump):
218
+ desc += (
219
+ f"{indent} Pump: {getattr(element, 'pump_type', 'Generic')}, "
220
+ f"from {element.start_node.name} → {element.end_node.name}, "
221
+ f"Head={getattr(element, 'head', 'NA')} m, Power={getattr(element, 'power', 'NA')} kW\n"
222
+ )
223
+ elif isinstance(element, Vessel):
224
+ inlets = ", ".join([n.name for n in getattr(element, "inlet_nodes", [])])
225
+ outlets = ", ".join([n.name for n in getattr(element, "outlet_nodes", [])])
226
+ desc += f"{indent} Vessel: {getattr(element, 'name', 'Vessel')}, Volume={getattr(element, 'volume', 'NA')} m³, P={getattr(element, 'pressure', 'NA')} bar, T={getattr(element, 'temperature', 'NA')}°C\n"
227
+ desc += f"{indent} Inlets: {inlets or '—'}\n"
228
+ desc += f"{indent} Outlets: {outlets or '—'}\n"
229
+ elif isinstance(element, Equipment):
230
+ inlets = ", ".join([n.name for n in getattr(element, "inlet_nodes", [])])
231
+ outlets = ", ".join([n.name for n in getattr(element, "outlet_nodes", [])])
232
+ desc += (f"{indent} Equipment: {getattr(element, 'name', 'Equipment')}, "
233
+ f"ΔP={getattr(element, 'pressure_drop', 'NA')} bar, "
234
+ f"Type={getattr(element, 'description', 'Generic')}\n")
235
+ desc += f"{indent} Inlet(s): {inlets or '—'}\n"
236
+ desc += f"{indent} Outlet(s): {outlets or '—'}\n"
237
+ elif isinstance(element, Fitting):
238
+ where = getattr(element, 'node', None)
239
+ at = where.name if where else "unknown"
240
+ desc += f"{indent} Fitting: {getattr(element, 'fitting_type', 'Fitting')}, at {at}\n"
241
+ elif isinstance(element, PipelineNetwork):
242
+ desc += element.describe(level + 1)
243
+ return desc
244
+
245
+ # ---------------- ASCII Schematic (engine-friendly) ----------------------
246
+ def _schematic_lines(self, prefix: str = "") -> List[str]:
247
+ lines: List[str] = []
248
+ header = f"{prefix}{self.name} [{self.connection_type}]"
249
+ lines.append(header)
250
+ child_prefix = prefix + " "
251
+ if self.connection_type == "series":
252
+ for i, el in enumerate(self.elements, start=1):
253
+ if isinstance(el, PipelineNetwork):
254
+ lines.extend(el._schematic_lines(child_prefix))
255
+ else:
256
+ label = getattr(el, "name", el.__class__.__name__)
257
+ lines.append(f"{child_prefix}{i}. {label}")
258
+ else: # parallel
259
+ for i, br in enumerate(self.elements, start=1):
260
+ if isinstance(br, PipelineNetwork):
261
+ lines.append(f"{child_prefix}(branch {i})")
262
+ lines.extend(br._schematic_lines(child_prefix + " "))
263
+ else:
264
+ label = getattr(br, "name", br.__class__.__name__)
265
+ lines.append(f"{child_prefix}├─(branch {i}) {label}")
266
+ return lines
267
+
268
+ def schematic(self, level: int = 0) -> str:
269
+ """
270
+ Generate ASCII schematic representation of the pipeline network.
271
+ Kept compatible with the PipelineEngine which calls `network.schematic()`.
272
+ """
273
+ return "\n".join(self._schematic_lines())
274
+
275
+ # ---------------- Python niceties ----------------------------------------
276
+ def __repr__(self) -> str:
277
+ return f"PipelineNetwork(name={self.name!r}, type={self.connection_type!r}, elements={len(self.elements)})"
@@ -0,0 +1,95 @@
1
+ # processpi/pipelines/pipelineresults.py
2
+
3
+ from typing import Dict, Any
4
+ from ..units import Diameter, Length, Pressure
5
+
6
+ class PipelineResults:
7
+ def __init__(self, results: dict):
8
+ self.results = results
9
+
10
+ def summary(self):
11
+ """Print a clean, human-readable summary of the last run."""
12
+ results = self.results
13
+ if not results:
14
+ print("No results available. Run the engine first.")
15
+ return
16
+
17
+ print("\n=== Pipeline Summary ===")
18
+
19
+ # -------------------- Network summary --------------------
20
+ summary = results.get("summary", {})
21
+ if summary:
22
+ inlet_flow = summary.get("inlet_flow_m3_s")
23
+ total_dp = summary.get("total_pressure_drop_Pa")
24
+ if inlet_flow:
25
+ fval = inlet_flow.value if hasattr(inlet_flow, "value") else inlet_flow
26
+ print(f"Inlet Flow: {fval:.3f} m³/s")
27
+ if total_dp:
28
+ fval = total_dp.value if hasattr(total_dp, "value") else total_dp
29
+ print(f"Total Pressure Drop: {fval:.2f} Pa")
30
+
31
+ # -------------------- Single Pipe Mode --------------------
32
+ if results.get("mode") == "single":
33
+ pipe_data = results.get("pipe", {})
34
+ d = pipe_data.get("internal_diameter")
35
+ l = pipe_data.get("length")
36
+ v = results.get("velocity_m_s")
37
+ Re = results.get("reynolds_number")
38
+ f = results.get("friction_factor")
39
+ dp = results.get("pressure_drop_Pa")
40
+
41
+ print("\n--- Single Pipe ---")
42
+ if d:
43
+ print(f"Pipe Internal Diameter: {d.value:.2f} mm")
44
+ if l:
45
+ print(f"Pipe Length: {l.value:.2f} m")
46
+ if v:
47
+ print(f"Velocity: {v.value:.3f} m/s")
48
+ if Re is not None:
49
+ re_val = Re.value if hasattr(Re, "value") else Re
50
+ print(f"Reynolds Number: {re_val:.0f}")
51
+ if f is not None:
52
+ fval = f.value if hasattr(f, "value") else f
53
+ print(f"Friction Factor: {fval:.4f}")
54
+ if dp:
55
+ print(f"Pressure Drop: {dp.value:.2f} Pa")
56
+
57
+ # -------------------- Network / Multiple Pipes --------------------
58
+ pipes = results.get("pipes", [])
59
+ if pipes:
60
+ print("\n--- Pipe & Fitting Details ---")
61
+ for p in pipes:
62
+ if p["type"] == "pipe":
63
+ d = p.get("diameter_m")
64
+ v = p.get("velocity_m_s")
65
+ Re = p.get("reynolds_number")
66
+ f = p.get("friction_factor")
67
+ dp = p.get("pressure_drop_Pa")
68
+ print(f"\nPipe: {p.get('name','pipe')}")
69
+ if d:
70
+ d_val = d.value if hasattr(d, "value") else d
71
+ print(f" Diameter: {d_val:.2f} mm")
72
+ if v:
73
+ v_val = v.value if hasattr(v, "value") else v
74
+ print(f" Velocity: {v_val:.3f} m/s")
75
+ if Re is not None:
76
+ re_val = Re.value if hasattr(Re, "value") else Re
77
+ print(f" Reynolds #: {re_val:.0f}")
78
+ if f is not None:
79
+ f_val = f.value if hasattr(f, "value") else f
80
+ print(f" Friction Factor: {f_val:.4f}")
81
+ if dp:
82
+ dp_val = dp.value if hasattr(dp, "value") else dp
83
+ print(f" Pressure Drop: {dp_val:.2f} Pa")
84
+ elif p["type"] == "fitting":
85
+ dp = p.get("pressure_drop_Pa")
86
+ print(f"\nFitting: {p.get('name','fitting')}")
87
+ print(f" K-factor: {p.get('K')}")
88
+ if dp:
89
+ dp_val = dp.value if hasattr(dp, "value") else dp
90
+ print(f" Pressure Drop: {dp_val:.2f} Pa")
91
+
92
+ # -------------------- Schematic --------------------
93
+ if "schematic" in results:
94
+ print("\n=== Schematic ===")
95
+ print(results["schematic"])
@@ -0,0 +1,84 @@
1
+ # processpi/pipelines/pipes.py
2
+
3
+ from typing import Optional, Dict
4
+ from .base import PipelineBase
5
+ from ..units import *
6
+ from .standards import get_roughness, get_internal_diameter
7
+
8
+
9
+ class Pipe(PipelineBase):
10
+ """
11
+ Represents a process pipeline element with geometric and material properties.
12
+
13
+ Attributes:
14
+ nominal_diameter (Diameter): Nominal diameter of the pipe (mm or inch).
15
+ schedule (str): Pipe schedule (e.g., '40', '80', 'STD', 'XS').
16
+ material (str): Pipe material (e.g., 'CS', 'SS316', 'PVC').
17
+ length (Length): Length of the pipe.
18
+ roughness (float): Absolute roughness of the pipe (mm).
19
+ internal_diameter (Diameter): Internal diameter based on nominal diameter and schedule.
20
+ inlet_pressure (Pressure): Optional inlet pressure for the pipe.
21
+ outlet_pressure (Pressure): Optional outlet pressure for the pipe.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ nominal_diameter: Diameter,
27
+ schedule: str = "40",
28
+ material: str = "CS",
29
+ length: Optional[Length] = None,
30
+ inlet_pressure: Optional[Pressure] = None,
31
+ outlet_pressure: Optional[Pressure] = None,
32
+ ):
33
+ self.nominal_diameter = nominal_diameter
34
+ self.schedule = schedule
35
+ self.material = material
36
+ self.length = length or Length(1.0, "m") # default 1 meter
37
+ self.roughness = get_roughness(self.material)
38
+ self.internal_diameter = get_internal_diameter(self.nominal_diameter, self.schedule)
39
+ self.inlet_pressure = inlet_pressure # user can set this explicitly
40
+ self.outlet_pressure = outlet_pressure # user can set this explicitly
41
+
42
+ def cross_sectional_area(self) -> float:
43
+ """
44
+ Calculate cross-sectional flow area (m²).
45
+ """
46
+ d_m = self.internal_diameter.to("m").value
47
+ return 3.14159 * (d_m / 2) ** 2
48
+
49
+ def surface_area(self) -> float:
50
+ """
51
+ Calculate external surface area (m²) for heat loss calculations.
52
+ """
53
+ d_m = self.nominal_diameter.to("m").value
54
+ return 3.14159 * d_m * self.length.to("m").value
55
+
56
+ def pressure_difference(self) -> Optional[Pressure]:
57
+ """
58
+ Calculate the pressure difference (inlet - outlet) if both are defined.
59
+ """
60
+ if self.inlet_pressure and self.outlet_pressure:
61
+ return self.inlet_pressure - self.outlet_pressure
62
+ return None
63
+
64
+ def to_dict(self) -> Dict[str, Optional[float]]:
65
+ """
66
+ Export pipe properties as dictionary for reporting.
67
+ """
68
+ return {
69
+ "nominal_diameter_mm": self.nominal_diameter.value,
70
+ "schedule": self.schedule,
71
+ "material": self.material,
72
+ "length_m": self.length.to("m").value,
73
+ "roughness_mm": self.roughness,
74
+ "internal_diameter_mm": self.internal_diameter.value if self.internal_diameter else None,
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
+ "pressure_difference_Pa": self.pressure_difference().to("Pa").value if self.pressure_difference() else None,
78
+ }
79
+
80
+ def calculate(self):
81
+ """
82
+ Placeholder for pipe-specific calculations.
83
+ """
84
+ pass