engforge 0.3.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.
- engforge/__init__.py +26 -0
- engforge/_testing_components.py +864 -0
- engforge/analysis.py +257 -0
- engforge/attr_dynamics.py +222 -0
- engforge/attr_plotting.py +811 -0
- engforge/attr_signals.py +116 -0
- engforge/attr_slots.py +211 -0
- engforge/attr_solver.py +443 -0
- engforge/attributes.py +326 -0
- engforge/common.py +76 -0
- engforge/component_collections.py +276 -0
- engforge/components.py +55 -0
- engforge/configuration.py +673 -0
- engforge/dataframe.py +185 -0
- engforge/datastores/__init__.py +116 -0
- engforge/datastores/data.py +519 -0
- engforge/datastores/gdocs.py +2650 -0
- engforge/datastores/reporting.py +935 -0
- engforge/datastores/secrets.py +549 -0
- engforge/dynamics.py +662 -0
- engforge/eng/__init__.py +0 -0
- engforge/eng/costs.py +1459 -0
- engforge/eng/fluid_material.py +279 -0
- engforge/eng/geometry.py +1060 -0
- engforge/eng/pipes.py +1211 -0
- engforge/eng/prediction.py +366 -0
- engforge/eng/solid_materials.py +445 -0
- engforge/eng/structure.py +2228 -0
- engforge/eng/structure_beams.py +821 -0
- engforge/eng/thermodynamics.py +271 -0
- engforge/engforge_attributes.py +380 -0
- engforge/env_var.py +214 -0
- engforge/locations.py +18 -0
- engforge/logging.py +290 -0
- engforge/patterns.py +449 -0
- engforge/problem_context.py +2251 -0
- engforge/properties.py +501 -0
- engforge/reporting.py +595 -0
- engforge/solveable.py +964 -0
- engforge/solver.py +316 -0
- engforge/solver_utils.py +726 -0
- engforge/system.py +143 -0
- engforge/system_reference.py +299 -0
- engforge/tabulation.py +288 -0
- engforge/test/__init__.py +2 -0
- engforge/test/_pre_test_structures.py +221 -0
- engforge/test/report_testing.py +158 -0
- engforge/test/test_airfilter.py +91 -0
- engforge/test/test_analysis.py +3 -0
- engforge/test/test_comp_iter.py +196 -0
- engforge/test/test_composition.py +139 -0
- engforge/test/test_costs.py +338 -0
- engforge/test/test_dynamics.py +109 -0
- engforge/test/test_dynamics_spaces.py +39 -0
- engforge/test/test_four_bar.py +97 -0
- engforge/test/test_modules.py +72 -0
- engforge/test/test_performance.py +93 -0
- engforge/test/test_pipes.py +49 -0
- engforge/test/test_problem.py +411 -0
- engforge/test/test_problem_deepscoping.py +68 -0
- engforge/test/test_slider_crank.py +51 -0
- engforge/test/test_solver.py +343 -0
- engforge/test/test_tabulation.py +233 -0
- engforge/typing.py +92 -0
- engforge-0.3.0.dist-info/METADATA +392 -0
- engforge-0.3.0.dist-info/RECORD +69 -0
- engforge-0.3.0.dist-info/WHEEL +5 -0
- engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
- engforge-0.3.0.dist-info/top_level.txt +1 -0
engforge/eng/pipes.py
ADDED
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
"""We'll use the QP formulation to develop a fluid analysis system for fluids
|
|
2
|
+
start with single phase and move to others
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
1) Pumps Power = C x Q x P
|
|
6
|
+
2) Compressor = C x (QP) x (PR^C2 - 1)
|
|
7
|
+
3) Pipes = dP = C x fXL/D x V^2 / 2g
|
|
8
|
+
4) Pipe Fittings / Valves = dP = C x V^2 (fXL/D +K) | K is a constant, for valves it can be interpolated between closed and opened
|
|
9
|
+
5) Splitters & Joins: Handle fluid mixing
|
|
10
|
+
6) Phase Separation (This one is gonna be hard)
|
|
11
|
+
7) Heat Exchanger dP = f(V,T) - phase change issues
|
|
12
|
+
8) Filtration dP = k x Vxc x (Afrontal/Asurface) "linear"
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from engforge.components import Component
|
|
16
|
+
from engforge.configuration import forge
|
|
17
|
+
from engforge.system_reference import Ref
|
|
18
|
+
from engforge.tabulation import (
|
|
19
|
+
system_property,
|
|
20
|
+
NUMERIC_VALIDATOR,
|
|
21
|
+
STR_VALIDATOR,
|
|
22
|
+
)
|
|
23
|
+
from engforge.eng.fluid_material import FluidMaterial
|
|
24
|
+
from engforge.common import G_grav_constant
|
|
25
|
+
from engforge.attr_slots import *
|
|
26
|
+
from engforge.attr_signals import *
|
|
27
|
+
from engforge.logging import LoggingMixin
|
|
28
|
+
from engforge.system import System
|
|
29
|
+
from engforge.properties import *
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
import networkx as nx
|
|
33
|
+
|
|
34
|
+
import attr, attrs
|
|
35
|
+
|
|
36
|
+
import numpy
|
|
37
|
+
import fluids
|
|
38
|
+
|
|
39
|
+
import attrs
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PipeLog(LoggingMixin):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
log = PipeLog()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# TODO: add compressibility effects
|
|
50
|
+
@forge
|
|
51
|
+
class PipeNode(Component):
|
|
52
|
+
x: float = attrs.field()
|
|
53
|
+
y: float = attrs.field()
|
|
54
|
+
z: float = attrs.field()
|
|
55
|
+
|
|
56
|
+
_segments: list # created on init
|
|
57
|
+
|
|
58
|
+
def __on_init__(self):
|
|
59
|
+
self._segments = []
|
|
60
|
+
|
|
61
|
+
def add_segment(self, pipe: "PipeFlow"):
|
|
62
|
+
if pipe not in self.segments:
|
|
63
|
+
self._segments.append(pipe)
|
|
64
|
+
else:
|
|
65
|
+
self.warning(f"pipe already added: {pipe}")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def segments(self):
|
|
69
|
+
return self._segments
|
|
70
|
+
|
|
71
|
+
@system_property
|
|
72
|
+
def sum_of_flows(self) -> float:
|
|
73
|
+
out = 0
|
|
74
|
+
for pipe_seg in self._segments:
|
|
75
|
+
if self is pipe_seg.node_s:
|
|
76
|
+
out -= pipe_seg.Q
|
|
77
|
+
elif self is pipe_seg.node_e:
|
|
78
|
+
out += pipe_seg.Q
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@forge
|
|
83
|
+
class PipeFlow(Component):
|
|
84
|
+
D: float = attrs.field()
|
|
85
|
+
v: float = attrs.field(default=0)
|
|
86
|
+
material = Slot.define(FluidMaterial)
|
|
87
|
+
|
|
88
|
+
def set_flow(self, flow):
|
|
89
|
+
v = flow / self.A
|
|
90
|
+
self.v = v
|
|
91
|
+
|
|
92
|
+
# Geometric Propreties
|
|
93
|
+
@system_property
|
|
94
|
+
def A(self) -> float:
|
|
95
|
+
""":returns: the cross sectional area of pipe in [m2]"""
|
|
96
|
+
return 3.1415 * (self.D / 2.0) ** 2.0
|
|
97
|
+
|
|
98
|
+
@system_property
|
|
99
|
+
def C(self) -> float:
|
|
100
|
+
""":returns: the sectional circmerence of pipe"""
|
|
101
|
+
return 3.1415 * self.D
|
|
102
|
+
|
|
103
|
+
# Fluid Propreties
|
|
104
|
+
@system_property
|
|
105
|
+
def Q(self) -> float:
|
|
106
|
+
""":returns: the volumetric flow through this pipe in [m3/s]"""
|
|
107
|
+
return self.A * self.v
|
|
108
|
+
|
|
109
|
+
@system_property
|
|
110
|
+
def Mf(self) -> float:
|
|
111
|
+
""":returns: the massflow through this pipe in [kg/s]"""
|
|
112
|
+
return self.density * self.Q
|
|
113
|
+
|
|
114
|
+
@system_property
|
|
115
|
+
def reynoldsNumber(self) -> float:
|
|
116
|
+
""":returns: the flow reynolds number"""
|
|
117
|
+
o = abs(self.density * self.v * self.D / self.viscosity)
|
|
118
|
+
return max(o, 1)
|
|
119
|
+
|
|
120
|
+
# Material Properties Exposure
|
|
121
|
+
@system_property
|
|
122
|
+
def density(self) -> float:
|
|
123
|
+
return self.material.density
|
|
124
|
+
|
|
125
|
+
@system_property
|
|
126
|
+
def viscosity(self) -> float:
|
|
127
|
+
return self.material.viscosity
|
|
128
|
+
|
|
129
|
+
@system_property
|
|
130
|
+
def enthalpy(self) -> float:
|
|
131
|
+
return self.material.enthalpy
|
|
132
|
+
|
|
133
|
+
@system_property
|
|
134
|
+
def T(self) -> float:
|
|
135
|
+
return self.material.T
|
|
136
|
+
|
|
137
|
+
@T.setter
|
|
138
|
+
def T(self, new_T):
|
|
139
|
+
self.material.T = new_T
|
|
140
|
+
|
|
141
|
+
@system_property
|
|
142
|
+
def P(self) -> float:
|
|
143
|
+
return self.material.P
|
|
144
|
+
|
|
145
|
+
@P.setter
|
|
146
|
+
def P(self, new_P):
|
|
147
|
+
self.material.P = new_P
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def dP_f(self):
|
|
151
|
+
"""The loss of pressure in the pipe due to pressure"""
|
|
152
|
+
raise NotImplemented()
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def dP_p(self):
|
|
156
|
+
"""The loss of pressure in the pipe due to potential"""
|
|
157
|
+
raise NotImplemented()
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def dP_tot(self):
|
|
161
|
+
raise NotImplemented()
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def Fvec(self):
|
|
165
|
+
"""returns the fluidized vector for 1D CFD for continuity:0, and momentum:1"""
|
|
166
|
+
# TODO: add energy eq
|
|
167
|
+
# TODO: 1D CFD Pipe Network Solver :)
|
|
168
|
+
return [
|
|
169
|
+
self.density * self.v,
|
|
170
|
+
self.density * self.v**2.0 + self.P,
|
|
171
|
+
] # TODO: include Txx
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@forge
|
|
175
|
+
class Pipe(PipeFlow, Component):
|
|
176
|
+
node_s = Slot.define(PipeNode, default_ok=False)
|
|
177
|
+
node_e = Slot.define(PipeNode, default_ok=False)
|
|
178
|
+
roughness: float = attrs.field(default=0.0)
|
|
179
|
+
bend_radius: float = attrs.field(default=None)
|
|
180
|
+
|
|
181
|
+
straight_method = "Clamond"
|
|
182
|
+
laminar_method = "Schmidt laminar"
|
|
183
|
+
turbulent_method = "Schmidt turbulent"
|
|
184
|
+
|
|
185
|
+
def __on_init__(self):
|
|
186
|
+
self.node_s.add_segment(self)
|
|
187
|
+
self.node_e.add_segment(self)
|
|
188
|
+
|
|
189
|
+
@system_property
|
|
190
|
+
def Lx(self) -> float:
|
|
191
|
+
return self.node_e.x - self.node_s.x
|
|
192
|
+
|
|
193
|
+
@system_property
|
|
194
|
+
def Ly(self) -> float:
|
|
195
|
+
return self.node_e.y - self.node_s.y
|
|
196
|
+
|
|
197
|
+
@system_property
|
|
198
|
+
def Lz(self) -> float:
|
|
199
|
+
return self.node_e.z - self.node_s.z
|
|
200
|
+
|
|
201
|
+
@system_property
|
|
202
|
+
def Lhz(self) -> float:
|
|
203
|
+
""":returns: The length of pipe element in the XY plane"""
|
|
204
|
+
return numpy.sqrt(self.Lx**2.0 + self.Ly**2.0)
|
|
205
|
+
|
|
206
|
+
@system_property
|
|
207
|
+
def L(self) -> float:
|
|
208
|
+
""":returns: The absolute length of pipe element"""
|
|
209
|
+
return numpy.sqrt(self.Lx**2.0 + self.Ly**2.0 + self.Lz**2.0)
|
|
210
|
+
|
|
211
|
+
@system_property
|
|
212
|
+
def inclination(self) -> float:
|
|
213
|
+
""":returns: the inclination angle in degrees"""
|
|
214
|
+
return numpy.rad2deg(numpy.arctan2(self.Lz, self.Lhz))
|
|
215
|
+
|
|
216
|
+
@system_property
|
|
217
|
+
def friction_factor(self) -> float:
|
|
218
|
+
"""The friction factor considering bend radius"""
|
|
219
|
+
if self.bend_radius is None:
|
|
220
|
+
re = self.reynoldsNumber
|
|
221
|
+
return fluids.friction.friction_factor(
|
|
222
|
+
re, self.roughness, Method=self.straight_method
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
re = self.reynoldsNumber
|
|
226
|
+
dc = self.D * self.bend_radius
|
|
227
|
+
return fluids.friction.friction_factor_curved(
|
|
228
|
+
re,
|
|
229
|
+
self.D,
|
|
230
|
+
dc,
|
|
231
|
+
self.roughness,
|
|
232
|
+
laminar_method=self.laminar_method,
|
|
233
|
+
turbulent_method=self.turbulent_method,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@system_property
|
|
237
|
+
def Kpipe(self) -> float:
|
|
238
|
+
"""The loss coeffient of this pipe section"""
|
|
239
|
+
return self.friction_factor * self.L / self.D
|
|
240
|
+
|
|
241
|
+
@system_property
|
|
242
|
+
def dP_f(self) -> float:
|
|
243
|
+
"""The loss of pressure in the pipe due to pressure"""
|
|
244
|
+
return self.sign * self.density * self.v**2.0 * self.Kpipe / 2.0
|
|
245
|
+
|
|
246
|
+
@system_property
|
|
247
|
+
def dP_p(self) -> float:
|
|
248
|
+
"""The loss of pressure in the pipe due to potential"""
|
|
249
|
+
return self.density * self.Lz * 9.81
|
|
250
|
+
|
|
251
|
+
@system_property
|
|
252
|
+
def dP_tot(self) -> float:
|
|
253
|
+
return self.dP_f + self.dP_p
|
|
254
|
+
|
|
255
|
+
@system_property
|
|
256
|
+
def sign(self) -> int:
|
|
257
|
+
return numpy.sign(self.v)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Specalized Nodes
|
|
261
|
+
class FlowNode(PipeNode):
|
|
262
|
+
"""Base For Boundary Condition Nodes of"""
|
|
263
|
+
|
|
264
|
+
@system_property
|
|
265
|
+
def dP_f(self) -> float:
|
|
266
|
+
return 0.0
|
|
267
|
+
|
|
268
|
+
@system_property
|
|
269
|
+
def dP_p(self) -> float:
|
|
270
|
+
return 0.0
|
|
271
|
+
|
|
272
|
+
@system_property
|
|
273
|
+
def dP_tot(self) -> float:
|
|
274
|
+
return 0.0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@forge
|
|
278
|
+
class PipeFitting(FlowNode, PipeFlow):
|
|
279
|
+
Kfitting = attr.ib(default=0.1, type=float)
|
|
280
|
+
|
|
281
|
+
@system_property
|
|
282
|
+
def dP_f(self) -> float:
|
|
283
|
+
"""The loss of pressure in the pipe due to pressure"""
|
|
284
|
+
return self.density * self.v**2.0 * self.Kfitting / 2.0
|
|
285
|
+
|
|
286
|
+
@system_property
|
|
287
|
+
def dP_p(self) -> float:
|
|
288
|
+
"""The loss of pressure in the pipe due to potential"""
|
|
289
|
+
return 0.0
|
|
290
|
+
|
|
291
|
+
@system_property
|
|
292
|
+
def dP_tot(self) -> float:
|
|
293
|
+
return self.dP_f + self.dP_p
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# TODO: Add in fitting numbers:
|
|
297
|
+
# https://neutrium.net/fluid-flow/pressure-loss-from-fittings-excess-head-k-method/
|
|
298
|
+
# https://neutrium.net/fluid-flow/discharge-coefficient-for-nozzles-and-orifices/
|
|
299
|
+
""" Fitting Types K
|
|
300
|
+
0 45° Elbow Standard (R/D = 1) 0.35
|
|
301
|
+
1 45° Elbow Long Radius (R/D = 1.5) 0.20
|
|
302
|
+
2 90° Elbow Curved Standard (R/D = 1) 0.75
|
|
303
|
+
3 90° Elbow Curved Long Radius (R/D = 1.5) 0.45
|
|
304
|
+
4 90° Elbow Square or Mitred NaN 1.30
|
|
305
|
+
5 180° Bend Close Return 1.50
|
|
306
|
+
6 Tee, Run Through Branch Blanked 0.40
|
|
307
|
+
7 Tee, as Elbow Entering in run 1.00
|
|
308
|
+
8 Tee, as Elbow Entering in branch 1.00
|
|
309
|
+
9 Tee, Branching Flow NaN 1.00
|
|
310
|
+
10 Coupling NaN 0.04
|
|
311
|
+
11 Union NaN 0.04
|
|
312
|
+
12 Gate valve Fully Open 0.17
|
|
313
|
+
13 Gate valve 3/4 Open 0.90
|
|
314
|
+
14 Gate valve 1/2 Open 4.50
|
|
315
|
+
15 Gate valve 1/4 Open 24.00
|
|
316
|
+
16 Diaphragm valve Fully Open 2.30
|
|
317
|
+
17 Diaphragm valve 3/4 Open 2.60
|
|
318
|
+
18 Diaphragm valve 1/2 Open 4.30
|
|
319
|
+
19 Diaphragm valve 1/4 Open 21.00
|
|
320
|
+
20 Globe valve, Bevel Seat Fully Open 6.00
|
|
321
|
+
21 Globe valve, Bevel Seat 1/2 Open 9.50
|
|
322
|
+
22 Globe Valve, Composition seat Fully Open 6.00
|
|
323
|
+
23 Globe Valve, Composition seat 1/2 Open 8.50
|
|
324
|
+
24 Plug disk Fully Open 9.00
|
|
325
|
+
25 Plug disk 3/4 Open 13.00
|
|
326
|
+
26 Plug disk 1/2 Open 36.00
|
|
327
|
+
27 Plug disk 1/4 Open 112.00
|
|
328
|
+
28 Angle valve Fully Open 2.00
|
|
329
|
+
29 Y valve or blowoff valve Fully Open 3.00
|
|
330
|
+
30 Plug cock \theta = 5° 0.05
|
|
331
|
+
31 Plug cock \theta = 10° 0.29
|
|
332
|
+
32 Plug cock \theta = 20° 1.56
|
|
333
|
+
33 Plug cock \theta = 40° 17.30
|
|
334
|
+
34 Plug cock \theta = 60° 206.00
|
|
335
|
+
35 Butterfly valve \theta = 5° 0.24
|
|
336
|
+
36 Butterfly valve \theta = 10° 0.52
|
|
337
|
+
37 Butterfly valve \theta = 20° 1.54
|
|
338
|
+
38 Butterfly valve \theta = 40° 10.80
|
|
339
|
+
39 Butterfly valve \theta = 60° 118.00
|
|
340
|
+
40 Check valve Swing 2.00
|
|
341
|
+
41 Check valve Disk 10.00
|
|
342
|
+
42 Check valve Ball 70.00
|
|
343
|
+
43 Foot valve NaN 15.00
|
|
344
|
+
44 Water meter Disk 7.00
|
|
345
|
+
45 Water meter Piston 15.00
|
|
346
|
+
46 Water meter Rotary (star-shaped disk) 10.00
|
|
347
|
+
47 Water meter Turbine-wheel 6.00"""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
#
|
|
351
|
+
@forge
|
|
352
|
+
class FlowInput(FlowNode):
|
|
353
|
+
flow_in: float = attrs.field(default=0.0)
|
|
354
|
+
|
|
355
|
+
@system_property
|
|
356
|
+
def sum_of_flows(self) -> float:
|
|
357
|
+
out = self.flow_in
|
|
358
|
+
for pipe_seg in self._segments:
|
|
359
|
+
if self is pipe_seg.node_s:
|
|
360
|
+
out -= pipe_seg.Q
|
|
361
|
+
elif self is pipe_seg.node_e:
|
|
362
|
+
out += pipe_seg.Q
|
|
363
|
+
return out
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
#
|
|
367
|
+
# class PressureInput(FlowNode):
|
|
368
|
+
# pressure_in: float = attrs.field()
|
|
369
|
+
#
|
|
370
|
+
# class PressureOut(FlowNode):
|
|
371
|
+
# pressure_out: float = attrs.field()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@forge
|
|
375
|
+
class Pump(Component):
|
|
376
|
+
"""Simulates a pump with power input, max flow, and max pressure by assuming a flow characteristic"""
|
|
377
|
+
|
|
378
|
+
max_flow: float = attrs.field()
|
|
379
|
+
max_pressure: float = attrs.field()
|
|
380
|
+
# throttle: float
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def design_flow_curve(self):
|
|
384
|
+
""":returns: a tuple output of flow vector, and pressure vector"""
|
|
385
|
+
flow = numpy.linspace(0, self.max_flow)
|
|
386
|
+
return flow, self.max_pressure * (1 - (flow / self.max_flow) ** 2.0)
|
|
387
|
+
|
|
388
|
+
def dPressure(self, current_flow):
|
|
389
|
+
"""The pressure the pump generates"""
|
|
390
|
+
flow, dP = self.design_flow_curve
|
|
391
|
+
assert current_flow >= 0, "Flow must be positive"
|
|
392
|
+
assert current_flow <= self.max_flow, "Flow must be less than max flow"
|
|
393
|
+
return numpy.interp(current_flow, flow, dP)
|
|
394
|
+
|
|
395
|
+
def power(self, current_flow):
|
|
396
|
+
"""The power used considering in watts"""
|
|
397
|
+
return self.dPressure(current_flow) * current_flow
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@forge
|
|
401
|
+
class PipeSystem(System):
|
|
402
|
+
in_node = Slot.define(PipeNode)
|
|
403
|
+
graph: nx.Graph
|
|
404
|
+
items: dict
|
|
405
|
+
|
|
406
|
+
def __on_init__(self):
|
|
407
|
+
self.items = {}
|
|
408
|
+
self.flow_solvers = {}
|
|
409
|
+
self.pipe_flow = {}
|
|
410
|
+
self.create_graph_from_pipe_or_node(self.in_node)
|
|
411
|
+
self.assemble_solvers()
|
|
412
|
+
|
|
413
|
+
def assemble_solvers(self):
|
|
414
|
+
for i, cycle in enumerate(nx.cycle_basis(self.graph)):
|
|
415
|
+
cycle_attr_name = f"_cycle_redisual_{i}"
|
|
416
|
+
pipes = []
|
|
417
|
+
self.info(f"found cycle: {cycle}")
|
|
418
|
+
for cs, cl in zip(cycle, cycle[1:] + [cycle[0]]):
|
|
419
|
+
pipe = self.graph.get_edge_data(cs, cl)["pipe"]
|
|
420
|
+
mult = 1
|
|
421
|
+
if cs == pipe.node_e.system_id:
|
|
422
|
+
# reverse
|
|
423
|
+
mult = -1
|
|
424
|
+
pipes.append((pipe, mult))
|
|
425
|
+
|
|
426
|
+
def res_func():
|
|
427
|
+
out = 0
|
|
428
|
+
for pipe, mult in pipes:
|
|
429
|
+
out += mult * pipe.dP_tot
|
|
430
|
+
return out / 1000.0
|
|
431
|
+
|
|
432
|
+
setattr(self, cycle_attr_name, res_func)
|
|
433
|
+
self.flow_solvers[cycle_attr_name] = Ref(self, cycle_attr_name)
|
|
434
|
+
|
|
435
|
+
bf = lambda kv: len(kv[1].segments)
|
|
436
|
+
for nid, node in sorted(self.nodes.items(), key=bf):
|
|
437
|
+
if len(node.segments) > 1:
|
|
438
|
+
self.flow_solvers[nid] = Ref(node, "sum_of_flows")
|
|
439
|
+
elif not isinstance(node, FlowInput):
|
|
440
|
+
self.info(f"deadend: {node.identity}")
|
|
441
|
+
# self.flow_solvers[nid] = Ref(node,'sum_of_flows')
|
|
442
|
+
|
|
443
|
+
for nid, node in self.nodes.items():
|
|
444
|
+
if isinstance(node, FlowInput):
|
|
445
|
+
self.flow_solvers[nid] = Ref(node, "sum_of_flows")
|
|
446
|
+
|
|
447
|
+
for pid, pipe in self.pipes.items():
|
|
448
|
+
self.pipe_flow[pid] = Ref(pipe, "v")
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def _X(self):
|
|
452
|
+
return {k: v for k, v in self.pipe_flow.items()}
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def _F(self):
|
|
456
|
+
return {k: v for k, v in self.flow_solvers.items()}
|
|
457
|
+
|
|
458
|
+
@instance_cached
|
|
459
|
+
def F_keyword_order(self):
|
|
460
|
+
"""defines the order of inputs in ordered mode for calcF"""
|
|
461
|
+
return {i: k for i, k in enumerate(self.flow_solvers)}
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def nodes(self):
|
|
465
|
+
return {k: v for k, v in self.items.items() if isinstance(v, PipeNode)}
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def pipes(self):
|
|
469
|
+
return {k: v for k, v in self.items.items() if isinstance(v, Pipe)}
|
|
470
|
+
|
|
471
|
+
# Pipe System Composition
|
|
472
|
+
def add_to_graph(self, graph, node_or_pipe) -> str:
|
|
473
|
+
"""recursively add node or pipe elements"""
|
|
474
|
+
|
|
475
|
+
idd = node_or_pipe.system_id
|
|
476
|
+
|
|
477
|
+
# Early Exit
|
|
478
|
+
if idd in self.items:
|
|
479
|
+
return idd
|
|
480
|
+
|
|
481
|
+
elif graph.has_node(idd):
|
|
482
|
+
return idd
|
|
483
|
+
|
|
484
|
+
log.info(f"adding {idd}")
|
|
485
|
+
|
|
486
|
+
if isinstance(node_or_pipe, PipeNode):
|
|
487
|
+
if not graph.has_node(idd):
|
|
488
|
+
graph.add_node(idd, sys_id=idd, node=node_or_pipe)
|
|
489
|
+
self.items[idd] = node_or_pipe
|
|
490
|
+
|
|
491
|
+
for seg in node_or_pipe.segments:
|
|
492
|
+
self.add_to_graph(graph, seg)
|
|
493
|
+
|
|
494
|
+
elif isinstance(node_or_pipe, Pipe):
|
|
495
|
+
nodes = node_or_pipe.node_s
|
|
496
|
+
nodee = node_or_pipe.node_e
|
|
497
|
+
|
|
498
|
+
if not graph.has_node(nodes.system_id):
|
|
499
|
+
nsid = self.add_to_graph(graph, nodes)
|
|
500
|
+
else:
|
|
501
|
+
nsid = nodes.system_id
|
|
502
|
+
|
|
503
|
+
if not graph.has_node(nodee.system_id):
|
|
504
|
+
neid = self.add_to_graph(graph, nodee)
|
|
505
|
+
else:
|
|
506
|
+
neid = nodee.system_id
|
|
507
|
+
|
|
508
|
+
if not graph.has_edge(nsid, neid):
|
|
509
|
+
graph.add_edge(nsid, neid, sys_id=idd, pipe=node_or_pipe)
|
|
510
|
+
self.items[idd] = node_or_pipe
|
|
511
|
+
|
|
512
|
+
else:
|
|
513
|
+
raise ValueError(f"not a node or pipe: {node_or_pipe}")
|
|
514
|
+
|
|
515
|
+
return idd
|
|
516
|
+
|
|
517
|
+
def create_graph_from_pipe_or_node(self, node_or_pipe) -> nx.Graph:
|
|
518
|
+
"""Creates a networkx graph from a pipe or node"""
|
|
519
|
+
|
|
520
|
+
self.graph = nx.Graph()
|
|
521
|
+
|
|
522
|
+
if isinstance(node_or_pipe, PipeNode):
|
|
523
|
+
self.add_to_graph(self.graph, node_or_pipe)
|
|
524
|
+
|
|
525
|
+
elif isinstance(node_or_pipe, Pipe):
|
|
526
|
+
self.add_to_graph(self.graph, node_or_pipe)
|
|
527
|
+
|
|
528
|
+
else:
|
|
529
|
+
raise ValueError(f"not a node or pipe: {node_or_pipe}")
|
|
530
|
+
|
|
531
|
+
return self.graph
|
|
532
|
+
|
|
533
|
+
def draw(self):
|
|
534
|
+
try:
|
|
535
|
+
from pyvis.network import Network
|
|
536
|
+
|
|
537
|
+
net = Network(directed=False)
|
|
538
|
+
|
|
539
|
+
net.from_nx(self.graph)
|
|
540
|
+
net.show("process_graph.html")
|
|
541
|
+
|
|
542
|
+
except:
|
|
543
|
+
pos = pos = nx.spring_layout(self.graph)
|
|
544
|
+
nx.draw(self.graph, pos=pos)
|
|
545
|
+
labels = nx.draw_networkx_labels(self.graph, pos=pos)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# TODO: update compressibility:
|
|
549
|
+
#
|
|
550
|
+
# import control as ct
|
|
551
|
+
# import numpy as np
|
|
552
|
+
# from engforge.components import Component, forge
|
|
553
|
+
# from matplotlib.pylab import *
|
|
554
|
+
#
|
|
555
|
+
# DT_DFLT = 1E-3
|
|
556
|
+
# @forge(auto_attribs=True)
|
|
557
|
+
# class Accumulator(Component):
|
|
558
|
+
#
|
|
559
|
+
# Rt: float = 5000
|
|
560
|
+
# Pref: float = 1E6
|
|
561
|
+
# Lref: float = 5
|
|
562
|
+
# Aact: float = 5
|
|
563
|
+
# Cact: float = 0.1
|
|
564
|
+
# Mact: float = 0.1
|
|
565
|
+
# Pmax: float = 7E8
|
|
566
|
+
# tlast: float = 0
|
|
567
|
+
# Kwall: float = 1
|
|
568
|
+
#
|
|
569
|
+
# lim_marg: float = 0.01
|
|
570
|
+
# min_dt: float = DT_DFLT
|
|
571
|
+
#
|
|
572
|
+
# model: 'InputOutputSystem' = None
|
|
573
|
+
#
|
|
574
|
+
# discrete: bool = True
|
|
575
|
+
# pressure_mode: bool = False #false is flow based
|
|
576
|
+
#
|
|
577
|
+
# def __on_init__(self):
|
|
578
|
+
# #handle defined pressure mode
|
|
579
|
+
# if self.pressure_mode:
|
|
580
|
+
# if not self.discrete:
|
|
581
|
+
# self.model = ct.NonlinearIOSystem(self.pr_update_accumulator,self.pr_accumulator_output,inputs=('Pi',),outputs=('Pc','Qc','Pout'),states=('x','v'),name= self.name if self.name else 'accumul ator')
|
|
582
|
+
# else:
|
|
583
|
+
# self.model = ct.NonlinearIOSystem(self.pr_discrete_update_accumulator,self.pr_accumulator_output,inputs=('Pi',),outputs=('Pc','Qc','Pout'),states=('x','v'),name= self.name if self.name else 'accumulator',dt=True)
|
|
584
|
+
# else:
|
|
585
|
+
# if not self.discrete:
|
|
586
|
+
# self.model = ct.NonlinearIOSystem(self.w_update_accumulator,self.w_output,inputs=('Qc',),outputs=('Pc','Pout'),states=('x'),name= self.name if self.name else 'accumulator')
|
|
587
|
+
# else:
|
|
588
|
+
# self.model = ct.NonlinearIOSystem(self.w_discrete_update_accumulator, self.w_output,inputs=('Qc',),outputs=('Pc','Pout'),states=('x'),name= self.name if self.name else 'accumulator',dt=True)
|
|
589
|
+
#
|
|
590
|
+
# def w_update_accumulator(self,t,x,u,params):
|
|
591
|
+
# Rt = params.get('Rt',self.Rt) #Pa/(m3/s)
|
|
592
|
+
# Pref = params.get('Pref',self.Pref)
|
|
593
|
+
# Lref = params.get('Lref',self.Lref)
|
|
594
|
+
# Aact = params.get('Aact',self.Aact)
|
|
595
|
+
# Cact = params.get('Cact',self.Cact)
|
|
596
|
+
# Mact = params.get('Mact',self.Mact)
|
|
597
|
+
# Kwall = params.get('Kwall',self.Kwall)
|
|
598
|
+
# Pmax = params.get('Pmax',self.Pmax)
|
|
599
|
+
#
|
|
600
|
+
# qin = u[0]
|
|
601
|
+
#
|
|
602
|
+
# xact = x[0]
|
|
603
|
+
# #Pi = x[0]
|
|
604
|
+
#
|
|
605
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
606
|
+
# self.dt = dt = self.model.dt
|
|
607
|
+
#
|
|
608
|
+
# else: #infer dt
|
|
609
|
+
# if t >= self.tlast+self.min_dt:
|
|
610
|
+
# self.dt = dt = max(abs(t - self.tlast),self.min_dt)
|
|
611
|
+
# self.tlast = t
|
|
612
|
+
# else:
|
|
613
|
+
# self.dt = dt = getattr(self,'dt',self.min_dt)
|
|
614
|
+
#
|
|
615
|
+
# #determine rate of x change (positive compresses accumulator (x->0))
|
|
616
|
+
# xdot = -qin/Aact
|
|
617
|
+
#
|
|
618
|
+
# xmin = Pref*Lref/Pmax
|
|
619
|
+
#
|
|
620
|
+
# #limit xdot to prevent over/under shoot
|
|
621
|
+
# if xact + dt * xdot > Lref*(1-self.lim_marg):
|
|
622
|
+
# xdot = (Lref-xact)/dt
|
|
623
|
+
# elif xact + dt * xdot < xmin:
|
|
624
|
+
# xdot = (xmin-xact)/dt
|
|
625
|
+
#
|
|
626
|
+
# return xdot
|
|
627
|
+
#
|
|
628
|
+
# def w_discrete_update_accumulator(self,t,x,u,params):
|
|
629
|
+
# v = self.w_update_accumulator(t,x,u,params)
|
|
630
|
+
# dt = self.model.dt
|
|
631
|
+
#
|
|
632
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
633
|
+
# dt = self.model.dt
|
|
634
|
+
# else:
|
|
635
|
+
# dt = self.dt
|
|
636
|
+
#
|
|
637
|
+
# xnew= x[0] + v*dt
|
|
638
|
+
#
|
|
639
|
+
# return xnew
|
|
640
|
+
#
|
|
641
|
+
# def w_output(self,t,x,u,params):
|
|
642
|
+
# Rt = params.get('Rt',self.Rt) #Pa/(m3/s)
|
|
643
|
+
# Pref = params.get('Pref',self.Pref)
|
|
644
|
+
# Lref = params.get('Lref',self.Lref)
|
|
645
|
+
# Aact = params.get('Aact',self.Aact)
|
|
646
|
+
# Cact = params.get('Cact',self.Cact)
|
|
647
|
+
# Mact = params.get('Mact',self.Mact)
|
|
648
|
+
# Kwall = params.get('Kwall',self.Kwall)
|
|
649
|
+
# Pmax = params.get('Pmax',self.Pmax)
|
|
650
|
+
#
|
|
651
|
+
# qin = u[0]
|
|
652
|
+
#
|
|
653
|
+
# xact = x[0]
|
|
654
|
+
#
|
|
655
|
+
# xmin = Pref*Lref/Pmax
|
|
656
|
+
#
|
|
657
|
+
# Pcomp = Pref*Lref / max(xmin,xact)
|
|
658
|
+
#
|
|
659
|
+
# #junction pressure is higher than pcomp when accumulator is compressed
|
|
660
|
+
# Pi = Pcomp + qin*Rt
|
|
661
|
+
#
|
|
662
|
+
# return [Pcomp,Pi]
|
|
663
|
+
#
|
|
664
|
+
#
|
|
665
|
+
#
|
|
666
|
+
#
|
|
667
|
+
# def pr_update_accumulator(self,t,x,u,params):
|
|
668
|
+
# """takes the current accumulator position, external pressure, and accumulator constants to determine the rate of change of accumulator position"""
|
|
669
|
+
# Rt = params.get('Rt',self.Rt) #Pa/(m3/s)
|
|
670
|
+
# Pref = params.get('Pref',self.Pref)
|
|
671
|
+
# Lref = params.get('Lref',self.Lref)
|
|
672
|
+
# Aact = params.get('Aact',self.Aact)
|
|
673
|
+
# Cact = params.get('Cact',self.Cact)
|
|
674
|
+
# Mact = params.get('Mact',self.Mact)
|
|
675
|
+
# Kwall = params.get('Kwall',self.Kwall)
|
|
676
|
+
# Pmax = params.get('Pmax',self.Pmax)
|
|
677
|
+
#
|
|
678
|
+
# Pi = u[0]
|
|
679
|
+
# #print(x,u)
|
|
680
|
+
# xact = x[0]
|
|
681
|
+
# v = x[1]
|
|
682
|
+
#
|
|
683
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
684
|
+
# self.dt = dt = self.model.dt
|
|
685
|
+
#
|
|
686
|
+
# else: #infer dt
|
|
687
|
+
# if t >= self.tlast+self.min_dt:
|
|
688
|
+
# self.dt = dt = max(abs(t - self.tlast),self.min_dt)
|
|
689
|
+
# self.tlast = t
|
|
690
|
+
# else:
|
|
691
|
+
# self.dt = dt = getattr(self,'dt',self.min_dt)
|
|
692
|
+
#
|
|
693
|
+
# xmin = Pref*Lref/Pmax
|
|
694
|
+
#
|
|
695
|
+
# Pcomp = Pref*Lref / max(max(xmin,xact),Lref)
|
|
696
|
+
# dP = Pcomp-Pi
|
|
697
|
+
# sgn = np.sign(dP)
|
|
698
|
+
#
|
|
699
|
+
# #FIXME: remove since dP is based on equillibritum
|
|
700
|
+
# #xeq = max(min(Pref*Lref/Pi,Lref),xmin)
|
|
701
|
+
# #K = 0*self.Kwall*(xeq-xact)/Lref
|
|
702
|
+
# K = 0
|
|
703
|
+
#
|
|
704
|
+
# xdot = sgn * ( abs(dP) / ( Rt * Aact**2.))**0.5/Lref
|
|
705
|
+
# dvdt = ((xdot-v) - K)/Mact - Cact*Aact*v*abs(v)/Mact
|
|
706
|
+
#
|
|
707
|
+
# x_proj = v * dt + xact
|
|
708
|
+
#
|
|
709
|
+
# #Consider projected limits
|
|
710
|
+
# if x_proj >= Lref*(1-self.lim_marg):
|
|
711
|
+
# vin = v
|
|
712
|
+
# v = (Lref-xact)/dt#,-1*Lref/10/dt)
|
|
713
|
+
# dvdt = (v-vin)/dt
|
|
714
|
+
#
|
|
715
|
+
# elif x_proj <= xmin:
|
|
716
|
+
# vin = v
|
|
717
|
+
# v = (0 - xact)/dt
|
|
718
|
+
# dvdt = (v-vin)/dt
|
|
719
|
+
#
|
|
720
|
+
# return v,dvdt
|
|
721
|
+
#
|
|
722
|
+
# def pr_discrete_update_accumulator(self,t,x,u,params):
|
|
723
|
+
# v,dvdt = self.pr_update_accumulator(t,x,u,params)
|
|
724
|
+
# dt = self.model.dt
|
|
725
|
+
#
|
|
726
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
727
|
+
# dt = self.model.dt
|
|
728
|
+
# else:
|
|
729
|
+
# dt = self.dt
|
|
730
|
+
#
|
|
731
|
+
# xnew= x[0] + v*dt
|
|
732
|
+
# vnew = x[1] + dvdt*dt
|
|
733
|
+
#
|
|
734
|
+
# return xnew,vnew
|
|
735
|
+
#
|
|
736
|
+
# def pr_accumulator_output(self,t,x,u,params):
|
|
737
|
+
# """returns accumulator pressure and flow (in positive)"""
|
|
738
|
+
# Rt = params.get('Rt',self.Rt) #Pa/(m3/s)
|
|
739
|
+
# Pref = params.get('Pref',self.Pref)
|
|
740
|
+
# Lref = params.get('Lref',self.Lref)
|
|
741
|
+
# Aact = params.get('Aact',self.Aact)
|
|
742
|
+
# Cact = params.get('Cact',self.Cact)
|
|
743
|
+
# Mact = params.get('Mact',self.Mact)
|
|
744
|
+
# Pmax = params.get('Pmax',self.Pmax)
|
|
745
|
+
# #dt = params.get('dt',self.dt)
|
|
746
|
+
#
|
|
747
|
+
# Pi = u[0]
|
|
748
|
+
# xact = x[0]
|
|
749
|
+
# v = x[1]
|
|
750
|
+
#
|
|
751
|
+
# xmin = Pref*Lref/Pmax
|
|
752
|
+
# Pcomp = Pref*Lref / max(xmin,xact)
|
|
753
|
+
# Q = v*Aact
|
|
754
|
+
# return [Pcomp,Q,Pi]
|
|
755
|
+
#
|
|
756
|
+
# @forge(auto_attribs=True)
|
|
757
|
+
# class Motor(Component):
|
|
758
|
+
# """Nonlinear motor with parasitics with fixed displacement"""
|
|
759
|
+
#
|
|
760
|
+
# I: float = 1
|
|
761
|
+
# D: float = 1E-6
|
|
762
|
+
# Cd: float = 0.0
|
|
763
|
+
# Ckloss: float = 0
|
|
764
|
+
# f: float = 0.0
|
|
765
|
+
# N: float = 1000
|
|
766
|
+
#
|
|
767
|
+
# min_dt: float = DT_DFLT
|
|
768
|
+
# model: 'InputOutputSystem' = None
|
|
769
|
+
# discrete: bool = True
|
|
770
|
+
# tlast = 0
|
|
771
|
+
#
|
|
772
|
+
# def __on_init__(self):
|
|
773
|
+
# #print(self.name)
|
|
774
|
+
# if not self.discrete:
|
|
775
|
+
# self.model = ct.NonlinearIOSystem(self.update_motor, self.motor_output, inputs=('P1','P2'), outputs=('Trq','Q','Pwr','Pin','Pout'), state=('w',), name=self.name if self.name else 'motor')
|
|
776
|
+
# else:
|
|
777
|
+
# self.model = ct.NonlinearIOSystem(self.discrete_update_motor, self.motor_output, inputs=('P1','P2'), outputs=('Trq','Q','Pwr','Pin','Pout'), state=('w',), name=self.name if self.name else 'motor',dt=True)
|
|
778
|
+
# self.tlast = 0
|
|
779
|
+
#
|
|
780
|
+
# def update_motor(self,t,x,u,params):
|
|
781
|
+
# """updates the motor acceleration based on pressure differential and nonlinear parasitics"""
|
|
782
|
+
# I = params.get('I',self.I)
|
|
783
|
+
# D = params.get('D',self.D)
|
|
784
|
+
# Cd = params.get('Cd',self.Cd)
|
|
785
|
+
# Ckloss = params.get('Ckloss',self.Ckloss)
|
|
786
|
+
# f = params.get('f',self.f)
|
|
787
|
+
# N = params.get('N',self.N)
|
|
788
|
+
#
|
|
789
|
+
# P1 = u[0]
|
|
790
|
+
# P2 = u[1]
|
|
791
|
+
# w = x[0]
|
|
792
|
+
#
|
|
793
|
+
# Q = D*(w/(2*3.14159))
|
|
794
|
+
# dPq = np.sign(Q)*Ckloss*Q**2
|
|
795
|
+
#
|
|
796
|
+
# dw_torque = (D/(2*np.pi))* ((P1 - P2)-dPq)
|
|
797
|
+
# dw_friction = w*f*N
|
|
798
|
+
# dw_drag = w*abs(w)*Cd
|
|
799
|
+
#
|
|
800
|
+
# return (dw_torque - dw_friction - dw_drag)/I
|
|
801
|
+
#
|
|
802
|
+
# def discrete_update_motor(self,t,x,u,params):
|
|
803
|
+
# dw = self.update_motor(t,x,u,params)
|
|
804
|
+
#
|
|
805
|
+
#
|
|
806
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
807
|
+
# self.dt = dt = self.model.dt
|
|
808
|
+
#
|
|
809
|
+
# else: #infer dt
|
|
810
|
+
# if t >= self.tlast+self.min_dt:
|
|
811
|
+
# self.dt = dt = max(abs(t - self.tlast),self.min_dt)
|
|
812
|
+
# self.tlast = t
|
|
813
|
+
# else:
|
|
814
|
+
# self.dt = dt = getattr(self,'dt',self.min_dt)
|
|
815
|
+
#
|
|
816
|
+
# return x[0] + dw*self.dt
|
|
817
|
+
#
|
|
818
|
+
# def motor_output(self,t,x,u,params):
|
|
819
|
+
# """calculates torque, flow and power output"""
|
|
820
|
+
# I = params.get('I',self.I)
|
|
821
|
+
# D = params.get('D',self.D)
|
|
822
|
+
# Cd = params.get('Cd',self.Cd)
|
|
823
|
+
# Ckloss = params.get('Ckloss',self.Ckloss)
|
|
824
|
+
# f = params.get('f',self.f)
|
|
825
|
+
# N = params.get('N',self.N)
|
|
826
|
+
#
|
|
827
|
+
# P1 = u[0]
|
|
828
|
+
# P2 = u[1]
|
|
829
|
+
# w = x[0]
|
|
830
|
+
#
|
|
831
|
+
# Q = (w/2*3.14159)*D
|
|
832
|
+
# dPq = np.sign(Q)*Ckloss*Q**2
|
|
833
|
+
#
|
|
834
|
+
# dP = (P1 - P2) - dPq
|
|
835
|
+
#
|
|
836
|
+
# Trq = D * dP / (2*3.14159)
|
|
837
|
+
# Pwr = dP * Q
|
|
838
|
+
# return [Trq, Q, Pwr,P1,P2]
|
|
839
|
+
#
|
|
840
|
+
#
|
|
841
|
+
# @forge(auto_attribs=True)
|
|
842
|
+
# class Actuator(Component):
|
|
843
|
+
# Ap: float = 1
|
|
844
|
+
# mp: float = 0.5
|
|
845
|
+
# bp: float = 0.1
|
|
846
|
+
# Kwall: float = 1000
|
|
847
|
+
# Lstroke: float = 1
|
|
848
|
+
#
|
|
849
|
+
# min_dt = DT_DFLT
|
|
850
|
+
# lim_marg = 0.01
|
|
851
|
+
#
|
|
852
|
+
# lim = None # true if high, false if low and none otherwise
|
|
853
|
+
#
|
|
854
|
+
# model: 'InterConnectedSystem' = None
|
|
855
|
+
#
|
|
856
|
+
# discrete: bool = True
|
|
857
|
+
#
|
|
858
|
+
# tlast = 0
|
|
859
|
+
#
|
|
860
|
+
# def __on_init__(self):
|
|
861
|
+
# if self.discrete:
|
|
862
|
+
# self.model = ct.NonlinearIOSystem(self.discrete_update_fnc,self.output_fnc,state=('x','v'),outputs=('Q1','Q2','P1','P2'),inputs=('F','P1','P2'),dt=True,name=self.name if self.name else 'actuator')
|
|
863
|
+
#
|
|
864
|
+
# else:
|
|
865
|
+
# self.model = ct.NonlinearIOSystem(self.update_fnc,self.output_fnc,state=('x','v'),outputs=('Q1','Q2','P1','P2'),inputs=('F','P1','P2'),name=self.name if self.name else 'actuator')
|
|
866
|
+
#
|
|
867
|
+
#
|
|
868
|
+
#
|
|
869
|
+
# def update_fnc(self,t,x,u,params):
|
|
870
|
+
# Ap = params.get('Ap',self.Ap)
|
|
871
|
+
# mp = params.get('mp',self.mp)
|
|
872
|
+
# bp = params.get('bp',self.bp)
|
|
873
|
+
# Kwall = params.get('Kwall',self.Kwall)
|
|
874
|
+
# Lstroke = params.get('Lstroke',self.Lstroke)
|
|
875
|
+
#
|
|
876
|
+
# xpos = x[0]
|
|
877
|
+
# v = x[1]
|
|
878
|
+
#
|
|
879
|
+
# f = u[0]
|
|
880
|
+
# p1 = u[1]
|
|
881
|
+
# p2 = u[2]
|
|
882
|
+
#
|
|
883
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
884
|
+
# dt = self.model.dt
|
|
885
|
+
# self.dt = dt
|
|
886
|
+
#
|
|
887
|
+
# else: #infer dt
|
|
888
|
+
# if t >= self.tlast+self.min_dt:
|
|
889
|
+
# self.dt = dt = max(abs(t - self.tlast),self.min_dt)
|
|
890
|
+
# self.tlast = t
|
|
891
|
+
# else:
|
|
892
|
+
# self.dt = dt = getattr(self,'dt',self.min_dt)
|
|
893
|
+
#
|
|
894
|
+
# if xpos > Lstroke:
|
|
895
|
+
# #print('kpos')
|
|
896
|
+
# K = self.Kwall*(xpos-Lstroke)
|
|
897
|
+
# elif xpos < 0:
|
|
898
|
+
# #print('kneg')
|
|
899
|
+
# K = self.Kwall*(xpos - 0)
|
|
900
|
+
# else:
|
|
901
|
+
# K = 0
|
|
902
|
+
#
|
|
903
|
+
# daf = f - (p1-p2)*Ap
|
|
904
|
+
# dad = bp*v*abs(v)
|
|
905
|
+
# dvdt = (daf - dad - K*daf)/mp
|
|
906
|
+
#
|
|
907
|
+
# x_proj = v * dt + xpos
|
|
908
|
+
#
|
|
909
|
+
# if x_proj >= Lstroke*(1-self.lim_marg):
|
|
910
|
+
# vin = v
|
|
911
|
+
# v = (Lstroke-xpos)/dt
|
|
912
|
+
# dvdt = min((v-vin)/dt,dvdt)
|
|
913
|
+
#
|
|
914
|
+
# elif x_proj <= self.lim_marg:
|
|
915
|
+
# vin = v
|
|
916
|
+
# v = (0 - xpos)/dt
|
|
917
|
+
# dvdt = max((v-vin)/dt,dvdt)
|
|
918
|
+
#
|
|
919
|
+
#
|
|
920
|
+
# return [v,dvdt]
|
|
921
|
+
#
|
|
922
|
+
# def discrete_update_fnc(self,t,x,u,params):
|
|
923
|
+
# v,dvdt = self.update_fnc(t,x,u,params)
|
|
924
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
925
|
+
# dt = self.model.dt
|
|
926
|
+
# else:
|
|
927
|
+
# dt = self.dt
|
|
928
|
+
#
|
|
929
|
+
# if hasattr(self,'last_a'):
|
|
930
|
+
# dvdt = (self.last_a + dvdt)/2
|
|
931
|
+
# v = (self.last_v + v)/2
|
|
932
|
+
#
|
|
933
|
+
# vnew = v + dvdt*dt
|
|
934
|
+
# newx = x[0] + v*dt
|
|
935
|
+
#
|
|
936
|
+
# if newx < 0:
|
|
937
|
+
# newx = 0
|
|
938
|
+
# if vnew < 0:
|
|
939
|
+
# vnew = 0
|
|
940
|
+
#
|
|
941
|
+
# elif newx > self.Lstroke:
|
|
942
|
+
# newx = self.Lstroke
|
|
943
|
+
# if vnew > 0:
|
|
944
|
+
# vnew = 0
|
|
945
|
+
#
|
|
946
|
+
# self.last_x = newx
|
|
947
|
+
# self.last_v = vnew
|
|
948
|
+
# self.last_a = dvdt
|
|
949
|
+
#
|
|
950
|
+
# return newx, vnew
|
|
951
|
+
#
|
|
952
|
+
# def output_fnc(self,t,x,u,params):
|
|
953
|
+
# Ap = params.get('Ap',self.Ap)
|
|
954
|
+
#
|
|
955
|
+
# v = x[1]
|
|
956
|
+
#
|
|
957
|
+
# Q1 = -1*v * Ap
|
|
958
|
+
# Q2 = v * Ap
|
|
959
|
+
#
|
|
960
|
+
# return [Q1,Q2,u[1],u[2]]
|
|
961
|
+
#
|
|
962
|
+
#
|
|
963
|
+
# @forge(auto_attribs=True)
|
|
964
|
+
# class Pipe(Component):
|
|
965
|
+
# '''models pressure change and flow Q from nodes with pressure P1->P2'''
|
|
966
|
+
# Kp:float = 1 #TODO: better friction model
|
|
967
|
+
#
|
|
968
|
+
# flow_input: bool = False
|
|
969
|
+
# model: 'InputOutputSystem' = None
|
|
970
|
+
#
|
|
971
|
+
# def __on_init__(self):
|
|
972
|
+
# if not self.flow_input:
|
|
973
|
+
# self.model = ct.NonlinearIOSystem(self.pipe_flow,self.pipe_flow_output,state=('Q'),inputs=('P1','P2'),outputs=('P1o','P2o','Q'),dt=True,name=self.name if self.name else 'pipe')
|
|
974
|
+
# else:
|
|
975
|
+
# self.model = ct.NonlinearIOSystem(self.pipe_pressure,self.pipe_pressure_output,state=('P2'),inputs=('P1','Q'),outputs=('P1o','P2','Q'),dt=True,name=self.name if self.name else 'pipe')
|
|
976
|
+
#
|
|
977
|
+
# def pipe_flow(self,t,x,u,params):
|
|
978
|
+
# Kp = params.get('Kp',self.Kp)
|
|
979
|
+
# p1 = u[0]
|
|
980
|
+
# p2 = u[1]
|
|
981
|
+
# dP = (p2-p1)
|
|
982
|
+
# return -1*np.sign(dP)*(abs(dP)/Kp)**0.5
|
|
983
|
+
#
|
|
984
|
+
# def pipe_pressure(self,t,x,u,params):
|
|
985
|
+
# Kp = params.get('Kp',self.Kp)
|
|
986
|
+
# p1 = u[0]
|
|
987
|
+
# q = u[1]
|
|
988
|
+
# return -1*np.sign(q)*Kp*q**2 + p1
|
|
989
|
+
#
|
|
990
|
+
# def pipe_flow_output(self,t,x,u,params):
|
|
991
|
+
# return [u[0],u[1],x[0]]
|
|
992
|
+
#
|
|
993
|
+
# def pipe_pressure_output(self,t,x,u,params):
|
|
994
|
+
# return [u[0],x[0],u[1]]
|
|
995
|
+
#
|
|
996
|
+
#
|
|
997
|
+
# @forge(auto_attribs=True)
|
|
998
|
+
# class Valve(Component):
|
|
999
|
+
# Ao: float = 0.1 #area
|
|
1000
|
+
# ts: float = 30/1000. #seconds
|
|
1001
|
+
# rho: float = 1000
|
|
1002
|
+
# Cd: float = 0.1
|
|
1003
|
+
# discrete_control: bool = True # contorl signal is zero or one, vs a goal
|
|
1004
|
+
#
|
|
1005
|
+
# tlast = 0
|
|
1006
|
+
# min_dt:float = 1E-3
|
|
1007
|
+
#
|
|
1008
|
+
# def update_alpha(self,t,x,u,params):
|
|
1009
|
+
# ts = params.get('ts',self.ts)
|
|
1010
|
+
# uctl = u[0]
|
|
1011
|
+
# alpha = x[0]
|
|
1012
|
+
#
|
|
1013
|
+
# if t >= self.tlast+self.min_dt:
|
|
1014
|
+
# self.dt = dt = max(abs(t - self.tlast),self.min_dt)
|
|
1015
|
+
# self.tlast = t
|
|
1016
|
+
# else:
|
|
1017
|
+
# self.dt = dt = getattr(self,'dt',self.min_dt)
|
|
1018
|
+
#
|
|
1019
|
+
# dA = 1.0 / ts
|
|
1020
|
+
#
|
|
1021
|
+
# if self.discrete_control:
|
|
1022
|
+
# if uctl==1:
|
|
1023
|
+
# if alpha < 1-dA*dt:
|
|
1024
|
+
# return dA
|
|
1025
|
+
# else:
|
|
1026
|
+
# return 1-alpha
|
|
1027
|
+
# elif uctl==0:
|
|
1028
|
+
# if alpha > dA*dt:
|
|
1029
|
+
# return dA
|
|
1030
|
+
# else:
|
|
1031
|
+
# return 0-alpha
|
|
1032
|
+
# else:
|
|
1033
|
+
# vctl = min(max(uctl,0),1)
|
|
1034
|
+
# d = (uctl-alpha)/dt
|
|
1035
|
+
# dv = max(min(d,dA),-dA)
|
|
1036
|
+
# return dv
|
|
1037
|
+
#
|
|
1038
|
+
#
|
|
1039
|
+
# def get_flow(self,t,x,u,params):
|
|
1040
|
+
# rho = params.get('rho',self.rho)
|
|
1041
|
+
# ts = params.get('ts',self.ts)
|
|
1042
|
+
# Cd = params.get('Cd',self.Cd)
|
|
1043
|
+
# Ao = params.get('Ao',self.Ao)
|
|
1044
|
+
#
|
|
1045
|
+
# alpha = x[0]
|
|
1046
|
+
# if alpha < 0.05:
|
|
1047
|
+
# return 0.0 #avoid divide by zero in area
|
|
1048
|
+
#
|
|
1049
|
+
# dP = u[1]
|
|
1050
|
+
# Av = Ao*min(max(alpha,0),1)
|
|
1051
|
+
# return np.sign(dP)*Cd*Av*((2/rho)*abs(dP))**0.5
|
|
1052
|
+
#
|
|
1053
|
+
# @forge(auto_attribs=True)
|
|
1054
|
+
# class PipeJunction(Component):
|
|
1055
|
+
#
|
|
1056
|
+
# n_pipes: int = 2
|
|
1057
|
+
# Vi: float = 1
|
|
1058
|
+
# B: float = 300E3
|
|
1059
|
+
#
|
|
1060
|
+
# min_dt = DT_DFLT
|
|
1061
|
+
# model: 'InputOutputSystem' = None
|
|
1062
|
+
# discrete:bool = True
|
|
1063
|
+
#
|
|
1064
|
+
# def __on_init__(self):
|
|
1065
|
+
# state = ('P',)
|
|
1066
|
+
# inputs = tuple(f'Q{i+1}' for i in range(self.n_pipes))
|
|
1067
|
+
# outins = tuple(f'Qout{i+1}' for i in range(self.n_pipes))
|
|
1068
|
+
# outputs = tuple(list(state)+list(outins)+['dQ'])
|
|
1069
|
+
# if self.discrete:
|
|
1070
|
+
# self.model = ct.NonlinearIOSystem(self.discrete_pressure_update,self.output,inputs=inputs,outputs=outputs,state=state,dt=True,name=self.name if self.name else 'junction')
|
|
1071
|
+
# self.tlast = 0
|
|
1072
|
+
# else:
|
|
1073
|
+
# self.model = ct.NonlinearIOSystem(self.pressure_update,self.output,inputs=inputs,outputs=outputs,state=state,name=self.name if self.name else 'junction')
|
|
1074
|
+
#
|
|
1075
|
+
# def pressure_update(self,t,x,u,params):
|
|
1076
|
+
# #TODO: update compressibility from temp / pressure ect.
|
|
1077
|
+
# B = params.get('B',self.B)
|
|
1078
|
+
# Vi = params.get('Vi',self.Vi)
|
|
1079
|
+
# sumQ = sum(u)
|
|
1080
|
+
# dPdt = (B/Vi)*sumQ
|
|
1081
|
+
# return dPdt
|
|
1082
|
+
#
|
|
1083
|
+
# def discrete_pressure_update(self,t,x,u,params):
|
|
1084
|
+
# dPdt = self.pressure_update(t,x,u,params)
|
|
1085
|
+
# if self.discrete and isinstance(self.model.dt,float):
|
|
1086
|
+
# dt = self.model.dt
|
|
1087
|
+
# self.dt = dt
|
|
1088
|
+
#
|
|
1089
|
+
# else: #infer dt
|
|
1090
|
+
# if t >= self.tlast+self.min_dt:
|
|
1091
|
+
# self.dt = dt = max(abs(t - self.tlast),self.min_dt)
|
|
1092
|
+
# self.tlast = t
|
|
1093
|
+
# else:
|
|
1094
|
+
# self.dt = dt = getattr(self,'dt',self.min_dt)
|
|
1095
|
+
# return self.dt*dPdt + x[0]
|
|
1096
|
+
#
|
|
1097
|
+
# def output(self,t,x,u,parms):
|
|
1098
|
+
# return tuple([x[0]]+list(u)+[sum(u)])
|
|
1099
|
+
#
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
if __name__ == "__main__":
|
|
1103
|
+
N = 10
|
|
1104
|
+
|
|
1105
|
+
rho = 1000.0
|
|
1106
|
+
f = 0.015
|
|
1107
|
+
L = 1.0
|
|
1108
|
+
Po = 1e5
|
|
1109
|
+
|
|
1110
|
+
A = 1.0 * ones(N)
|
|
1111
|
+
u = 0.0 * ones(N)
|
|
1112
|
+
p = Po * ones(N)
|
|
1113
|
+
|
|
1114
|
+
Pin = 1.1e5
|
|
1115
|
+
Uin = lambda Ps: sqrt((Pin - Ps) / (rho * 2.0))
|
|
1116
|
+
|
|
1117
|
+
def F(i):
|
|
1118
|
+
ui = u[i]
|
|
1119
|
+
ps = p[i]
|
|
1120
|
+
F1 = rho * ui
|
|
1121
|
+
F2 = F1 * ui + ps
|
|
1122
|
+
return F1, F2
|
|
1123
|
+
|
|
1124
|
+
def J(i):
|
|
1125
|
+
return 0.0, -rho * f
|
|
1126
|
+
|
|
1127
|
+
def decodeF(F1, F2):
|
|
1128
|
+
up = F1 / rho
|
|
1129
|
+
pp = F2 - rho * up**2.0
|
|
1130
|
+
return up, pp
|
|
1131
|
+
|
|
1132
|
+
plast = p.copy()
|
|
1133
|
+
ulast = u.copy()
|
|
1134
|
+
it = 0
|
|
1135
|
+
started = False
|
|
1136
|
+
while not started or (plast - p).mean() > 1e-3 or (ulast - u).mean() > 1e-3:
|
|
1137
|
+
started = True
|
|
1138
|
+
|
|
1139
|
+
plast = p.copy()
|
|
1140
|
+
ulast = u.copy()
|
|
1141
|
+
|
|
1142
|
+
for i in range(N):
|
|
1143
|
+
if i == 0: # inlet
|
|
1144
|
+
ps = p[i]
|
|
1145
|
+
ui = Uin(ps)
|
|
1146
|
+
u[i] = ui
|
|
1147
|
+
|
|
1148
|
+
# STATUS
|
|
1149
|
+
F1, F2 = F(i)
|
|
1150
|
+
J1, J2 = J(i)
|
|
1151
|
+
|
|
1152
|
+
# PREDICT i+1
|
|
1153
|
+
dF1, dF2 = J1, J2
|
|
1154
|
+
Fp1, Fp2 = F1 + dF1 * L, F2 + dF2 * L
|
|
1155
|
+
|
|
1156
|
+
up1, pp1 = decodeF(Fp1, Fp2)
|
|
1157
|
+
|
|
1158
|
+
# CORRECTOR i+1
|
|
1159
|
+
Jp1, Jp2 = 0.0, -rho * f
|
|
1160
|
+
dpF1, dpF2 = Jp1, Jp2
|
|
1161
|
+
|
|
1162
|
+
# avgGradients
|
|
1163
|
+
dF1n, dF2n = 0.5 * (dpF1 + dF1), 0.5 * (dpF2 + dF2)
|
|
1164
|
+
|
|
1165
|
+
# Calculate i+1 actual
|
|
1166
|
+
Fn1, Fn2 = F1 + dF1n * L, F2 + dF2n * L
|
|
1167
|
+
|
|
1168
|
+
un, pn = decodeF(Fn1, Fn2)
|
|
1169
|
+
if N - 1 > i:
|
|
1170
|
+
u[i + 1] = un
|
|
1171
|
+
p[i + 1] = pn
|
|
1172
|
+
|
|
1173
|
+
for i in reversed(range(N)):
|
|
1174
|
+
# if i == 0: #inlet
|
|
1175
|
+
# ps = p[i]
|
|
1176
|
+
# ui = Uin(ps)
|
|
1177
|
+
# u[i] = ui
|
|
1178
|
+
|
|
1179
|
+
if i == N - 1:
|
|
1180
|
+
p[i] = Po
|
|
1181
|
+
|
|
1182
|
+
# STATUS
|
|
1183
|
+
F1, F2 = F(i)
|
|
1184
|
+
J1, J2 = J(i)
|
|
1185
|
+
|
|
1186
|
+
# PREDICT i+1
|
|
1187
|
+
dF1, dF2 = J1, J2
|
|
1188
|
+
Fp1, Fp2 = F1 + dF1 * L, F2 + dF2 * L
|
|
1189
|
+
|
|
1190
|
+
up1, pp1 = decodeF(Fp1, Fp2)
|
|
1191
|
+
|
|
1192
|
+
# CORRECTOR i+1
|
|
1193
|
+
Jp1, Jp2 = 0.0, -rho * f
|
|
1194
|
+
dpF1, dpF2 = Jp1, Jp2
|
|
1195
|
+
|
|
1196
|
+
# avgGradients
|
|
1197
|
+
dF1n, dF2n = 0.5 * (dpF1 + dF1), 0.5 * (dpF2 + dF2)
|
|
1198
|
+
|
|
1199
|
+
# Calculate i+1 actual
|
|
1200
|
+
Fn1, Fn2 = F1 + dF1n * L, F2 + dF2n * L
|
|
1201
|
+
|
|
1202
|
+
un, pn = decodeF(Fn1, Fn2)
|
|
1203
|
+
if N - 1 > i:
|
|
1204
|
+
u[i - 1] = un
|
|
1205
|
+
p[i - 1] = pn
|
|
1206
|
+
|
|
1207
|
+
print(u)
|
|
1208
|
+
print(p)
|
|
1209
|
+
it += 1
|
|
1210
|
+
if it >= 2:
|
|
1211
|
+
break
|