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,673 @@
|
|
|
1
|
+
import attr, attrs
|
|
2
|
+
|
|
3
|
+
from engforge.engforge_attributes import AttributedBaseMixin
|
|
4
|
+
from engforge.logging import LoggingMixin, log, change_all_log_levels
|
|
5
|
+
from engforge.properties import *
|
|
6
|
+
from engforge.env_var import EnvVariable
|
|
7
|
+
import randomname
|
|
8
|
+
import datetime
|
|
9
|
+
import copy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# make a module logger
|
|
13
|
+
class ConfigLog(LoggingMixin):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
log = ConfigLog()
|
|
18
|
+
|
|
19
|
+
conv_nms = lambda v: v.split(",") if isinstance(v, str) else v
|
|
20
|
+
NAME_ADJ = EnvVariable(
|
|
21
|
+
"FORGE_NAME_ADJ",
|
|
22
|
+
default=(
|
|
23
|
+
"geometry",
|
|
24
|
+
"size",
|
|
25
|
+
"algorithms",
|
|
26
|
+
"complexity",
|
|
27
|
+
"colors",
|
|
28
|
+
"materials",
|
|
29
|
+
),
|
|
30
|
+
type_conv=conv_nms,
|
|
31
|
+
)
|
|
32
|
+
NAME_NOUN = EnvVariable(
|
|
33
|
+
"FORGE_NAME_NOUN",
|
|
34
|
+
default=(
|
|
35
|
+
"chemistry",
|
|
36
|
+
"astronomy",
|
|
37
|
+
"linear_algebra",
|
|
38
|
+
"geometry",
|
|
39
|
+
"coding",
|
|
40
|
+
"corporate_job",
|
|
41
|
+
"design",
|
|
42
|
+
"car_parts",
|
|
43
|
+
"machine_learning",
|
|
44
|
+
"physics_units",
|
|
45
|
+
),
|
|
46
|
+
type_conv=conv_nms,
|
|
47
|
+
)
|
|
48
|
+
FORGE_DEBUG = EnvVariable(
|
|
49
|
+
"FORGE_LOG_LEVEL",
|
|
50
|
+
default=20,
|
|
51
|
+
type_conv=int,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def name_generator(instance):
|
|
56
|
+
"""a name generator for the instance"""
|
|
57
|
+
base = str(instance.__class__.__name__).lower() + "-"
|
|
58
|
+
if instance.__class__._use_random_name:
|
|
59
|
+
out = base + randomname.get_name(adj=NAME_ADJ.secret, noun=NAME_NOUN.secret)
|
|
60
|
+
else:
|
|
61
|
+
out = base
|
|
62
|
+
log.debug(f"generated name: {out}")
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
PROTECTED_NAMES = [
|
|
67
|
+
"solver",
|
|
68
|
+
"dataframe",
|
|
69
|
+
"_anything_changed",
|
|
70
|
+
"time",
|
|
71
|
+
"run_id",
|
|
72
|
+
"converged",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Wraps Configuration with a decorator, similar to @attrs.define(**options)
|
|
77
|
+
def forge(cls=None, **kwargs):
|
|
78
|
+
"""Wrap all Configurations with this decorator with the following behavior
|
|
79
|
+
1) we use the callback when any property changes
|
|
80
|
+
2) repr is default
|
|
81
|
+
3) hash is by object identity"""
|
|
82
|
+
|
|
83
|
+
# Define defaults and handle conflicts
|
|
84
|
+
dflts = dict(repr=False, eq=False, slots=False, kw_only=True, hash=False)
|
|
85
|
+
for k, v in kwargs.items():
|
|
86
|
+
if k in dflts:
|
|
87
|
+
dflts.pop(k)
|
|
88
|
+
|
|
89
|
+
if cls is not None:
|
|
90
|
+
# we can't import system here since cls might be system, so look for any system subclasses
|
|
91
|
+
if "System" in [c.__name__ for c in cls.mro()]:
|
|
92
|
+
log.info(f"Configuring System: {cls.__name__}")
|
|
93
|
+
acls = attr.s(
|
|
94
|
+
cls,
|
|
95
|
+
on_setattr=property_changed,
|
|
96
|
+
field_transformer=signals_slots_handler,
|
|
97
|
+
**dflts,
|
|
98
|
+
**kwargs,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# must be here since can't inspect till after fields corrected
|
|
102
|
+
acls.pre_compile() # custom class compiler
|
|
103
|
+
acls.validate_class()
|
|
104
|
+
if acls.__name__ != "Configuration": # prevent configuration lookup
|
|
105
|
+
acls.compile_classes() # compile subclasses
|
|
106
|
+
return acls
|
|
107
|
+
|
|
108
|
+
# Component/Config Flow
|
|
109
|
+
log.msg(f"Configuring: {cls.__name__}")
|
|
110
|
+
acls = attr.s(
|
|
111
|
+
cls,
|
|
112
|
+
on_setattr=property_changed,
|
|
113
|
+
field_transformer=comp_transform,
|
|
114
|
+
**dflts,
|
|
115
|
+
**kwargs,
|
|
116
|
+
)
|
|
117
|
+
if FORGE_DEBUG.in_env:
|
|
118
|
+
change_all_log_levels(inst=acls, new_log_level=FORGE_DEBUG.secret)
|
|
119
|
+
# must be here since can't inspect till after fields corrected
|
|
120
|
+
acls.pre_compile() # custom class compiler
|
|
121
|
+
acls.validate_class()
|
|
122
|
+
if acls.__name__ != "Configuration": # prevent configuration lookup
|
|
123
|
+
acls.compile_classes() # compile subclasses
|
|
124
|
+
return acls
|
|
125
|
+
|
|
126
|
+
else:
|
|
127
|
+
|
|
128
|
+
def f(cls, *args):
|
|
129
|
+
return forge(cls, **kwargs)
|
|
130
|
+
|
|
131
|
+
f.__name__ == "forge_wrapper"
|
|
132
|
+
|
|
133
|
+
return f
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def meta(title, desc=None, **kwargs):
|
|
137
|
+
"""a convienience wrapper to add metadata to attr.ib
|
|
138
|
+
:param title: a title that gets formatted for column headers
|
|
139
|
+
:param desc: a description of the property"""
|
|
140
|
+
out = {
|
|
141
|
+
"label": title.replace(".", "_").replace("-", "_").title(),
|
|
142
|
+
"desc": None,
|
|
143
|
+
**kwargs,
|
|
144
|
+
}
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Class Definition Wrapper Methods
|
|
149
|
+
def property_changed(instance, variable, value):
|
|
150
|
+
"""a callback for when a property changes, this will set the _anything_changed flag to True, and change value when appropriate"""
|
|
151
|
+
from engforge.tabulation import TabulationMixin
|
|
152
|
+
from engforge.problem_context import ProblemExec
|
|
153
|
+
|
|
154
|
+
# TODO: determine when, in an active problem if components change, and update refs accordingly!
|
|
155
|
+
|
|
156
|
+
if not isinstance(instance, (TabulationMixin)):
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
# Establish Active Session
|
|
160
|
+
session = None
|
|
161
|
+
if hasattr(ProblemExec.class_cache, "session"):
|
|
162
|
+
session = ProblemExec.class_cache.session
|
|
163
|
+
|
|
164
|
+
if not session and instance._anything_changed:
|
|
165
|
+
# Bypass Check since we've already flagged for an update
|
|
166
|
+
if log.log_level <= 2:
|
|
167
|
+
log.debug(f"already property changed {instance}{variable.name} {value}")
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
# elif session:
|
|
171
|
+
# notify session that this variable has changed
|
|
172
|
+
# log.info(f'property changed {variable.name} {value}')
|
|
173
|
+
|
|
174
|
+
# TODO: notify change input system here, perhaps with & without a session
|
|
175
|
+
# session.change_sys_var(variable,value,doset=False)
|
|
176
|
+
attrs = attr.fields(instance.__class__) # check identity of variable
|
|
177
|
+
cur = getattr(instance, variable.name)
|
|
178
|
+
is_different = value != cur if isinstance(value, (int, float, str)) else True
|
|
179
|
+
is_var = variable in attrs
|
|
180
|
+
chgnw = instance._anything_changed
|
|
181
|
+
|
|
182
|
+
if log.log_level <= 6:
|
|
183
|
+
log.debug(
|
|
184
|
+
f"checking property changed {instance}{variable.name} {value}|invar: {is_var}| nteqval: {is_different}"
|
|
185
|
+
)
|
|
186
|
+
print(
|
|
187
|
+
f"checking property changed {instance}{variable.name} {value}|invar: {is_var}| nteqval: {is_different}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Check if should be updated
|
|
191
|
+
if not chgnw and is_var and is_different:
|
|
192
|
+
if log.log_level < 5:
|
|
193
|
+
log.debug(f"changing variables: {variable.name} {value}")
|
|
194
|
+
instance._anything_changed = True
|
|
195
|
+
|
|
196
|
+
elif log.log_level < 4 and is_var and not is_different:
|
|
197
|
+
log.warning(f"variables same {variable.name}| {value} == {cur}")
|
|
198
|
+
|
|
199
|
+
# If active session in dynamic mode and the component is dynamic, flag for matrix update
|
|
200
|
+
# TODO: determine if dynamic matricies affected by this.
|
|
201
|
+
if session and session.dynamic_solve and instance.is_dynamic:
|
|
202
|
+
# log.info(f'dynamics changed')
|
|
203
|
+
session.dynamics_updated = True # flag for update
|
|
204
|
+
|
|
205
|
+
elif session:
|
|
206
|
+
# log.info(f'static change')
|
|
207
|
+
session.dynamics_updated = False
|
|
208
|
+
|
|
209
|
+
return value
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Class Definition Wrapper Methods
|
|
213
|
+
def signals_slots_handler(
|
|
214
|
+
cls, fields, slots=True, signals=True, solvers=True, sys=True, plots=True
|
|
215
|
+
):
|
|
216
|
+
"""
|
|
217
|
+
creates attributes as per the attrs.define field_transformer use case.
|
|
218
|
+
|
|
219
|
+
Customize initalization with slots,signals,solvers and sys flags.
|
|
220
|
+
"""
|
|
221
|
+
from engforge.problem_context import ProblemExec
|
|
222
|
+
|
|
223
|
+
log.debug(f"transforming signals and slots for {cls.__name__}")
|
|
224
|
+
|
|
225
|
+
# add attrs attributes from mro parent class
|
|
226
|
+
# TODO: work on this to allow attrs from non attrs classes
|
|
227
|
+
# fd = {f.name:f for f in fields}
|
|
228
|
+
# has_fields = set(list(fd.keys()))
|
|
229
|
+
#
|
|
230
|
+
# if cls._inherit_parent_attributres:
|
|
231
|
+
# non_dec_attrs = {} #get all attrs variables from non-decorated mixins
|
|
232
|
+
# for icls in cls.mro():
|
|
233
|
+
# if attrs.has(icls):
|
|
234
|
+
# print(icls,str(list(attrs.fields_dict(icls).keys()))[:100])
|
|
235
|
+
# atr_dict = {k:v.evolve(icls) for k,v in icls.__dict__.items() if isinstance(v,attrs.Attribute)}
|
|
236
|
+
# non_dec_attrs.update({k:v for k,v in atr_dict.items() if k not in non_dec_attrs and k not in has_fields})
|
|
237
|
+
# if non_dec_attrs:
|
|
238
|
+
# log.info(f'adding non decorated attrs: {list(non_dec_attrs.keys())}')
|
|
239
|
+
# fields = fields + list(non_dec_attrs.values())
|
|
240
|
+
|
|
241
|
+
# Fields
|
|
242
|
+
for t in fields:
|
|
243
|
+
if t.name in PROTECTED_NAMES:
|
|
244
|
+
raise Exception(f"cannot use {t.name} as a field name, its protected")
|
|
245
|
+
if t.type is None:
|
|
246
|
+
log.warning(f"{cls.__name__}.{t.name} has no type")
|
|
247
|
+
|
|
248
|
+
out = []
|
|
249
|
+
field_names = set([o.name for o in fields])
|
|
250
|
+
log.debug(f"fields: {field_names}")
|
|
251
|
+
|
|
252
|
+
# Add Fields (no underscored fields)
|
|
253
|
+
in_fields = {f.name: f for f in fields if not f.name.startswith("_")}
|
|
254
|
+
if "name" in in_fields:
|
|
255
|
+
name = in_fields.pop("name")
|
|
256
|
+
out.append(name)
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
log.warning(f"{cls.__name__} does not have a name!")
|
|
260
|
+
name = attrs.Attribute(
|
|
261
|
+
name="name",
|
|
262
|
+
default=attrs.Factory(name_generator, True),
|
|
263
|
+
validator=None,
|
|
264
|
+
repr=True,
|
|
265
|
+
cmp=None,
|
|
266
|
+
eq=True,
|
|
267
|
+
eq_key=None,
|
|
268
|
+
order=True,
|
|
269
|
+
order_key=None,
|
|
270
|
+
hash=None,
|
|
271
|
+
init=True,
|
|
272
|
+
metadata=None,
|
|
273
|
+
type=str,
|
|
274
|
+
converter=None,
|
|
275
|
+
kw_only=True,
|
|
276
|
+
inherited=True,
|
|
277
|
+
on_setattr=None,
|
|
278
|
+
alias="name",
|
|
279
|
+
)
|
|
280
|
+
out.append(name)
|
|
281
|
+
|
|
282
|
+
# Add Slots
|
|
283
|
+
if slots:
|
|
284
|
+
for slot_name, slot in cls.slots_attributes().items():
|
|
285
|
+
at = slot.make_attribute(slot_name, cls)
|
|
286
|
+
out.append(at)
|
|
287
|
+
|
|
288
|
+
# Add Signals
|
|
289
|
+
if signals:
|
|
290
|
+
for signal_name, signal in cls.signals_attributes().items():
|
|
291
|
+
at = signal.make_attribute(signal_name, cls)
|
|
292
|
+
out.append(at)
|
|
293
|
+
|
|
294
|
+
# Add SOLVERS
|
|
295
|
+
if solvers:
|
|
296
|
+
for solver_name, solver in cls.solvers_attributes().items():
|
|
297
|
+
at = solver.make_attribute(solver_name, cls)
|
|
298
|
+
out.append(at)
|
|
299
|
+
|
|
300
|
+
# Add Time
|
|
301
|
+
for solver_name, solver in cls.transients_attributes().items():
|
|
302
|
+
# add from cls since not accessible from attrs
|
|
303
|
+
at = solver.make_attribute(solver_name, cls)
|
|
304
|
+
out.append(at)
|
|
305
|
+
|
|
306
|
+
if plots:
|
|
307
|
+
for pltname, plot in cls.plot_attributes().items():
|
|
308
|
+
at = plot.make_attribute(pltname, cls)
|
|
309
|
+
out.append(at)
|
|
310
|
+
|
|
311
|
+
for pltname, plot in cls.trace_attributes().items():
|
|
312
|
+
at = plot.make_attribute(pltname, cls)
|
|
313
|
+
out.append(at)
|
|
314
|
+
|
|
315
|
+
created_fields = set([o.name for o in out])
|
|
316
|
+
|
|
317
|
+
# Merge Fields Checking if we are overriding an attribute with system_property
|
|
318
|
+
# hack since TabulationMixin isn't available yet
|
|
319
|
+
if "TabulationMixin" in str(cls.mro()):
|
|
320
|
+
cls_properties = cls.system_properties_classdef(True)
|
|
321
|
+
else:
|
|
322
|
+
cls_properties = {}
|
|
323
|
+
|
|
324
|
+
cls_dict = cls.__dict__.copy()
|
|
325
|
+
cls.__anony_store = {}
|
|
326
|
+
# print(f'tab found!! {cls_properties.keys()}')
|
|
327
|
+
for k, o in in_fields.items():
|
|
328
|
+
if k not in created_fields:
|
|
329
|
+
if k in cls_properties and o.inherited:
|
|
330
|
+
log.warning(
|
|
331
|
+
f"{cls.__name__} overriding inherited attr: {o.name} as a system property overriding it"
|
|
332
|
+
)
|
|
333
|
+
elif o.inherited and k in cls_dict:
|
|
334
|
+
log.warning(
|
|
335
|
+
f"{cls.__name__} overriding inherited attr: {o.name} as {cls_dict[k]} in cls"
|
|
336
|
+
)
|
|
337
|
+
# FIXME: should we deepcopy?
|
|
338
|
+
val = copy.copy(cls_dict[k])
|
|
339
|
+
cls.__anony_store[k] = sscb = lambda: val
|
|
340
|
+
out.append(o.evolve(default=attrs.Factory(sscb)))
|
|
341
|
+
else:
|
|
342
|
+
log.msg(f"{cls.__name__} adding attr: {o.name}")
|
|
343
|
+
out.append(o)
|
|
344
|
+
else:
|
|
345
|
+
log.warning(
|
|
346
|
+
f"{cls.__name__} skipping inherited attr: {o.name} as a custom type overriding it"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Enforce Property Changing
|
|
350
|
+
# FIXME: is this more reliable than a class wide callback?
|
|
351
|
+
# real_out = []
|
|
352
|
+
# for fld in out:
|
|
353
|
+
# if fld.type in (int,float,str):
|
|
354
|
+
# #log.warning(f"setting property changed on {fld}")
|
|
355
|
+
# fld = fld.evolve(on_setattr = property_changed)
|
|
356
|
+
# real_out.append(fld)
|
|
357
|
+
# else:
|
|
358
|
+
# real_out.append(fld)
|
|
359
|
+
# #return real_out
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# alternate initalisers
|
|
364
|
+
comp_transform = lambda c, f: signals_slots_handler(
|
|
365
|
+
c, f, slots=True, signals=True, solvers=True, sys=False, plots=False
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# TODO: Make A MetaClass for Configuration, and provide forge interface there. Problem with replaceing metaclass later, as in the case of a singleton.
|
|
370
|
+
@forge
|
|
371
|
+
class Configuration(AttributedBaseMixin):
|
|
372
|
+
"""Configuration is a pattern for storing attributes that might change frequently, and proivdes the core functionality for a host of different applications.
|
|
373
|
+
|
|
374
|
+
Configuration is able to go through itself and its objects and map all included Configurations, just to a specific level.
|
|
375
|
+
|
|
376
|
+
Common functionality includes an __on_init__ wrapper for attrs post-init method
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
_temp_vars = None
|
|
380
|
+
|
|
381
|
+
_use_random_name: bool = True
|
|
382
|
+
name: str = attr.ib(
|
|
383
|
+
default=attrs.Factory(name_generator, True),
|
|
384
|
+
validator=attr.validators.instance_of(str),
|
|
385
|
+
kw_only=True,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
_inherit_parent_attributres = True
|
|
389
|
+
log_fmt = "[%(name)-24s]%(message)s"
|
|
390
|
+
log_silo = True
|
|
391
|
+
|
|
392
|
+
_created_datetime = None
|
|
393
|
+
_subclass_init: bool = True
|
|
394
|
+
|
|
395
|
+
# Configuration Information
|
|
396
|
+
def internal_configurations(
|
|
397
|
+
self, check_config=True, use_dict=True, none_ok=False
|
|
398
|
+
) -> dict:
|
|
399
|
+
"""go through all attributes determining which are configuration objects
|
|
400
|
+
additionally this skip any configuration that start with an underscore (private variable)
|
|
401
|
+
"""
|
|
402
|
+
from engforge.configuration import Configuration
|
|
403
|
+
|
|
404
|
+
slots = self.slots_attributes()
|
|
405
|
+
|
|
406
|
+
if check_config:
|
|
407
|
+
chk = lambda k, v: isinstance(v, Configuration)
|
|
408
|
+
else:
|
|
409
|
+
chk = lambda k, v: k in slots and (none_ok or v is not None)
|
|
410
|
+
|
|
411
|
+
obj = self.__dict__
|
|
412
|
+
if not use_dict: # slots
|
|
413
|
+
obj = {k: obj.get(k, None) for k in slots}
|
|
414
|
+
|
|
415
|
+
return {k: v for k, v in obj.items() if chk(k, v) and not k.startswith("_")}
|
|
416
|
+
|
|
417
|
+
def copy_config_at_state(
|
|
418
|
+
self, level=None, levels_deep: int = -1, changed: dict = None, **kw
|
|
419
|
+
):
|
|
420
|
+
"""copy the system at the current state recrusively to a certain level, by default copying everything
|
|
421
|
+
:param levels_deep: how many levels deep to copy, -1 is all
|
|
422
|
+
:param level: the current level, defaults to 0 if not set
|
|
423
|
+
"""
|
|
424
|
+
from engforge.configuration import Configuration
|
|
425
|
+
from engforge.components import SolveableInterface
|
|
426
|
+
|
|
427
|
+
if changed is None:
|
|
428
|
+
# top!
|
|
429
|
+
changed = {}
|
|
430
|
+
|
|
431
|
+
if self in changed:
|
|
432
|
+
self.debug(f"already changed {self}")
|
|
433
|
+
return changed[self]
|
|
434
|
+
|
|
435
|
+
if level is None:
|
|
436
|
+
level = 0
|
|
437
|
+
# at top level add parent to changed to prevent infinte parent recursion
|
|
438
|
+
changed[self] = None
|
|
439
|
+
|
|
440
|
+
# exit early if below the level
|
|
441
|
+
if level >= levels_deep and levels_deep > 0:
|
|
442
|
+
self.debug(f"below level {level} {levels_deep} {self}")
|
|
443
|
+
new_sys = attrs.evolve(self) # copy as is
|
|
444
|
+
changed[self] = new_sys
|
|
445
|
+
return new_sys
|
|
446
|
+
|
|
447
|
+
# copy the internal configurations
|
|
448
|
+
kwcomps = {}
|
|
449
|
+
for key, config in self.internal_configurations().items():
|
|
450
|
+
self.debug(f"copying {key} {config} {level} {changed}")
|
|
451
|
+
# copy the system
|
|
452
|
+
if config in changed:
|
|
453
|
+
ccomp = changed[config]
|
|
454
|
+
else:
|
|
455
|
+
ccomp = config.copy_config_at_state(level + 1, levels_deep, changed)
|
|
456
|
+
kwcomps[key] = ccomp
|
|
457
|
+
|
|
458
|
+
# Finally make the new system with changed internals
|
|
459
|
+
self.debug(f"changing with changes {self} {kwcomps} {kw}")
|
|
460
|
+
new_sys = attrs.evolve(self, **kwcomps, **kw)
|
|
461
|
+
changed[self] = new_sys
|
|
462
|
+
|
|
463
|
+
# Finally change references!
|
|
464
|
+
if isinstance(new_sys, SolveableInterface):
|
|
465
|
+
new_sys.system_references(recache=True)
|
|
466
|
+
|
|
467
|
+
# update the parents
|
|
468
|
+
# TODO: use pyee to broadcast change
|
|
469
|
+
if hasattr(self, "parent"):
|
|
470
|
+
if self.parent in changed:
|
|
471
|
+
new_sys.parent = changed[self.parent]
|
|
472
|
+
|
|
473
|
+
return new_sys
|
|
474
|
+
|
|
475
|
+
def go_through_configurations(
|
|
476
|
+
self,
|
|
477
|
+
level=0,
|
|
478
|
+
levels_to_descend=-1,
|
|
479
|
+
parent_level=0,
|
|
480
|
+
only_inst=True,
|
|
481
|
+
**kw,
|
|
482
|
+
):
|
|
483
|
+
"""A generator that will go through all internal configurations up to a certain level
|
|
484
|
+
if levels_to_descend is less than 0 ie(-1) it will go down, if it 0, None, or False it will
|
|
485
|
+
only go through this configuration
|
|
486
|
+
|
|
487
|
+
:return: level,config"""
|
|
488
|
+
from engforge.configuration import Configuration
|
|
489
|
+
|
|
490
|
+
# TODO: instead of a recursive loop a global map per problem context should be used, with a static map of slots, updating with every change per note in system_references. This function could be a part of that but each system shouldn't be responsible for it.
|
|
491
|
+
|
|
492
|
+
should_yield_level = lambda level: all(
|
|
493
|
+
[
|
|
494
|
+
level >= parent_level,
|
|
495
|
+
any([levels_to_descend < 0, level <= levels_to_descend]),
|
|
496
|
+
]
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if should_yield_level(level):
|
|
500
|
+
yield "", level, self
|
|
501
|
+
|
|
502
|
+
level += 1
|
|
503
|
+
if "check_config" not in kw:
|
|
504
|
+
kw["check_config"] = False
|
|
505
|
+
|
|
506
|
+
for key, config in self.internal_configurations(**kw).items():
|
|
507
|
+
if isinstance(config, Configuration):
|
|
508
|
+
for skey, level, iconf in config.go_through_configurations(
|
|
509
|
+
level, levels_to_descend, parent_level
|
|
510
|
+
):
|
|
511
|
+
if not only_inst or iconf is not None:
|
|
512
|
+
yield f"{key}.{skey}" if skey else key, level, iconf
|
|
513
|
+
else:
|
|
514
|
+
if not only_inst or config is not None:
|
|
515
|
+
yield key, level, config
|
|
516
|
+
|
|
517
|
+
# Our Special Init Methodology
|
|
518
|
+
def __on_init__(self):
|
|
519
|
+
"""Override this when creating your special init functionality, you must use attrs for input variables, this is called after parents are assigned. subclasses are always called after"""
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
def __pre_init__(self):
|
|
523
|
+
"""Override this when creating your special init functionality, you must use attrs for input variables, this is called before parents are assigned"""
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
def __attrs_post_init__(self):
|
|
527
|
+
"""This is called after __init__ by attr's functionality, we expose __oninit__ for you to use!"""
|
|
528
|
+
# Store abs path On Creation, in case we change
|
|
529
|
+
|
|
530
|
+
from engforge.components import Component
|
|
531
|
+
|
|
532
|
+
self._log = None
|
|
533
|
+
self._anything_changed = True # save by default first go!
|
|
534
|
+
self._created_datetime = datetime.datetime.utcnow()
|
|
535
|
+
|
|
536
|
+
# subclass instance instance init causes conflicts in structures ect
|
|
537
|
+
self.__pre_init__()
|
|
538
|
+
if self._subclass_init:
|
|
539
|
+
try:
|
|
540
|
+
for comp in self.__class__.mro():
|
|
541
|
+
if (
|
|
542
|
+
hasattr(comp, "__pre_init__")
|
|
543
|
+
and comp.__pre_init__ != Configuration.__pre_init__
|
|
544
|
+
):
|
|
545
|
+
comp.__pre_init__(self)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
self.error(e, f"error in __pre_init__ {e}")
|
|
548
|
+
|
|
549
|
+
# Assign Parents, ensure single componsition
|
|
550
|
+
# TODO: allow multi-parent, w/wo keeping state, state swap on update()?
|
|
551
|
+
# TODO: replace parent concept with problem context
|
|
552
|
+
for compnm, comp in self.internal_configurations(False).items():
|
|
553
|
+
if isinstance(comp, Component):
|
|
554
|
+
# TODO: allow multiple parents
|
|
555
|
+
if (hasattr(comp, "parent")) and (comp.parent is not None):
|
|
556
|
+
self.warning(
|
|
557
|
+
f"Component {compnm} already has a parent {comp.parent} #copying, and assigning to {self}"
|
|
558
|
+
)
|
|
559
|
+
# setattr(self, compnm, attrs.evolve(comp, parent=self))
|
|
560
|
+
else:
|
|
561
|
+
comp.parent = self
|
|
562
|
+
|
|
563
|
+
self.debug(f"created {self.identity}")
|
|
564
|
+
|
|
565
|
+
# subclass instance instance init causes conflicts in structures
|
|
566
|
+
self.__on_init__()
|
|
567
|
+
runs = set((self.__class__.__on_init__,)) # keep track of unique functions
|
|
568
|
+
if self._subclass_init:
|
|
569
|
+
try:
|
|
570
|
+
for comp in self.__class__.mro():
|
|
571
|
+
if (
|
|
572
|
+
hasattr(comp, "__on_init__")
|
|
573
|
+
and comp.__on_init__ != Configuration.__on_init__
|
|
574
|
+
and comp.__on_init__ not in runs
|
|
575
|
+
):
|
|
576
|
+
comp.__on_init__(self)
|
|
577
|
+
runs.add(comp.__on_init__)
|
|
578
|
+
|
|
579
|
+
except Exception as e:
|
|
580
|
+
self.error(e, f"error in __on_init__")
|
|
581
|
+
|
|
582
|
+
@classmethod
|
|
583
|
+
def validate_class(cls):
|
|
584
|
+
"""A customizeable validator at the end of class creation in forge"""
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
@classmethod
|
|
588
|
+
def pre_compile(cls):
|
|
589
|
+
"""an overrideable classmethod that executes when compiled, however will not execute as a subclass"""
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
@classmethod
|
|
593
|
+
def compile_classes(cls):
|
|
594
|
+
"""compiles all subclass functionality"""
|
|
595
|
+
cls.cls_compile()
|
|
596
|
+
for subcls in cls.parent_configurations_cls():
|
|
597
|
+
if subcls.subcls_compile is not Configuration:
|
|
598
|
+
log.debug(f"{cls.__name__} compiling {subcls.__name__}")
|
|
599
|
+
subcls.subcls_compile()
|
|
600
|
+
|
|
601
|
+
@classmethod
|
|
602
|
+
def cls_compile(cls):
|
|
603
|
+
"""compiles this class, override this to compile functionality for this class"""
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
@classmethod
|
|
607
|
+
def subcls_compile(cls):
|
|
608
|
+
"""reliably compiles this method even for subclasses, override this to compile functionality for subclass interfaces & mixins"""
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
@classmethod
|
|
612
|
+
def parent_configurations_cls(cls) -> list:
|
|
613
|
+
"""returns all subclasses that are a Configuration"""
|
|
614
|
+
return [c for c in cls.mro() if issubclass(c, Configuration)]
|
|
615
|
+
|
|
616
|
+
# Identity & location Methods
|
|
617
|
+
@property
|
|
618
|
+
def filename(self):
|
|
619
|
+
"""A nice to have, good to override"""
|
|
620
|
+
fil = (
|
|
621
|
+
self.identity.replace(" ", "_")
|
|
622
|
+
.replace("-", "_")
|
|
623
|
+
.replace(":", "")
|
|
624
|
+
.replace("|", "_")
|
|
625
|
+
.title()
|
|
626
|
+
)
|
|
627
|
+
filename = "".join(
|
|
628
|
+
[c for c in fil if c.isalpha() or c.isdigit() or c == "_" or c == "-"]
|
|
629
|
+
).rstrip()
|
|
630
|
+
return filename
|
|
631
|
+
|
|
632
|
+
@property
|
|
633
|
+
def displayname(self):
|
|
634
|
+
dn = (
|
|
635
|
+
self.identity.replace("_", " ")
|
|
636
|
+
.replace("|", " ")
|
|
637
|
+
.replace("-", " ")
|
|
638
|
+
.replace(" ", " ")
|
|
639
|
+
.title()
|
|
640
|
+
)
|
|
641
|
+
# dispname = "".join([c for c in dn if c.isalpha() or c.isdigit() or c=='_' or c=='-']).rstrip()
|
|
642
|
+
return dn
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def identity(self):
|
|
646
|
+
"""A customizeable property that will be in the log by default"""
|
|
647
|
+
if not self.name or self.name == "default":
|
|
648
|
+
return self.classname.lower()
|
|
649
|
+
if self.classname in self.name:
|
|
650
|
+
return self.name.lower()
|
|
651
|
+
return f"{self.classname}-{self.name}".lower()
|
|
652
|
+
|
|
653
|
+
@property
|
|
654
|
+
def classname(self):
|
|
655
|
+
"""Shorthand for the classname"""
|
|
656
|
+
return str(type(self).__name__).lower()
|
|
657
|
+
|
|
658
|
+
# Structural Orchestration Through Subclassing
|
|
659
|
+
@classmethod
|
|
660
|
+
def subclasses(cls, out=None):
|
|
661
|
+
"""return all subclasses of components, including their subclasses
|
|
662
|
+
:param out: out is to pass when the middle of a recursive operation, do not use it!
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
# init the set by default
|
|
666
|
+
if out is None:
|
|
667
|
+
out = set()
|
|
668
|
+
|
|
669
|
+
for cls in cls.__subclasses__():
|
|
670
|
+
out.add(cls)
|
|
671
|
+
cls.subclasses(out)
|
|
672
|
+
|
|
673
|
+
return out
|