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/tabulation.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Tabulation Module:
|
|
2
|
+
|
|
3
|
+
Incrementally records attrs input values and system_properties per save_data() call.
|
|
4
|
+
|
|
5
|
+
save_data() is called after item.eval() is called.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
import attr
|
|
10
|
+
|
|
11
|
+
from engforge.common import inst_vectorize, chunks
|
|
12
|
+
|
|
13
|
+
# from engforge.configuration import Configuration, forge
|
|
14
|
+
from engforge.solveable import SolveableMixin
|
|
15
|
+
from engforge.logging import LoggingMixin
|
|
16
|
+
from engforge.dataframe import DataframeMixin
|
|
17
|
+
from engforge.typing import *
|
|
18
|
+
from engforge.properties import *
|
|
19
|
+
from typing import Callable
|
|
20
|
+
|
|
21
|
+
import numpy
|
|
22
|
+
import pandas
|
|
23
|
+
import os
|
|
24
|
+
import collections
|
|
25
|
+
import uuid
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TableLog(LoggingMixin):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
log = TableLog()
|
|
33
|
+
|
|
34
|
+
# SKIP_REF = ["run_id", "converged", "name", "index"]
|
|
35
|
+
SKIP_REF = ["name", "index"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TabulationMixin(SolveableMixin, DataframeMixin):
|
|
39
|
+
"""In which we define a class that can enable tabulation"""
|
|
40
|
+
|
|
41
|
+
# Super Special Tabulating Index
|
|
42
|
+
# index = 0 # Not an attr on purpose, we want pandas to provide the index
|
|
43
|
+
|
|
44
|
+
# override per class:
|
|
45
|
+
_skip_table_vars: list = None
|
|
46
|
+
_skip_plot_vars: list
|
|
47
|
+
|
|
48
|
+
# Cached and private
|
|
49
|
+
# _table: dict = None
|
|
50
|
+
_anything_changed: bool = True
|
|
51
|
+
_always_save_data = False
|
|
52
|
+
|
|
53
|
+
def __getstate__(self):
|
|
54
|
+
"""remove references and storage of properties, be sure to call super if overwriting this function in your subclass"""
|
|
55
|
+
vs = super().__getstate__()
|
|
56
|
+
pir = vs.pop("_prv_internal_references", None)
|
|
57
|
+
pir = vs.pop("_system_properties_def", None)
|
|
58
|
+
pir = vs.pop("parent", None)
|
|
59
|
+
|
|
60
|
+
return vs
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def anything_changed(self):
|
|
64
|
+
"""use the on_setattr method to determine if anything changed,
|
|
65
|
+
also assume that stat_tab could change without input changes"""
|
|
66
|
+
if not hasattr(self, "_anything_changed"):
|
|
67
|
+
self._anything_changed = True
|
|
68
|
+
|
|
69
|
+
if self._anything_changed or self.always_save_data:
|
|
70
|
+
if self.log_level <= 5:
|
|
71
|
+
self.msg(
|
|
72
|
+
f"change: {self._anything_changed}| always: {self.always_save_data}"
|
|
73
|
+
)
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def last_context(self):
|
|
79
|
+
"""Returns the last context"""
|
|
80
|
+
raise NotImplemented("this should be implemented in the solvable class")
|
|
81
|
+
|
|
82
|
+
# TODO: create an intelligent graph informed anything_changed alerting system (pyee?) and trigger solver_cache expirations appropriately
|
|
83
|
+
# @solver_cached #FIXME: not caching correctly
|
|
84
|
+
@property # FIXME: this is slow
|
|
85
|
+
def dataframe(self):
|
|
86
|
+
"""
|
|
87
|
+
Returns a pandas DataFrame based on the current context.
|
|
88
|
+
|
|
89
|
+
This method checks for the presence of `last_context` and its `dataframe` attribute.
|
|
90
|
+
If they exist, it returns the `dataframe` from `last_context`.
|
|
91
|
+
If not, it checks for the `_patch_dataframe` attribute and returns it if it exists.
|
|
92
|
+
If neither condition is met, it returns an empty DataFrame.
|
|
93
|
+
|
|
94
|
+
:return: A pandas DataFrame based on the current context or an empty DataFrame if no context is available.
|
|
95
|
+
:rtype: pandas.DataFrame
|
|
96
|
+
"""
|
|
97
|
+
""""""
|
|
98
|
+
if hasattr(self, "last_context") and hasattr(self.last_context, "dataframe"):
|
|
99
|
+
return self.last_context.dataframe
|
|
100
|
+
if hasattr(self, "_patch_dataframe") and self._patch_dataframe is not None:
|
|
101
|
+
return self._patch_dataframe
|
|
102
|
+
return pandas.DataFrame([])
|
|
103
|
+
|
|
104
|
+
@dataframe.setter
|
|
105
|
+
def dataframe(self, input_dataframe):
|
|
106
|
+
if hasattr(self, "last_context") and hasattr(self.last_context, "dataframe"):
|
|
107
|
+
raise Exception(f"may not set dataframe on run component")
|
|
108
|
+
self._patch_dataframe = input_dataframe
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def plotable_variables(self):
|
|
112
|
+
"""Checks columns for ones that only contain numeric types or haven't been explicitly skipped"""
|
|
113
|
+
if self.dataframe is not None:
|
|
114
|
+
check_type = lambda key: all(
|
|
115
|
+
[isinstance(v, NUMERIC_TYPES) for v in self.dataframe[key]]
|
|
116
|
+
)
|
|
117
|
+
check_non_mono = lambda key: len(set(self.dataframe[key])) > 1
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
var
|
|
121
|
+
for var in self.dataframe.columns
|
|
122
|
+
if var.lower() not in self.skip_plot_vars
|
|
123
|
+
and check_type(var)
|
|
124
|
+
and check_non_mono(var)
|
|
125
|
+
]
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
# Properties & Attribues
|
|
129
|
+
def print_info(self):
|
|
130
|
+
print(f"INFO: {self.name} | {self.identity}")
|
|
131
|
+
print("#" * 80)
|
|
132
|
+
for key, value in sorted(self.data_dict.items(), key=lambda kv: kv[0]):
|
|
133
|
+
print(f"{key:>40} | {value}")
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def data_dict(self):
|
|
137
|
+
"""this is what is captured and used in each row of the dataframe / table"""
|
|
138
|
+
# NOTE: Solver class overrides this with full system references
|
|
139
|
+
out = collections.OrderedDict()
|
|
140
|
+
sref = self.internal_references()
|
|
141
|
+
for k, v in sref["attributes"].items():
|
|
142
|
+
if k in self.attr_raw_keys:
|
|
143
|
+
out[k] = v.value()
|
|
144
|
+
for k, v in sref["properties"].items():
|
|
145
|
+
out[k] = v.value()
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
@instance_cached
|
|
149
|
+
def attr_raw_keys(self) -> list:
|
|
150
|
+
good = set(self.table_fields())
|
|
151
|
+
return [k for k in attr.fields_dict(self.__class__).keys() if k in good]
|
|
152
|
+
|
|
153
|
+
def set_attr(self, **kwargs):
|
|
154
|
+
assert set(kwargs).issubset(set(self.attr_raw_keys))
|
|
155
|
+
# TODO: support subcomponents via slots lookup
|
|
156
|
+
for k, v in kwargs.items():
|
|
157
|
+
setattr(self, k, v)
|
|
158
|
+
|
|
159
|
+
@instance_cached
|
|
160
|
+
def always_save_data(self):
|
|
161
|
+
"""Checks if any properties are stochastic (random)"""
|
|
162
|
+
return self._always_save_data
|
|
163
|
+
|
|
164
|
+
@solver_cached
|
|
165
|
+
def table_dict(self):
|
|
166
|
+
# We use __get__ to emulate the property, we could call regularly from self but this is more straightforward
|
|
167
|
+
return {
|
|
168
|
+
k.lower(): obj.__get__(self)
|
|
169
|
+
for k, obj in self.system_properties_def.items()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@solver_cached
|
|
173
|
+
def system_properties(self):
|
|
174
|
+
# We use __get__ to emulate the property, we could call regularly from self but this is more straightforward
|
|
175
|
+
tabulated_properties = [
|
|
176
|
+
obj.__get__(self) for k, obj in self.system_properties_def.items()
|
|
177
|
+
]
|
|
178
|
+
return tabulated_properties
|
|
179
|
+
|
|
180
|
+
@instance_cached
|
|
181
|
+
def system_properties_labels(self) -> list:
|
|
182
|
+
"""Returns the labels from table properties"""
|
|
183
|
+
class_dict = self.__class__.__dict__
|
|
184
|
+
tabulated_properties = [
|
|
185
|
+
obj.label.lower() for k, obj in self.system_properties_def.items()
|
|
186
|
+
]
|
|
187
|
+
return tabulated_properties
|
|
188
|
+
|
|
189
|
+
@instance_cached
|
|
190
|
+
def system_properties_types(self) -> list:
|
|
191
|
+
"""Returns the types from table properties"""
|
|
192
|
+
class_dict = self.__class__.__dict__
|
|
193
|
+
tabulated_properties = [
|
|
194
|
+
obj.return_type for k, obj in self.system_properties_def.items()
|
|
195
|
+
]
|
|
196
|
+
return tabulated_properties
|
|
197
|
+
|
|
198
|
+
@instance_cached
|
|
199
|
+
def system_properties_keys(self) -> list:
|
|
200
|
+
"""Returns the table property keys"""
|
|
201
|
+
tabulated_properties = [k for k, obj in self.system_properties_def.items()]
|
|
202
|
+
return tabulated_properties
|
|
203
|
+
|
|
204
|
+
@instance_cached
|
|
205
|
+
def system_properties_description(self) -> list:
|
|
206
|
+
"""returns system_property descriptions if they exist"""
|
|
207
|
+
class_dict = self.__class__.__dict__
|
|
208
|
+
tabulated_properties = [
|
|
209
|
+
obj.desc for k, obj in self.system_properties_def.items()
|
|
210
|
+
]
|
|
211
|
+
return tabulated_properties
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def cls_all_property_labels(cls):
|
|
215
|
+
return [obj.label for k, obj in cls.system_properties_classdef().items()]
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def cls_all_property_keys(cls):
|
|
219
|
+
return [k for k, obj in cls.system_properties_classdef().items()]
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def cls_all_attrs_fields(cls):
|
|
223
|
+
return attr.fields_dict(cls)
|
|
224
|
+
|
|
225
|
+
@solver_cached
|
|
226
|
+
def system_properties_def(self):
|
|
227
|
+
"""Combine other classes table properties into this one, in the case of subclassed system_properties as a property that is cached"""
|
|
228
|
+
return self.__class__.system_properties_classdef()
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def system_properties_classdef(cls, recache=False):
|
|
232
|
+
"""Combine other parent-classes table properties into this one, in the case of subclassed system_properties"""
|
|
233
|
+
from engforge.tabulation import TabulationMixin
|
|
234
|
+
|
|
235
|
+
cls_key = f"_{cls.__name__}_system_properties"
|
|
236
|
+
# Use a cache for deep recursion
|
|
237
|
+
if not recache and hasattr(cls, cls_key):
|
|
238
|
+
res = getattr(cls, cls_key)
|
|
239
|
+
if res is not None:
|
|
240
|
+
return res
|
|
241
|
+
|
|
242
|
+
# otherwise make the cache
|
|
243
|
+
__system_properties = {}
|
|
244
|
+
for k, obj in cls.__dict__.items():
|
|
245
|
+
if isinstance(obj, system_property):
|
|
246
|
+
__system_properties[k] = obj
|
|
247
|
+
|
|
248
|
+
#
|
|
249
|
+
mrl = cls.mro()
|
|
250
|
+
inx_comp = mrl.index(TabulationMixin)
|
|
251
|
+
|
|
252
|
+
# Ensures everything is includes Tabulation Functionality
|
|
253
|
+
mrvs = mrl[1:inx_comp]
|
|
254
|
+
|
|
255
|
+
for mrv in mrvs:
|
|
256
|
+
# Remove anything not in the user code
|
|
257
|
+
log.msg(f"adding system properties from {mrv.__name__}")
|
|
258
|
+
if (
|
|
259
|
+
issubclass(mrv, TabulationMixin)
|
|
260
|
+
# and "engforge" not in mrv.__module__
|
|
261
|
+
):
|
|
262
|
+
for k, obj in mrv.__dict__.items():
|
|
263
|
+
if k not in __system_properties and isinstance(
|
|
264
|
+
obj, system_property
|
|
265
|
+
): # Precedent
|
|
266
|
+
# Assumes our instance has assumed this table property
|
|
267
|
+
prop = getattr(cls, k, None)
|
|
268
|
+
if prop and isinstance(prop, system_property):
|
|
269
|
+
__system_properties[k] = prop
|
|
270
|
+
if log.log_level <= 3:
|
|
271
|
+
log.msg(f"adding system property {mrv.__name__}.{k}")
|
|
272
|
+
|
|
273
|
+
setattr(cls, cls_key, __system_properties)
|
|
274
|
+
|
|
275
|
+
return __system_properties
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def pre_compile(cls):
|
|
279
|
+
cls._anything_changed = True # set default on class
|
|
280
|
+
if any([v.stochastic for k, v in cls.system_properties_classdef(True).items()]):
|
|
281
|
+
log.info(f"setting always save on {cls.__name__}")
|
|
282
|
+
cls._always_save_data = True
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def system_id(self) -> str:
|
|
286
|
+
"""returns an instance unique id based on id(self)"""
|
|
287
|
+
idd = id(self)
|
|
288
|
+
return f"{self.classname}.{idd}"
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from matplotlib import pylab
|
|
3
|
+
from engforge.eng.structure import *
|
|
4
|
+
from engforge.eng.geometry import ShapelySection
|
|
5
|
+
from sectionproperties.pre.library.steel_sections import i_section
|
|
6
|
+
|
|
7
|
+
from numpy import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class test_cantilever(unittest.TestCase):
|
|
11
|
+
# tests the first example here
|
|
12
|
+
# https://www.engineeringtoolbox.com/cantilever-beams-d_1848.html
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self.st = Structure(name="cantilever_beam", add_gravity_force=False)
|
|
16
|
+
self.st.add_node("wall", 0, 0, 0)
|
|
17
|
+
self.st.add_node("free", 0, 5, 0)
|
|
18
|
+
|
|
19
|
+
self.ibeam = i_section(0.3072, 0.1243, 0.0121, 0.008, 0.0089, 4)
|
|
20
|
+
beam = ShapelySection(shape=self.ibeam, material=ANSI_4130())
|
|
21
|
+
self.bm = self.st.add_member("mem", "wall", "free", section=beam)
|
|
22
|
+
|
|
23
|
+
self.st.def_support(
|
|
24
|
+
"wall",
|
|
25
|
+
support_DX=True,
|
|
26
|
+
support_DY=True,
|
|
27
|
+
support_DZ=True,
|
|
28
|
+
support_RY=True,
|
|
29
|
+
support_RX=True,
|
|
30
|
+
support_RZ=True,
|
|
31
|
+
)
|
|
32
|
+
self.st.frame.add_node_load("free", "FX", 3000, case="gravity")
|
|
33
|
+
|
|
34
|
+
self.st.run(check_statics=True)
|
|
35
|
+
|
|
36
|
+
def test_beam(self):
|
|
37
|
+
self.subtest_assert_near(self.bm.A, 53.4 / (100**2))
|
|
38
|
+
self.subtest_assert_near(self.bm.Ix, 8196 / (100**4))
|
|
39
|
+
self.subtest_assert_near(self.bm.Iy, 388.8 / (100**4))
|
|
40
|
+
self.subtest_assert_near(self.bm.section_mass, 41.9)
|
|
41
|
+
|
|
42
|
+
self.subtest_assert_near(self.bm.max_von_mises(), 27.4e6)
|
|
43
|
+
self.subtest_assert_near(float(self.bm.data_dict["min_deflection_y"]), -0.0076)
|
|
44
|
+
# self.subtest_assert_near(float(self.bm.data_dict["max_shear_y"]), 3000)
|
|
45
|
+
self.subtest_assert_near(float(self.bm.data_dict["max_shear_y"]), 3000)
|
|
46
|
+
|
|
47
|
+
df = self.st.node_dataframe.loc["gravity"]
|
|
48
|
+
|
|
49
|
+
dfw = df.loc["wall"]
|
|
50
|
+
dff = df.loc["free"]
|
|
51
|
+
|
|
52
|
+
self.subtest_assert_near(float(dfw["rxfx"]), -3000, msg="wall rxfx")
|
|
53
|
+
self.subtest_assert_near(float(dfw["rxmz"]), 15000, msg="wall rxmz")
|
|
54
|
+
self.subtest_assert_near(float(dfw["dx"]), 0, msg="wall dx")
|
|
55
|
+
|
|
56
|
+
self.subtest_assert_near(float(dff["dx"]), 0.0076, msg="dx")
|
|
57
|
+
self.subtest_assert_near(float(dff["rxfx"]), 0, msg="rxfx")
|
|
58
|
+
self.subtest_assert_near(float(dff["rxmz"]), 0, msg="rxmz")
|
|
59
|
+
|
|
60
|
+
stress_obj = self.bm.get_stress_at(0, "gravity")
|
|
61
|
+
# stress_obj.plot_stress_vm()
|
|
62
|
+
|
|
63
|
+
def subtest_assert_near(self, value, truth, pct=0.025, **kw):
|
|
64
|
+
with self.subTest(**kw):
|
|
65
|
+
self.assertAlmostEqual(value, truth, delta=max(abs(truth * pct), abs(pct)))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class test_truss(unittest.TestCase):
|
|
69
|
+
# Match this example, no beam stresses
|
|
70
|
+
# https://engineeringlibrary.org/reference/trusses-air-force-stress-manual
|
|
71
|
+
|
|
72
|
+
def setUp(self):
|
|
73
|
+
self.st = Structure(name="truss", add_gravity_force=False)
|
|
74
|
+
self.st.add_node("A", 0, 0, 0)
|
|
75
|
+
self.st.add_node("B", 15, 30 * sqrt(3) / 2, 0)
|
|
76
|
+
self.st.add_node("C", 45, 30 * sqrt(3) / 2, 0)
|
|
77
|
+
self.st.add_node("D", 75, 30 * sqrt(3) / 2, 0)
|
|
78
|
+
self.st.add_node("E", 90, 0, 0)
|
|
79
|
+
self.st.add_node("F", 60, 0, 0)
|
|
80
|
+
self.st.add_node("G", 30, 0, 0)
|
|
81
|
+
|
|
82
|
+
pairs = set()
|
|
83
|
+
Lmin = 30 * sqrt(3) / 2
|
|
84
|
+
Lmax = 30.1
|
|
85
|
+
|
|
86
|
+
for n1 in self.st.nodes.values():
|
|
87
|
+
for n2 in self.st.nodes.values():
|
|
88
|
+
L = numpy.sqrt(
|
|
89
|
+
(n1.X - n2.X) ** 2.0 + (n1.Y - n2.Y) ** 2.0 + (n1.Z - n2.Z) ** 2.0
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
L >= Lmin
|
|
94
|
+
and L <= Lmax
|
|
95
|
+
and (n1.name, n2.name) not in pairs
|
|
96
|
+
and (n2.name, n1.name) not in pairs
|
|
97
|
+
):
|
|
98
|
+
pairs.add((n1.name, n2.name))
|
|
99
|
+
|
|
100
|
+
elif (n1.name, n2.name) in pairs or (
|
|
101
|
+
n2.name,
|
|
102
|
+
n1.name,
|
|
103
|
+
) in pairs:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
self.beam = sections.rectangular_section(1.5, 1.5)
|
|
107
|
+
material = ANSI_4130()
|
|
108
|
+
self.section = ShapelySection(shape=self.beam, material=material)
|
|
109
|
+
|
|
110
|
+
constrained = ("A", "E")
|
|
111
|
+
for n1, n2 in pairs:
|
|
112
|
+
bkey = f"{n1}_{n2}"
|
|
113
|
+
self.bm = self.st.add_member(
|
|
114
|
+
bkey,
|
|
115
|
+
n1,
|
|
116
|
+
n2,
|
|
117
|
+
# material=ANSI_4130(),
|
|
118
|
+
section=self.section,
|
|
119
|
+
# min_mesh_size=0.01,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# if n1 not in constrained:
|
|
123
|
+
# print(f'releasing {bkey}')
|
|
124
|
+
# self.st.frame.DefineReleases(bkey, Rzi=True)
|
|
125
|
+
|
|
126
|
+
# if n2 not in constrained:
|
|
127
|
+
# print(f'releasing {bkey} J')
|
|
128
|
+
# self.st.frame.DefineReleases(bkey, Rzj=True)
|
|
129
|
+
|
|
130
|
+
self.st.def_support(
|
|
131
|
+
"A",
|
|
132
|
+
support_DX=True,
|
|
133
|
+
support_DY=True,
|
|
134
|
+
support_DZ=True,
|
|
135
|
+
support_RY=True,
|
|
136
|
+
support_RX=True,
|
|
137
|
+
support_RZ=True,
|
|
138
|
+
)
|
|
139
|
+
self.st.def_support(
|
|
140
|
+
"E",
|
|
141
|
+
support_DX=False,
|
|
142
|
+
support_DY=True,
|
|
143
|
+
support_DZ=True,
|
|
144
|
+
support_RY=False,
|
|
145
|
+
support_RX=False,
|
|
146
|
+
support_RZ=True,
|
|
147
|
+
)
|
|
148
|
+
# for node in self.st.nodes:
|
|
149
|
+
# self.st.frame.Definesupport_(node,support_DZ=True,support_RZ=True)
|
|
150
|
+
|
|
151
|
+
self.st.add_node_load("F", "FY", -1000, case="gravity")
|
|
152
|
+
self.st.add_node_load("G", "FY", -2000, case="gravity")
|
|
153
|
+
|
|
154
|
+
self.st.run(check_statics=True)
|
|
155
|
+
# self.st.visulize()
|
|
156
|
+
|
|
157
|
+
def test_reactions(self):
|
|
158
|
+
df = self.st.node_dataframe.loc["gravity"]
|
|
159
|
+
# print(df)
|
|
160
|
+
|
|
161
|
+
dfa = df.loc["A"]
|
|
162
|
+
dfe = df.loc["E"]
|
|
163
|
+
|
|
164
|
+
# print(dfa)
|
|
165
|
+
# print(dfe)
|
|
166
|
+
|
|
167
|
+
self.subtest_assert_near(float(dfa["rxfy"]), 1667)
|
|
168
|
+
self.subtest_assert_near(float(dfe["rxfy"]), 1333)
|
|
169
|
+
|
|
170
|
+
self.subtest_member("A", "B", "max_axial", 1925)
|
|
171
|
+
self.subtest_member("A", "G", "max_axial", -949) # -926 textbook
|
|
172
|
+
self.subtest_member("B", "C", "max_axial", 1925)
|
|
173
|
+
self.subtest_member("B", "G", "max_axial", -1925)
|
|
174
|
+
self.subtest_member("C", "D", "max_axial", 1541)
|
|
175
|
+
self.subtest_member("F", "G", "max_axial", -1734)
|
|
176
|
+
self.subtest_member("C", "F", "max_axial", 382)
|
|
177
|
+
self.subtest_member("C", "G", "max_axial", -382)
|
|
178
|
+
self.subtest_member("C", "G", "max_axial", -378) # 1541 textbook
|
|
179
|
+
self.subtest_member("D", "F", "max_axial", -1541)
|
|
180
|
+
self.subtest_member("E", "F", "max_axial", -770)
|
|
181
|
+
|
|
182
|
+
# Visualization.RenderModel( self.st.frame )
|
|
183
|
+
|
|
184
|
+
def subtest_member(self, nodea, nodeb, result_key, truth, pct=0.025):
|
|
185
|
+
with self.subTest(msg=f"test {nodea}->{nodeb}"):
|
|
186
|
+
key_1 = f"{nodea}_{nodeb}"
|
|
187
|
+
key_2 = f"{nodeb}_{nodea}"
|
|
188
|
+
|
|
189
|
+
if key_1 in self.st.beams:
|
|
190
|
+
key = key_1
|
|
191
|
+
elif key_2 in self.st.beams:
|
|
192
|
+
key = key_2
|
|
193
|
+
else:
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
value = self.get_member_result(nodea, nodeb, result_key)
|
|
197
|
+
dopasst = abs(value - truth) <= abs(truth) * pct
|
|
198
|
+
|
|
199
|
+
if not dopasst:
|
|
200
|
+
print(f"fails {key} {result_key}| {value:3.5f} == {truth:3.5f}?")
|
|
201
|
+
self.subtest_assert_near(value, truth, pct=pct)
|
|
202
|
+
|
|
203
|
+
def subtest_assert_near(self, value, truth, pct=0.025):
|
|
204
|
+
with self.subTest():
|
|
205
|
+
self.assertAlmostEqual(value, truth, delta=abs(truth * pct))
|
|
206
|
+
|
|
207
|
+
def get_member_result(self, nodea, nodeb, result_key):
|
|
208
|
+
key_1 = f"{nodea}_{nodeb}"
|
|
209
|
+
key_2 = f"{nodeb}_{nodea}"
|
|
210
|
+
|
|
211
|
+
if key_1 in self.st.beams:
|
|
212
|
+
mem = self.st.beams[key_1]
|
|
213
|
+
if result_key in mem.data_dict:
|
|
214
|
+
return float(mem.data_dict[result_key])
|
|
215
|
+
|
|
216
|
+
elif key_2 in self.st.beams:
|
|
217
|
+
mem = self.st.beams[key_2]
|
|
218
|
+
if result_key in mem.data_dict:
|
|
219
|
+
return float(mem.data_dict[result_key])
|
|
220
|
+
|
|
221
|
+
return numpy.nan # shouod fail, nan is not comparable
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from engforge.datastores.reporting import *
|
|
2
|
+
from engforge.configuration import forge
|
|
3
|
+
from engforge.tabulation import system_property
|
|
4
|
+
from engforge.components import Component
|
|
5
|
+
from engforge.analysis import Analysis
|
|
6
|
+
from logging import *
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
|
|
10
|
+
# tests scripts
|
|
11
|
+
|
|
12
|
+
# suprise the db and add more columns to test dynamic creation
|
|
13
|
+
suprise = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@forge
|
|
17
|
+
class TestComponent(Component):
|
|
18
|
+
blank = attr.ib(default=None, validator=None) # no validator
|
|
19
|
+
|
|
20
|
+
name = attr.ib(default="test", validator=STR_VALIDATOR())
|
|
21
|
+
word = attr.ib(default="bird", validator=STR_VALIDATOR())
|
|
22
|
+
val = attr.ib(default=10)
|
|
23
|
+
|
|
24
|
+
always_save_data = True
|
|
25
|
+
|
|
26
|
+
@system_property
|
|
27
|
+
def synthetic_val(self):
|
|
28
|
+
if self.word == "schfiftyfive":
|
|
29
|
+
return 55
|
|
30
|
+
return random.random() * self.val
|
|
31
|
+
|
|
32
|
+
@system_property
|
|
33
|
+
def nan_val(self):
|
|
34
|
+
return numpy.nan
|
|
35
|
+
|
|
36
|
+
@system_property
|
|
37
|
+
def inf_val(self):
|
|
38
|
+
return numpy.inf
|
|
39
|
+
|
|
40
|
+
if suprise:
|
|
41
|
+
|
|
42
|
+
@system_property
|
|
43
|
+
def surpise_val(self):
|
|
44
|
+
return 13
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@forge
|
|
48
|
+
class OtherComponent(Component):
|
|
49
|
+
arg1 = attr.ib(default="mmmm", validator=STR_VALIDATOR())
|
|
50
|
+
arg2 = attr.ib(default="word", validator=STR_VALIDATOR())
|
|
51
|
+
argnum = attr.ib(default=10)
|
|
52
|
+
|
|
53
|
+
if suprise:
|
|
54
|
+
ones = attr.ib(default=1111)
|
|
55
|
+
wacky = attr.ib(default="one hundred and 111", validator=STR_VALIDATOR())
|
|
56
|
+
more = attr.ib(default="other stuff", validator=STR_VALIDATOR())
|
|
57
|
+
|
|
58
|
+
always_save_data = True
|
|
59
|
+
|
|
60
|
+
@system_property
|
|
61
|
+
def random_val(self):
|
|
62
|
+
return self.argnum * random.random() / random.random()
|
|
63
|
+
|
|
64
|
+
if suprise:
|
|
65
|
+
|
|
66
|
+
@system_property
|
|
67
|
+
def new_val(self):
|
|
68
|
+
return self.argnum * random.random()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if suprise:
|
|
72
|
+
|
|
73
|
+
@forge
|
|
74
|
+
class SupriseComponent(Component):
|
|
75
|
+
arg3 = attr.ib(default="mmmm", validator=STR_VALIDATOR())
|
|
76
|
+
arg4 = attr.ib(default="word", validator=STR_VALIDATOR())
|
|
77
|
+
argque = attr.ib(default=-999)
|
|
78
|
+
# ones = attr.ib(default=1111)
|
|
79
|
+
# wacky = attr.ib(default='one hundred and 111', validator=STR_VALIDATOR())
|
|
80
|
+
# more = attr.ib(default='other stuff', validator=STR_VALIDATOR())
|
|
81
|
+
|
|
82
|
+
always_save_data = True
|
|
83
|
+
|
|
84
|
+
@system_property
|
|
85
|
+
def negative_random_val(self):
|
|
86
|
+
return self.argque * random.random() / random.random()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@forge
|
|
90
|
+
class TestAnalysis(Analysis):
|
|
91
|
+
if suprise:
|
|
92
|
+
surprise = attr.ib(factory=SupriseComponent)
|
|
93
|
+
|
|
94
|
+
other_cocomponent = attr.ib(factory=OtherComponent)
|
|
95
|
+
internal_component = attr.ib(factory=TestComponent)
|
|
96
|
+
some_random_value = attr.ib(default=10)
|
|
97
|
+
other_rand_val = attr.ib(default=1e6)
|
|
98
|
+
|
|
99
|
+
mode = "iterator"
|
|
100
|
+
iterator = attr.ib(factory=lambda: list(range(10)))
|
|
101
|
+
always_save_data = True
|
|
102
|
+
|
|
103
|
+
@system_property
|
|
104
|
+
def internal_val(self):
|
|
105
|
+
return self.internal_component.val
|
|
106
|
+
|
|
107
|
+
@system_property
|
|
108
|
+
def rand_val(self):
|
|
109
|
+
return random.random() * self.internal_component.val
|
|
110
|
+
|
|
111
|
+
def eval(self, item):
|
|
112
|
+
self.some_random_value = item
|
|
113
|
+
|
|
114
|
+
@system_property
|
|
115
|
+
def random_none(self):
|
|
116
|
+
val = random.random()
|
|
117
|
+
if val > 0.6:
|
|
118
|
+
return None
|
|
119
|
+
return val
|
|
120
|
+
|
|
121
|
+
if suprise:
|
|
122
|
+
|
|
123
|
+
@system_property
|
|
124
|
+
def suprise(self):
|
|
125
|
+
return random.random() * self.internal_component.val
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
log = logging.getLogger()
|
|
130
|
+
DEBUG = True
|
|
131
|
+
if DEBUG:
|
|
132
|
+
log.setLevel(logging.DEBUG)
|
|
133
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
134
|
+
set_all_loggers_to(logging.DEBUG)
|
|
135
|
+
|
|
136
|
+
analysis = TestAnalysis()
|
|
137
|
+
tc = analysis.internal_component
|
|
138
|
+
|
|
139
|
+
# 1) Ensure exists Reflect The Database
|
|
140
|
+
db = DBConnection("reports", host="localhost", user="postgres", passd="dumbpass")
|
|
141
|
+
db.ensure_database_exists()
|
|
142
|
+
# db.engine.echo = True
|
|
143
|
+
|
|
144
|
+
rr = ResultsRegistry(db=db)
|
|
145
|
+
# rr.ensure_all_tables()
|
|
146
|
+
# rr.base.metadata.drop_all()
|
|
147
|
+
# rr.initalize()
|
|
148
|
+
|
|
149
|
+
# 2) Use ComponentRegistry, and AnalysisRegistry to gather subclasses of each. Analysis will also have results tables since they are components, these will be referenced in the analysis table.
|
|
150
|
+
analysis.run()
|
|
151
|
+
rr.ensure_analysis(analysis)
|
|
152
|
+
thread = rr.upload_analysis(analysis, use_thread=True)
|
|
153
|
+
thread.join()
|
|
154
|
+
|
|
155
|
+
# 3) For each component and analysis map create tables or map them if they dont exist.
|
|
156
|
+
# - use a registry approach (singleton?) to map the {component: db_class} pairs
|
|
157
|
+
|
|
158
|
+
# 4) When an an an analysis is solved, if configured for reporting, on post processing the results will be added to the database
|