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.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. 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)