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
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
from engforge.system import System
|
|
2
|
+
from engforge.configuration import Configuration
|
|
3
|
+
from engforge.components import Component, forge, SolveableInterface
|
|
4
|
+
from engforge.solveable import SolveableMixin
|
|
5
|
+
from engforge.attr_signals import Signal
|
|
6
|
+
from engforge.attr_slots import Slot
|
|
7
|
+
from engforge.attr_dynamics import Time
|
|
8
|
+
from engforge.attr_signals import Signal
|
|
9
|
+
from engforge.attr_solver import Solver
|
|
10
|
+
from engforge.properties import system_property
|
|
11
|
+
from engforge.dynamics import DynamicsMixin, GlobalDynamics
|
|
12
|
+
from engforge.logging import LoggingMixin
|
|
13
|
+
from engforge.analysis import Analysis
|
|
14
|
+
|
|
15
|
+
import unittest
|
|
16
|
+
from engforge.configuration import forge
|
|
17
|
+
from engforge import field
|
|
18
|
+
from engforge.eng.costs import CostModel, Economics, cost_property
|
|
19
|
+
from engforge.attr_slots import Slot
|
|
20
|
+
from engforge.system import System
|
|
21
|
+
from engforge.components import Component
|
|
22
|
+
from engforge.component_collections import ComponentIterator
|
|
23
|
+
from engforge.attr_plotting import *
|
|
24
|
+
|
|
25
|
+
from engforge.eng.solid_materials import *
|
|
26
|
+
|
|
27
|
+
from engforge.properties import system_property
|
|
28
|
+
|
|
29
|
+
import attrs
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestCompLog(LoggingMixin):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
log = TestCompLog()
|
|
38
|
+
|
|
39
|
+
Fun_size = lambda sys, *a, **kw: sys.volume
|
|
40
|
+
Fun_eff = lambda sys, *a, **kw: sys.cost / sys.volume
|
|
41
|
+
Fun_minz = lambda sys, *a, **kw: max(sys.x, sys.y)
|
|
42
|
+
Fun_em = lambda sys, *a, **kw: sys.max_length - sys.combine_length
|
|
43
|
+
Fun_cm = lambda sys, *a, **kw: sys.budget - sys.cost
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def wrap_f(function):
|
|
47
|
+
def wrapper(*args, **kwargs):
|
|
48
|
+
# log.info(f'calling {function.__name__}| {args} | {kwargs}')
|
|
49
|
+
return function(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
cm = -0.01
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@forge(auto_attribs=True)
|
|
58
|
+
class SpaceMixin(SolveableInterface):
|
|
59
|
+
x: float = attrs.field(default=1.0)
|
|
60
|
+
y: float = attrs.field(default=1.0)
|
|
61
|
+
z: float = attrs.field(default=1.0)
|
|
62
|
+
|
|
63
|
+
base_cost: float = 0
|
|
64
|
+
cost_x: float = 10
|
|
65
|
+
cost_y: float = 10
|
|
66
|
+
cost_z: float = 10
|
|
67
|
+
|
|
68
|
+
budget: float = 100
|
|
69
|
+
max_length: float = 50
|
|
70
|
+
volume_goal: float = 10
|
|
71
|
+
|
|
72
|
+
static_A = np.array([[cm, 1, -1], [-1, cm, 1], [1, -1, cm]])
|
|
73
|
+
|
|
74
|
+
# size constraints
|
|
75
|
+
Xvar = Solver.declare_var("x", combos="x", active=True)
|
|
76
|
+
Yvar = Solver.declare_var("y", combos="y", active=True)
|
|
77
|
+
Zvar = Solver.declare_var("z", combos="z", active=True)
|
|
78
|
+
Xvar.add_var_constraint(0.1, kind="min", combos=["min_len"])
|
|
79
|
+
Yvar.add_var_constraint(0.1, kind="min", combos=["min_len"])
|
|
80
|
+
Zvar.add_var_constraint(0.1, kind="min", combos=["min_len"])
|
|
81
|
+
# Zvar.add_var_constraint(Fun_minz, kind="min",combos=['len_fun_z'])
|
|
82
|
+
|
|
83
|
+
# Constraints by function
|
|
84
|
+
sym = Solver.constraint_equality("x", "y", combos="sym", active=False)
|
|
85
|
+
|
|
86
|
+
costA = Solver.con_ineq("budget", "cost", combos="ineq_cost", active=False)
|
|
87
|
+
costF = Solver.con_ineq(wrap_f(Fun_cm), combos="ineq_cost", active=False)
|
|
88
|
+
costP = Solver.con_ineq("cost_margin", combos="ineq_cost", active=False)
|
|
89
|
+
|
|
90
|
+
# Constraints by length
|
|
91
|
+
lenA = Solver.con_ineq(
|
|
92
|
+
"max_length", "combine_length", combos="ineq_length", active=False
|
|
93
|
+
)
|
|
94
|
+
lenF = Solver.con_ineq(wrap_f(Fun_em), combos="ineq_length", active=False)
|
|
95
|
+
lenP = Solver.con_ineq("edge_margin", combos="ineq_length", active=False)
|
|
96
|
+
|
|
97
|
+
# Objectives
|
|
98
|
+
size_goal = Solver.objective("goal", combos="prop_goal", kind="min", active=False)
|
|
99
|
+
|
|
100
|
+
size = Solver.objective("volume", combos="obj_size", kind="max", active=False)
|
|
101
|
+
sizeF = Solver.objective(
|
|
102
|
+
wrap_f(Fun_size), combos="obj_size", kind="max", active=False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
eff = Solver.objective("cost_to_volume", combos="obj_eff", kind="min", active=False)
|
|
106
|
+
effF = Solver.objective(wrap_f(Fun_eff), combos="obj_eff", kind="min", active=False)
|
|
107
|
+
|
|
108
|
+
@system_property
|
|
109
|
+
def combine_length(self) -> float:
|
|
110
|
+
return (abs(self.x) + abs(self.y) + abs(self.z)) * 4
|
|
111
|
+
|
|
112
|
+
@system_property
|
|
113
|
+
def cost_margin(self) -> float:
|
|
114
|
+
dv = self.budget - self.cost
|
|
115
|
+
# print(f'cost_margin: {dv}')
|
|
116
|
+
return dv
|
|
117
|
+
|
|
118
|
+
@system_property
|
|
119
|
+
def edge_margin(self) -> float:
|
|
120
|
+
dv = self.max_length - self.combine_length
|
|
121
|
+
# print(f'edge_margin: {dv}')
|
|
122
|
+
return dv
|
|
123
|
+
|
|
124
|
+
@system_property
|
|
125
|
+
def goal(self) -> float:
|
|
126
|
+
dv = self.volume_goal - self.volume
|
|
127
|
+
return (dv) ** 2
|
|
128
|
+
|
|
129
|
+
@system_property
|
|
130
|
+
def volume(self) -> float:
|
|
131
|
+
return abs(self.x * self.y * self.z)
|
|
132
|
+
|
|
133
|
+
@system_property
|
|
134
|
+
def cost(self) -> float:
|
|
135
|
+
edge_cost = abs(self.x) * 4 * self.cost_x
|
|
136
|
+
edge_cost = edge_cost + abs(self.y) * 4 * self.cost_y
|
|
137
|
+
edge_cost = edge_cost + abs(self.z) * 4 * self.cost_z
|
|
138
|
+
return self.base_cost + edge_cost
|
|
139
|
+
|
|
140
|
+
@system_property
|
|
141
|
+
def cost_to_volume(self) -> float:
|
|
142
|
+
if self.volume == 0:
|
|
143
|
+
return 0
|
|
144
|
+
return self.cost / self.volume
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@forge(auto_attribs=True)
|
|
148
|
+
class CubeComp(Component, SpaceMixin):
|
|
149
|
+
# this should be energy neutral 🤞
|
|
150
|
+
|
|
151
|
+
dynamic_state_vars: list = ["x", "y", "z"]
|
|
152
|
+
|
|
153
|
+
base_cost: float = attrs.field(default=10)
|
|
154
|
+
cost_x: float = attrs.field(default=10)
|
|
155
|
+
cost_y: float = attrs.field(default=20)
|
|
156
|
+
cost_z: float = attrs.field(default=5)
|
|
157
|
+
|
|
158
|
+
budget: float = attrs.field(default=300)
|
|
159
|
+
max_length: float = attrs.field(default=100)
|
|
160
|
+
volume_goal: float = attrs.field(default=100)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@forge(auto_attribs=True)
|
|
164
|
+
class CubeSystem(System, SpaceMixin):
|
|
165
|
+
dynamic_state_vars: list = ["x", "y", "z"]
|
|
166
|
+
|
|
167
|
+
comp = Slot.define(CubeComp)
|
|
168
|
+
|
|
169
|
+
x: float = attrs.field(default=0.5)
|
|
170
|
+
y: float = attrs.field(default=0.5)
|
|
171
|
+
z: float = attrs.field(default=0.5)
|
|
172
|
+
|
|
173
|
+
cost_x: float = attrs.field(default=5)
|
|
174
|
+
cost_y: float = attrs.field(default=15)
|
|
175
|
+
cost_z: float = attrs.field(default=10)
|
|
176
|
+
|
|
177
|
+
goal_vol_frac: float = 0.5
|
|
178
|
+
|
|
179
|
+
sys_budget = Solver.con_ineq("total_budget", "system_cost", combos="total_budget")
|
|
180
|
+
|
|
181
|
+
sys_length = Solver.con_ineq("total_length", "system_length", combos="total_length")
|
|
182
|
+
|
|
183
|
+
volfrac = Solver.con_eq(
|
|
184
|
+
"goal_vol_frac", "vol_frac", combos="vol_frac_eq", active=False
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
obj = Solver.objective("total_volume", combos="volume", kind="max")
|
|
188
|
+
hght = Solver.objective("total_height", combos="height", kind="max")
|
|
189
|
+
|
|
190
|
+
x_lim = Solver.con_ineq("x", "comp.x", combos="tierd_top", active=False)
|
|
191
|
+
y_lim = Solver.con_ineq("y", "comp.y", combos="tierd_top", active=False)
|
|
192
|
+
|
|
193
|
+
sig_x_cst = Signal.define(
|
|
194
|
+
"comp.cost_x",
|
|
195
|
+
"cost_x",
|
|
196
|
+
mode="both",
|
|
197
|
+
combos="mirror_costs",
|
|
198
|
+
active=False,
|
|
199
|
+
)
|
|
200
|
+
sig_y_cst = Signal.define(
|
|
201
|
+
"comp.cost_y",
|
|
202
|
+
"cost_y",
|
|
203
|
+
mode="both",
|
|
204
|
+
combos="mirror_costs",
|
|
205
|
+
active=False,
|
|
206
|
+
)
|
|
207
|
+
sig_z_cst = Signal.define(
|
|
208
|
+
"comp.cost_z",
|
|
209
|
+
"cost_z",
|
|
210
|
+
mode="both",
|
|
211
|
+
combos="mirror_costs",
|
|
212
|
+
active=False,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@system_property
|
|
216
|
+
def total_budget(self) -> float:
|
|
217
|
+
return self.budget + self.comp.budget
|
|
218
|
+
|
|
219
|
+
@system_property
|
|
220
|
+
def total_length(self) -> float:
|
|
221
|
+
return self.max_length + self.comp.max_length
|
|
222
|
+
|
|
223
|
+
@system_property
|
|
224
|
+
def vol_frac(self) -> float:
|
|
225
|
+
cv = self.comp.volume
|
|
226
|
+
return cv / (self.volume + cv)
|
|
227
|
+
|
|
228
|
+
@system_property
|
|
229
|
+
def total_volume(self) -> float:
|
|
230
|
+
return self.volume + self.comp.volume
|
|
231
|
+
|
|
232
|
+
@system_property
|
|
233
|
+
def total_height(self) -> float:
|
|
234
|
+
return max(self.z, 0) + max(self.comp.z, 0)
|
|
235
|
+
|
|
236
|
+
@system_property
|
|
237
|
+
def system_cost(self) -> float:
|
|
238
|
+
return self.cost + self.comp.cost
|
|
239
|
+
|
|
240
|
+
@system_property
|
|
241
|
+
def system_length(self) -> float:
|
|
242
|
+
return self.combine_length + self.comp.combine_length
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@forge(auto_attribs=True)
|
|
246
|
+
class DynamicComponent(Component):
|
|
247
|
+
dynamic_state_vars: list = ["x", "v"]
|
|
248
|
+
x: float = 1
|
|
249
|
+
v: float = 0
|
|
250
|
+
|
|
251
|
+
b: float = 0.1
|
|
252
|
+
K: float = 10
|
|
253
|
+
M: float = 100
|
|
254
|
+
|
|
255
|
+
x0: float = 0.5
|
|
256
|
+
|
|
257
|
+
nonlinear: bool = False
|
|
258
|
+
Fext: float = 0
|
|
259
|
+
|
|
260
|
+
acc = Solver.declare_var("x0")
|
|
261
|
+
no_load = Solver.constraint_equality("a")
|
|
262
|
+
|
|
263
|
+
def create_state_matrix(self, **kwargs) -> np.ndarray:
|
|
264
|
+
"""creates the state matrix for the system"""
|
|
265
|
+
return np.array([[0, 1], [-self.K / self.M, -self.b / self.M]])
|
|
266
|
+
|
|
267
|
+
def create_state_constants(self, **kwargs) -> np.ndarray:
|
|
268
|
+
"""creates the input matrix for the system, called B"""
|
|
269
|
+
return np.array([0, self.K * self.x0 / self.M])
|
|
270
|
+
|
|
271
|
+
def update_state_constants(self, t, F, X) -> np.ndarray:
|
|
272
|
+
"""override"""
|
|
273
|
+
F = F.copy()
|
|
274
|
+
F[-1] = (self.K * self.x0 - self.Fext) / self.M
|
|
275
|
+
return F
|
|
276
|
+
|
|
277
|
+
@system_property
|
|
278
|
+
def a(self) -> float:
|
|
279
|
+
return (-self.v * self.b - (self.x - self.x0) * self.K) / self.M
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@forge(auto_attribs=True)
|
|
283
|
+
class TransientSys(Component):
|
|
284
|
+
x: float = 0
|
|
285
|
+
v: float = 0
|
|
286
|
+
a: float = 0
|
|
287
|
+
|
|
288
|
+
speed = Time.integrate("x", "v", mode="euler")
|
|
289
|
+
accel = Time.integrate("v", "a", mode="euler")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@forge(auto_attribs=True)
|
|
293
|
+
class DynamicSystem(System):
|
|
294
|
+
dynamic_state_vars: list = ["x", "v"]
|
|
295
|
+
|
|
296
|
+
x: float = 0
|
|
297
|
+
v: float = 0
|
|
298
|
+
a: float = 0
|
|
299
|
+
|
|
300
|
+
Force: float = 0.0
|
|
301
|
+
Damp: float = 10
|
|
302
|
+
Mass: float = 100.0
|
|
303
|
+
K: float = 20
|
|
304
|
+
|
|
305
|
+
Fref: float = 10
|
|
306
|
+
omega: float = 1
|
|
307
|
+
|
|
308
|
+
comp = Slot.define(DynamicComponent)
|
|
309
|
+
trns = Slot.define(TransientSys)
|
|
310
|
+
|
|
311
|
+
sig = Signal.define("trns.a", "spring_accel")
|
|
312
|
+
fig = Signal.define("comp.Fext", "Force")
|
|
313
|
+
slv = Solver.declare_var("Force")
|
|
314
|
+
# slv = Solver.declare_var("delta_a")
|
|
315
|
+
|
|
316
|
+
nonlinear: bool = True
|
|
317
|
+
|
|
318
|
+
@system_property
|
|
319
|
+
def spring_accel(self) -> float:
|
|
320
|
+
# print(self.comp.v,self.comp.x,self.comp.a)
|
|
321
|
+
return (
|
|
322
|
+
-self.comp.v * self.comp.b - (self.comp.x - self.comp.x0) * self.comp.K
|
|
323
|
+
) / self.comp.M
|
|
324
|
+
|
|
325
|
+
@system_property
|
|
326
|
+
def delta_a(self) -> float:
|
|
327
|
+
return (
|
|
328
|
+
self.Fref * np.cos(self.omega * self.time)
|
|
329
|
+
- self.Force
|
|
330
|
+
+ self.v * self.Damp
|
|
331
|
+
- self.K * self.x
|
|
332
|
+
) / self.Mass - self.spring_accel
|
|
333
|
+
|
|
334
|
+
def create_state_matrix(self, *args, **kwargs) -> np.ndarray:
|
|
335
|
+
"""creates the state matrix for the system"""
|
|
336
|
+
return np.array([[0, 1.0], [-self.K / self.Mass, -1 * self.Damp / self.Mass]])
|
|
337
|
+
|
|
338
|
+
def create_state_constants(self, *args, **kwargs) -> np.ndarray:
|
|
339
|
+
"""creates the input matrix for the system, called B"""
|
|
340
|
+
return np.array([0, self.Force / self.Mass])
|
|
341
|
+
|
|
342
|
+
def update_state(self, *args, **kwargs) -> np.ndarray:
|
|
343
|
+
"""creates the state matrix for the system"""
|
|
344
|
+
return np.array([[0, 1.0], [-self.K / self.Mass, -1 * self.Damp / self.Mass]])
|
|
345
|
+
|
|
346
|
+
def update_state_constants(self, *args, **kwargs) -> np.ndarray:
|
|
347
|
+
"""creates the input matrix for the system, called B"""
|
|
348
|
+
return np.array([0, self.Force / self.Mass])
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@forge
|
|
352
|
+
class SpringMass(System):
|
|
353
|
+
k: float = attrs.field(default=50)
|
|
354
|
+
m: float = attrs.field(default=1)
|
|
355
|
+
g: float = attrs.field(default=9.81)
|
|
356
|
+
u: float = attrs.field(default=0.3)
|
|
357
|
+
|
|
358
|
+
a: float = attrs.field(default=0)
|
|
359
|
+
x: float = attrs.field(default=0.0)
|
|
360
|
+
v: float = attrs.field(default=0.0)
|
|
361
|
+
|
|
362
|
+
wo_f: float = attrs.field(default=1.0)
|
|
363
|
+
Fa: float = attrs.field(default=10.0)
|
|
364
|
+
|
|
365
|
+
x_neutral: float = attrs.field(default=0.5)
|
|
366
|
+
|
|
367
|
+
res = Solver.constraint_equality("sumF")
|
|
368
|
+
var_a = Solver.declare_var("a", combos="a", active=False)
|
|
369
|
+
var_b = Solver.declare_var("u", combos="u", active=False)
|
|
370
|
+
var_b.add_var_constraint(0.0, kind="min")
|
|
371
|
+
var_b.add_var_constraint(1.0, kind="max")
|
|
372
|
+
|
|
373
|
+
vtx = Time.integrate("v", "accl")
|
|
374
|
+
xtx = Time.integrate("x", "v")
|
|
375
|
+
xtx.add_var_constraint(0, kind="min")
|
|
376
|
+
|
|
377
|
+
# FIXME: implement trace testing
|
|
378
|
+
pos = Trace.define(y="x", y2=["v", "a"])
|
|
379
|
+
|
|
380
|
+
@system_property
|
|
381
|
+
def dx(self) -> float:
|
|
382
|
+
return self.x_neutral - self.x
|
|
383
|
+
|
|
384
|
+
@system_property
|
|
385
|
+
def Fspring(self) -> float:
|
|
386
|
+
return self.k * self.dx
|
|
387
|
+
|
|
388
|
+
@system_property
|
|
389
|
+
def Fgrav(self) -> float:
|
|
390
|
+
return self.g * self.m
|
|
391
|
+
|
|
392
|
+
@system_property
|
|
393
|
+
def Faccel(self) -> float:
|
|
394
|
+
return self.a * self.m
|
|
395
|
+
|
|
396
|
+
@system_property
|
|
397
|
+
def Ffric(self) -> float:
|
|
398
|
+
return self.u * self.v
|
|
399
|
+
|
|
400
|
+
@system_property
|
|
401
|
+
def sumF(self) -> float:
|
|
402
|
+
return self.Fspring - self.Fgrav - self.Faccel - self.Ffric + self.Fext
|
|
403
|
+
|
|
404
|
+
@system_property
|
|
405
|
+
def Fext(self) -> float:
|
|
406
|
+
return self.Fa * np.cos(self.time * self.wo_f)
|
|
407
|
+
|
|
408
|
+
@system_property
|
|
409
|
+
def accl(self) -> float:
|
|
410
|
+
return self.sumF / self.m
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
##### Air Filter
|
|
414
|
+
@forge
|
|
415
|
+
class Fan(Component):
|
|
416
|
+
n_frac: float = field(default=1)
|
|
417
|
+
dp_design: float = field(default=100)
|
|
418
|
+
w_design: float = field(default=2)
|
|
419
|
+
|
|
420
|
+
@system_property
|
|
421
|
+
def dP_fan(self) -> float:
|
|
422
|
+
return self.dp_design * (self.n_frac * self.w_design) ** 2.0
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@forge
|
|
426
|
+
class Filter(Component):
|
|
427
|
+
w: float = field(default=0)
|
|
428
|
+
k_loss: float = field(default=50)
|
|
429
|
+
|
|
430
|
+
@system_property
|
|
431
|
+
def dP_filter(self) -> float:
|
|
432
|
+
return self.k_loss * self.w
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@forge
|
|
436
|
+
class Airfilter(System):
|
|
437
|
+
throttle: float = field(default=1)
|
|
438
|
+
w: float = field(default=1)
|
|
439
|
+
k_parasitic: float = field(default=0.1)
|
|
440
|
+
|
|
441
|
+
fan: Fan = Slot.define(Fan)
|
|
442
|
+
filt: Filter = Slot.define(Filter)
|
|
443
|
+
|
|
444
|
+
set_fan_n = Signal.define("fan.n_frac", "throttle", mode="both")
|
|
445
|
+
set_filter_w = Signal.define("filt.w", "w", mode="both")
|
|
446
|
+
|
|
447
|
+
flow_var = Solver.declare_var("w", combos="flow")
|
|
448
|
+
flow_var.add_var_constraint(0, "min", combos="flow")
|
|
449
|
+
|
|
450
|
+
pr_eq = Solver.constraint_equality("sum_dP", 0, combos="flow")
|
|
451
|
+
|
|
452
|
+
flow_curve = Plot.define("throttle", "w", kind="lineplot", title="Flow Curve")
|
|
453
|
+
|
|
454
|
+
@system_property
|
|
455
|
+
def dP_parasitic(self) -> float:
|
|
456
|
+
return self.k_parasitic * self.w**2.0
|
|
457
|
+
|
|
458
|
+
@system_property
|
|
459
|
+
def sum_dP(self) -> float:
|
|
460
|
+
return self.fan.dP_fan - self.dP_parasitic - self.filt.dP_filter
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@forge
|
|
464
|
+
class AirFilterAnalysis(Analysis):
|
|
465
|
+
efficiency: float = attrs.field(default=0.9)
|
|
466
|
+
system = Slot.define(Airfilter, default_ok=True)
|
|
467
|
+
|
|
468
|
+
@system_property
|
|
469
|
+
def cadr(self) -> float:
|
|
470
|
+
return self.system.w * self.efficiency
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
#### SLIDER CRANK
|
|
474
|
+
theta = np.concatenate((np.linspace(0, 2 * np.pi, 120), np.array([0])))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@forge(auto_attribs=True)
|
|
478
|
+
class SliderCrank(System, CostModel):
|
|
479
|
+
success_thresh = 1000
|
|
480
|
+
dynamic_state_vars: list = ["theta", "omega"]
|
|
481
|
+
dynamic_input_vars: list = ["Tg"]
|
|
482
|
+
|
|
483
|
+
# outer gear
|
|
484
|
+
Ro: float = 0.33
|
|
485
|
+
gear_thickness: float = 0.0127 # 1/2 in
|
|
486
|
+
gear_material: SolidMaterial = attrs.field(factory=ANSI_4130)
|
|
487
|
+
|
|
488
|
+
# motor gear
|
|
489
|
+
Tg: float = 0
|
|
490
|
+
Rg: float = 0.03
|
|
491
|
+
Tg_max: float = 10 # Nm
|
|
492
|
+
omg_g_max: float = 1500 * (2 * 3.14159 / 60) # 3000 rpm
|
|
493
|
+
|
|
494
|
+
# crank
|
|
495
|
+
Rc: float = 0.25
|
|
496
|
+
Lo: float = 0.3
|
|
497
|
+
Lo_factor: float = 2.0001
|
|
498
|
+
|
|
499
|
+
# rotation position
|
|
500
|
+
theta: float = 0.0
|
|
501
|
+
omega: float = 0.0
|
|
502
|
+
b_rot: float = 0.0
|
|
503
|
+
|
|
504
|
+
# pushrod & spring
|
|
505
|
+
k_spring: float = 0.0
|
|
506
|
+
bf_rod: float = 0.0
|
|
507
|
+
y_offset: float = 0
|
|
508
|
+
x_offset: float = 0
|
|
509
|
+
X_spring_center: float = 0
|
|
510
|
+
|
|
511
|
+
# signal targets or input
|
|
512
|
+
F_external_x: float = 0
|
|
513
|
+
F_external_y: float = 0
|
|
514
|
+
dX_actuator: float = 1
|
|
515
|
+
|
|
516
|
+
max_size: float = 0.6
|
|
517
|
+
|
|
518
|
+
# Radius Design Variables
|
|
519
|
+
rc_slv = Solver.declare_var("Rc", combos="design")
|
|
520
|
+
rc_slv.add_var_constraint(lambda s, p: s.Ro, "max", combos="max_crank")
|
|
521
|
+
rc_slv.add_var_constraint(0, "min")
|
|
522
|
+
|
|
523
|
+
ro_slv = Solver.declare_var("Ro", combos="design")
|
|
524
|
+
ro_slv.add_var_constraint(
|
|
525
|
+
lambda sys, prob: sys.max_size / 2, "max", combos="max_size"
|
|
526
|
+
)
|
|
527
|
+
ro_slv.add_var_constraint(0, "min")
|
|
528
|
+
|
|
529
|
+
rg_slv = Solver.declare_var("Rg", combos="design")
|
|
530
|
+
rg_slv.add_var_constraint(0.0125, "min")
|
|
531
|
+
|
|
532
|
+
rg_slv = Solver.declare_var("Lo", combos="design")
|
|
533
|
+
rg_slv.add_var_constraint(lambda s, p: s.Rc * s.Lo_factor, "min", combos="design")
|
|
534
|
+
|
|
535
|
+
offy_slv = Solver.declare_var("y_offset", combos="design")
|
|
536
|
+
offx_slv = Solver.declare_var("x_offset", combos="design")
|
|
537
|
+
spring_slv = Solver.declare_var("X_spring_center", combos="spring_sym")
|
|
538
|
+
spring_slv.add_var_constraint(lambda s, p: -s.max_dx_range / 4, "min")
|
|
539
|
+
spring_slv.add_var_constraint(lambda s, p: s.max_dx_range / 4, "max")
|
|
540
|
+
|
|
541
|
+
# objectives
|
|
542
|
+
goal_dx_range: float = 0.25
|
|
543
|
+
main_gear_speed_min: float = 20 * (2 * 3.14159 / 60)
|
|
544
|
+
main_gear_speed_max: float = 120 * (2 * 3.14159 / 60)
|
|
545
|
+
|
|
546
|
+
cost_slv = Solver.objective("combine_cost", kind="min", combos="cost")
|
|
547
|
+
sym_slv = Solver.objective("end_force_diff", combos="spring_sym")
|
|
548
|
+
|
|
549
|
+
# constraints
|
|
550
|
+
gear_speed_slv = Solver.con_eq("dx_goal", combos="speed_goal")
|
|
551
|
+
range_slv = Solver.con_eq("ds_goal", combos="range_goal")
|
|
552
|
+
|
|
553
|
+
gear_pos_slv = Solver.con_ineq("final_gear_ratio", combos="design,gear")
|
|
554
|
+
crank_pos_slv = Solver.con_ineq("crank_gear_ratio", combos="design,gear")
|
|
555
|
+
motor_pos_slv = Solver.con_ineq("motor_gear_ratio", combos="design,gear")
|
|
556
|
+
size = Solver.con_ineq(
|
|
557
|
+
lambda sys, prob: sys.max_size, "overall_length", combos="max_size"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Dynamics
|
|
561
|
+
nonlinear: bool = True # sinusoidal-isms
|
|
562
|
+
# TODO: v_pos = Transient.time_derivative('x_pos)
|
|
563
|
+
|
|
564
|
+
# Forces & Torques
|
|
565
|
+
@system_property
|
|
566
|
+
def input_torque(self) -> float:
|
|
567
|
+
return self.Tg * self.final_gear_ratio
|
|
568
|
+
|
|
569
|
+
@system_property
|
|
570
|
+
def overall_length(self) -> float:
|
|
571
|
+
return self.Lo + self.Rc * 2
|
|
572
|
+
|
|
573
|
+
@system_property
|
|
574
|
+
def end_force_diff(self) -> float:
|
|
575
|
+
x = self.motion_curve
|
|
576
|
+
return (
|
|
577
|
+
(x.max() - self.X_spring_center) - (x.min() - self.X_spring_center)
|
|
578
|
+
) ** 2
|
|
579
|
+
|
|
580
|
+
@system_property
|
|
581
|
+
def reaction_torque(self) -> float:
|
|
582
|
+
return self.Rc_x * self.Freact_y - self.Rc_y * self.Fslide_tot
|
|
583
|
+
|
|
584
|
+
@system_property
|
|
585
|
+
def Freact_y(self) -> float:
|
|
586
|
+
return (
|
|
587
|
+
np.sin(self.gamma) * np.cos(self.gamma) * self.Fslide_tot
|
|
588
|
+
+ self.F_external_y
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
@system_property
|
|
592
|
+
def Fslide_tot(self) -> float:
|
|
593
|
+
return self.Fslide_fric + self.Fslide_spring + self.F_external_x
|
|
594
|
+
|
|
595
|
+
@system_property
|
|
596
|
+
def Fslide_fric(self) -> float:
|
|
597
|
+
return -1 * self.bf_rod * self.v_pos
|
|
598
|
+
|
|
599
|
+
@system_property
|
|
600
|
+
def Fslide_spring(self) -> float:
|
|
601
|
+
return -1 * self.k_spring * (self.x_pos - self.X_spring_center)
|
|
602
|
+
|
|
603
|
+
# Positions & Angles
|
|
604
|
+
@system_property
|
|
605
|
+
def crank_angle(self) -> float:
|
|
606
|
+
"""angle between the crank and the vertical axis"""
|
|
607
|
+
return np.rad2deg(self.theta)
|
|
608
|
+
|
|
609
|
+
@system_property
|
|
610
|
+
def alpha(self) -> float:
|
|
611
|
+
"""angle between the crank and the horizontal axis"""
|
|
612
|
+
return self.theta - np.pi / 2
|
|
613
|
+
|
|
614
|
+
@system_property
|
|
615
|
+
def gamma(self) -> float:
|
|
616
|
+
"""angle between the rod and the horizontal axis"""
|
|
617
|
+
return np.arcsin(np.sin(self.alpha) * (self.Rc / self.Lo))
|
|
618
|
+
|
|
619
|
+
@solver_cached
|
|
620
|
+
def motion_curve(self) -> np.ndarray:
|
|
621
|
+
x = (
|
|
622
|
+
self.Rc * np.cos(theta)
|
|
623
|
+
+ (self.Lo**2 - (self.Rc * np.sin(theta) - self.y_offset) ** 2) ** 0.5
|
|
624
|
+
)
|
|
625
|
+
return x
|
|
626
|
+
|
|
627
|
+
@system_property
|
|
628
|
+
def max_dx_range(self) -> float:
|
|
629
|
+
x = self.motion_curve
|
|
630
|
+
return max(x.max() - x.min(), 0)
|
|
631
|
+
|
|
632
|
+
@system_property
|
|
633
|
+
def max_x_theta(self) -> float:
|
|
634
|
+
x = self.motion_curve
|
|
635
|
+
return theta[np.argmax(x)]
|
|
636
|
+
|
|
637
|
+
@system_property
|
|
638
|
+
def min_x_theta(self) -> float:
|
|
639
|
+
x = self.motion_curve
|
|
640
|
+
return theta[np.argmin(x)]
|
|
641
|
+
|
|
642
|
+
@system_property
|
|
643
|
+
def dx_goal(self) -> float:
|
|
644
|
+
return self.max_dx_range - self.goal_dx_range
|
|
645
|
+
|
|
646
|
+
@system_property
|
|
647
|
+
def Rc_x(self) -> float:
|
|
648
|
+
return self.Rc * np.cos(self.theta)
|
|
649
|
+
|
|
650
|
+
@system_property
|
|
651
|
+
def Rc_y(self) -> float:
|
|
652
|
+
return self.Rc * np.sin(self.theta)
|
|
653
|
+
|
|
654
|
+
@system_property
|
|
655
|
+
def x_pos(self) -> float:
|
|
656
|
+
return self.Rc_x + self.x_offset
|
|
657
|
+
|
|
658
|
+
@system_property
|
|
659
|
+
def y_pos(self) -> float:
|
|
660
|
+
return self.Rc_y - self.y_offset
|
|
661
|
+
|
|
662
|
+
@system_property
|
|
663
|
+
def v_pos(self) -> float:
|
|
664
|
+
# TODO:replace with v_pos = Transient.time_derivative('x_pos)
|
|
665
|
+
omega_rad = self.omega
|
|
666
|
+
th = self.theta
|
|
667
|
+
A = self.Rc * omega_rad
|
|
668
|
+
|
|
669
|
+
s = self.Rc * np.sin(th) - self.y_offset
|
|
670
|
+
N = s * np.cos(th)
|
|
671
|
+
D = (self.Lo**2 - s**2) ** 0.5
|
|
672
|
+
return A * ((N / D) - np.sin(th))
|
|
673
|
+
|
|
674
|
+
# Gear Properties
|
|
675
|
+
@system_property
|
|
676
|
+
def ds_goal(self) -> float:
|
|
677
|
+
dS = self.main_gear_speed_max - self.omg_g_max / self.motor_gear_ratio
|
|
678
|
+
return dS
|
|
679
|
+
|
|
680
|
+
@system_property
|
|
681
|
+
def mass_main_gear(self) -> float:
|
|
682
|
+
return np.pi * self.gear_material.density * self.Ro**2 * self.gear_thickness
|
|
683
|
+
|
|
684
|
+
@system_property
|
|
685
|
+
def Imain_gear(self) -> float:
|
|
686
|
+
return self.mass_main_gear * (self.Ro**2)
|
|
687
|
+
|
|
688
|
+
@system_property
|
|
689
|
+
def mass_pwr_gear(self) -> float:
|
|
690
|
+
return np.pi * self.gear_material.density * self.Rg**2 * self.gear_thickness
|
|
691
|
+
|
|
692
|
+
@system_property
|
|
693
|
+
def Ipwr_gear(self) -> float:
|
|
694
|
+
return self.mass_main_gear * (self.Rg**2)
|
|
695
|
+
|
|
696
|
+
@cost_property
|
|
697
|
+
def main_gear_cost(self) -> float:
|
|
698
|
+
return self.gear_material.cost_per_kg * self.mass_main_gear
|
|
699
|
+
|
|
700
|
+
@cost_property
|
|
701
|
+
def gear_cost(self) -> float:
|
|
702
|
+
return self.gear_material.cost_per_kg * self.mass_pwr_gear
|
|
703
|
+
|
|
704
|
+
@system_property
|
|
705
|
+
def motor_gear_ratio(self) -> float:
|
|
706
|
+
return self.Ro / self.Rg
|
|
707
|
+
|
|
708
|
+
@system_property
|
|
709
|
+
def crank_gear_ratio(self) -> float:
|
|
710
|
+
return self.Rc / self.Ro
|
|
711
|
+
|
|
712
|
+
@system_property
|
|
713
|
+
def final_gear_ratio(self) -> float:
|
|
714
|
+
return self.motor_gear_ratio * self.crank_gear_ratio
|
|
715
|
+
|
|
716
|
+
@system_property
|
|
717
|
+
def crank_rod_ratio(self) -> float:
|
|
718
|
+
return self.Lo / self.Rc
|
|
719
|
+
|
|
720
|
+
# Dynamics Matricies
|
|
721
|
+
def create_state_matrix(self, *args, **kw):
|
|
722
|
+
# TODO: add rotational-positional parasitics (if any)
|
|
723
|
+
|
|
724
|
+
return np.array([[0, 1], [0, -self.b_rot / self.Imain_gear]])
|
|
725
|
+
|
|
726
|
+
def create_input_matrix(self, **kwargs) -> np.ndarray:
|
|
727
|
+
return np.array([[0], [1 / self.Imain_gear]])
|
|
728
|
+
|
|
729
|
+
def create_state_constants(self, **kwargs) -> np.ndarray:
|
|
730
|
+
return np.array([0, 0])
|
|
731
|
+
|
|
732
|
+
def update_state(self, t, A, X):
|
|
733
|
+
return np.array([[0, 1], [0, -self.b_rot / self.Imain_gear]])
|
|
734
|
+
|
|
735
|
+
def update_state_constants(self, t, F, X) -> np.ndarray:
|
|
736
|
+
# TODO: add torque & force interactions
|
|
737
|
+
accl_input = self.input_torque / self.Imain_gear
|
|
738
|
+
accl_react = self.reaction_torque / self.Imain_gear
|
|
739
|
+
return np.array([0, accl_input + accl_react])
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
# COSTS Testing
|
|
743
|
+
|
|
744
|
+
# COSTS Testing
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
@forge
|
|
748
|
+
class Norm(Component, CostModel):
|
|
749
|
+
pass
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@forge
|
|
753
|
+
class Comp1(Component, CostModel):
|
|
754
|
+
norm = Slot.define(Norm, none_ok=True)
|
|
755
|
+
not_cost = Slot.define(Component)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
@forge
|
|
759
|
+
class Comp2(Norm, CostModel):
|
|
760
|
+
comp1 = Slot.define(Comp1, none_ok=True, default_ok=False)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
quarterly = lambda inst, term, econ: True if (term + 1) % 3 == 0 else False
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@forge
|
|
767
|
+
class TermCosts(Comp1, CostModel):
|
|
768
|
+
@cost_property(category="capex")
|
|
769
|
+
def cost_init(self):
|
|
770
|
+
return 100
|
|
771
|
+
|
|
772
|
+
@cost_property(mode="maintenance", category="opex")
|
|
773
|
+
def cost_maintenance(self):
|
|
774
|
+
return 10
|
|
775
|
+
|
|
776
|
+
@cost_property(mode="always", category="tax")
|
|
777
|
+
def cost_tax(self):
|
|
778
|
+
return 1
|
|
779
|
+
|
|
780
|
+
@cost_property(mode=quarterly, category="opex,tax", label="quarterly wage tax")
|
|
781
|
+
def cost_wage_tax(self):
|
|
782
|
+
return 5 * 3
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@forge
|
|
786
|
+
class EconDefault(System, CostModel):
|
|
787
|
+
econ = Slot.define(Economics)
|
|
788
|
+
comp = Slot.define(Component, none_ok=True)
|
|
789
|
+
comp1 = Slot.define(Comp1, none_ok=True)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@forge
|
|
793
|
+
class EconRecursive(System, CostModel):
|
|
794
|
+
econ = Slot.define(Economics)
|
|
795
|
+
comp1 = Slot.define(Comp1, none_ok=True)
|
|
796
|
+
comp2 = Slot.define(Comp2, none_ok=True)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
# FAN System test
|
|
800
|
+
@forge
|
|
801
|
+
class Fan(Component, CostModel):
|
|
802
|
+
"""a fan component"""
|
|
803
|
+
|
|
804
|
+
blade_cost_com: float = attrs.field(default=100.0)
|
|
805
|
+
area: float = attrs.field(default=10.0)
|
|
806
|
+
V: float = attrs.field(default=5.0)
|
|
807
|
+
|
|
808
|
+
@system_property
|
|
809
|
+
def volumetric_flow(self) -> float:
|
|
810
|
+
return self.V * self.area
|
|
811
|
+
|
|
812
|
+
@cost_property(category="capex,mfg,material", mode="initial")
|
|
813
|
+
def blade_cost(self):
|
|
814
|
+
return self.area * self.blade_cost_com
|
|
815
|
+
|
|
816
|
+
@cost_property(category="labor,opex", mode="maintenance")
|
|
817
|
+
def repair_cost(self):
|
|
818
|
+
return self.volumetric_flow * 0.1
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
@forge
|
|
822
|
+
class Motor(Component, CostModel):
|
|
823
|
+
"""a fan component"""
|
|
824
|
+
|
|
825
|
+
spc_motor_cost: float = attrs.field(default=100.0)
|
|
826
|
+
|
|
827
|
+
@system_property
|
|
828
|
+
def power(self) -> float:
|
|
829
|
+
return self.parent.fan.volumetric_flow * self.parent.fan.V
|
|
830
|
+
|
|
831
|
+
@cost_property(category="capex,mfg,electrical", mode="initial")
|
|
832
|
+
def motor_cost(self):
|
|
833
|
+
return self.power * self.spc_motor_cost
|
|
834
|
+
|
|
835
|
+
@cost_property(category="labor,opex", mode="maintenance")
|
|
836
|
+
def repair_cost(self):
|
|
837
|
+
return self.power * 0.1
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@forge
|
|
841
|
+
class MetalBase(Component, CostModel):
|
|
842
|
+
cost_per_item = 1000
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
@forge
|
|
846
|
+
class SysEcon(Economics):
|
|
847
|
+
terms_per_year = 12
|
|
848
|
+
|
|
849
|
+
def calculate_production(self, parent, term):
|
|
850
|
+
return self.parent.fan.volumetric_flow
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
@forge
|
|
854
|
+
class FanSystem(System, CostModel):
|
|
855
|
+
base = Slot.define(
|
|
856
|
+
Component
|
|
857
|
+
) # FIXME: not showing in "econ" (due to base default cost?)
|
|
858
|
+
fan = Slot.define(Fan)
|
|
859
|
+
motor = Slot.define(Motor)
|
|
860
|
+
|
|
861
|
+
econ = Slot.define(SysEcon)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
FanSystem.default_cost("base", 100)
|