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,271 @@
1
+ import attr
2
+
3
+ # from engforge.configuration import Configuration, forge
4
+ from engforge.components import Component, system_property, forge
5
+
6
+ from engforge.eng.fluid_material import Water, Air, Steam
7
+ from CoolProp.CoolProp import PropsSI
8
+ import CoolProp.CoolProp as CP
9
+ import numpy
10
+ from numpy import vectorize
11
+
12
+ STD_TEMP = 273 + 20
13
+ STD_PRESSURE = 1.01325e5
14
+
15
+
16
+ # Thermodynamics
17
+ @vectorize
18
+ def heat_of_vaporization(P, material="Water", measure="Hmolar"):
19
+ """Returns heat of vaporization for material
20
+ :param P: pressure in P
21
+ """
22
+ assert measure.startswith("H") # Must be an enthalpy
23
+ if P > 2.2063e07: # Critical Pressure
24
+ return 0.0
25
+ H_L = PropsSI(measure, "P", P, "Q", 0, material)
26
+ H_V = PropsSI(measure, "P", P, "Q", 1, material)
27
+ return H_V - H_L
28
+
29
+
30
+ @vectorize
31
+ def boiling_point(P, material="Water"):
32
+ """Returns the boiling point in `K` for the material"""
33
+ return PropsSI("T", "P", P, "Q", 1, material)
34
+
35
+
36
+ # Heat Exchanger
37
+ def dp_he_entrance(sigma, G, rho):
38
+ """Heat Exchanger Entrance Pressure Loss
39
+ :param sigma: contraction-ratio - ratio of minimum flow area to frontal area
40
+ :param G: mass flux of fluid
41
+ :param rho: density of fluid
42
+ """
43
+ Kc = 0.42 * (1.0 - sigma**2.0) ** 2.0
44
+ return (1.0 - sigma**2.0 + Kc) * (G**2.0 / rho) / 2.0
45
+
46
+
47
+ def dp_he_exit(sigma, G, rho):
48
+ """Heat Exchanger Exit Pressure Loss
49
+ :param sigma: contraction-ratio - ratio of minimum flow area to frontal area
50
+ :param G: mass flux of fluid
51
+ :param rho: density of fluid
52
+ """
53
+ Ke = (1.0 - sigma) ** 2.0
54
+ return (1.0 - sigma**2.0 + Ke) * (G**2.0 / rho) / 2.0
55
+
56
+
57
+ def dp_he_core(G, f, L, rho, Dh):
58
+ """Losses due to friction
59
+ :param f: fanning friction factor
60
+ :param G: mass flux (massflow / Area)
61
+ :param L: length of heat exchanger
62
+ :param rho: intermediate density
63
+ :param Dh: diameter of heat exchanger
64
+ """
65
+ top = 4 * f * L * G**2.0
66
+ btm = Dh * 2 * rho
67
+ dp_friciton = top / btm
68
+ return dp_friciton
69
+
70
+
71
+ def dp_he_gas_losses(G, rhoe, rhoi):
72
+ """Measures the pressure loss or gain due to density changes in the HE
73
+ :param G: mass flux
74
+ :param rhoe: exit density
75
+ :param rhoi: entrance density
76
+ """
77
+ dp_pressure = G**2.0 * ((1.0 / rhoe) - (1.0 / rhoi))
78
+ return dp_pressure
79
+
80
+
81
+ def fanning_friction_factor(Re, method="turbulent"):
82
+ if method == "turbulent":
83
+ if Re <= 5e4:
84
+ return 0.0791 / (Re**0.25)
85
+ return 0.0014 + 0.125 / (Re**0.32)
86
+ elif method == "laminar":
87
+ return 16.0 / Re
88
+ else: # Default to turbulent
89
+ return fanning_friction_factor(Re, method="turbulent")
90
+
91
+
92
+ # Simple Elements
93
+ @forge
94
+ class SimpleHeatExchanger(Component):
95
+ Thi = attr.ib()
96
+ mdot_h = attr.ib()
97
+ Cp_h = attr.ib()
98
+
99
+ Tci = attr.ib()
100
+ mdot_c = attr.ib()
101
+ Cp_c = attr.ib()
102
+
103
+ efficiency = attr.ib(default=0.8)
104
+ name = attr.ib(default="HeatExchanger")
105
+
106
+ @system_property
107
+ def CmatH(self) -> float:
108
+ return self.Cp_h * self.mdot_h
109
+
110
+ @system_property
111
+ def CmatC(self) -> float:
112
+ return self.Cp_c * self.mdot_c
113
+
114
+ @system_property
115
+ def Tout_ideal(self) -> float:
116
+ numerator = self.Thi * self.CmatH + self.Tci * self.CmatC
117
+ denominator = self.CmatC + self.CmatH
118
+ return numerator / denominator
119
+
120
+ @system_property
121
+ def Qdot_ideal(self) -> float:
122
+ """Use Tout ideal to determine the heat flow should be the same for both"""
123
+ v1 = self.CmatH * (self.Thi - self.Tout_ideal)
124
+ v2 = self.CmatC * (self.Tout_ideal - self.Tci)
125
+ if abs((v2 - v1) / float(v1)) >= 0.1:
126
+ self.warning("Qdot_ideal not matching")
127
+ return (v1 + v2) / 2.0
128
+
129
+ @system_property
130
+ def Qdot(self) -> float:
131
+ return self.Qdot_ideal * self.efficiency
132
+
133
+ @system_property
134
+ def Th_out(self) -> float:
135
+ return self.Thi - self.Qdot / self.CmatH
136
+
137
+ @system_property
138
+ def Tc_out(self) -> float:
139
+ return self.Tci + self.Qdot / self.CmatC
140
+
141
+
142
+ # Compression
143
+ @forge
144
+ class SimpleCompressor(Component):
145
+ pressure_ratio = attr.ib()
146
+
147
+ Tin = attr.ib()
148
+ mdot = attr.ib()
149
+
150
+ Cp = attr.ib()
151
+ gamma = attr.ib(default=1.4)
152
+
153
+ efficiency = attr.ib(default=0.75)
154
+ name = attr.ib(default="Compressor")
155
+
156
+ @system_property
157
+ def temperature_ratio(self) -> float:
158
+ return (
159
+ self.pressure_ratio ** ((self.gamma - 1.0) / self.gamma) - 1.0
160
+ ) / self.efficiency
161
+
162
+ @system_property
163
+ def Tout(self) -> float:
164
+ return self.temperature_ratio * self.Tin
165
+
166
+ @system_property
167
+ def power_input(self) -> float:
168
+ return self.Cp * self.mdot * (self.Tout - self.Tin)
169
+
170
+ def pressure_out(self, pressure_in):
171
+ return self.pressure_ratio * pressure_in
172
+
173
+
174
+ @forge
175
+ class SimpleTurbine(Component):
176
+ Pout = attr.ib()
177
+
178
+ Pin = attr.ib()
179
+ Tin = attr.ib()
180
+
181
+ mdot = attr.ib()
182
+
183
+ Cp = attr.ib()
184
+ gamma = attr.ib(default=1.4)
185
+
186
+ efficiency = attr.ib(default=0.80)
187
+ name = attr.ib(default="Turbine")
188
+
189
+ @system_property
190
+ def pressure_ratio(self) -> float:
191
+ return self.Pin / self.Pout
192
+
193
+ @system_property
194
+ def Tout(self) -> float:
195
+ return self.Tin * (
196
+ 1
197
+ - self.efficiency
198
+ * (1.0 - (1 / self.pressure_ratio) ** ((self.gamma - 1.0) / self.gamma))
199
+ )
200
+ # return self.Tin * self.pressure_ratio**((self.gamma-1.0)/self.gamma)/ self.efficiency
201
+
202
+ @system_property
203
+ def power_output(self) -> float:
204
+ """calculates power output base on temp diff (where eff applied)"""
205
+ return self.Cp * self.mdot * (self.Tin - self.Tout)
206
+
207
+
208
+ # Compression
209
+ @forge
210
+ class SimplePump(Component):
211
+ MFin = attr.field() # kg/s
212
+ pressure_ratio = attr.field() # nd
213
+ Tin = attr.field(default=STD_TEMP) # k
214
+ Pin = attr.field(default=STD_PRESSURE) # c
215
+
216
+ efficiency = attr.field(default=0.75)
217
+ fluid = attr.field(factory=Water)
218
+ name = attr.field(default="pump")
219
+
220
+ def __on_init__(self):
221
+ self.eval()
222
+
223
+ def eval(self):
224
+ self.fluid.T = self.Tin
225
+ self.fluid.P = self.Pin
226
+
227
+ Tsat = self.fluid.Tsat
228
+ if self.Tin / Tsat >= 1:
229
+ self.warning("infeasible: pumping vapor!")
230
+
231
+ elif (self.Tin / self.fluid.Tsat) > 0.8:
232
+ self.warning("pump near saturated temp")
233
+
234
+ @system_property
235
+ def volumetric_flow(self) -> float:
236
+ return self.MFin / self.fluid.density
237
+
238
+ @system_property
239
+ def temperature_delta(self) -> float:
240
+ rho = self.fluid.density
241
+ v = self.MFin / rho
242
+ p = self.Pin
243
+ w = v * p / self.efficiency # work done
244
+ cp = self.fluid.specific_heat
245
+ dt = w * (1 - self.efficiency) / (rho * cp * v)
246
+ return dt
247
+
248
+ @system_property
249
+ def Pout(self) -> float:
250
+ return self.pressure_ratio * self.Pin
251
+
252
+ @system_property
253
+ def Tout(self) -> float:
254
+ return self.temperature_delta + self.Tin
255
+
256
+ @system_property
257
+ def pressure_delta(self) -> float:
258
+ return (self.pressure_ratio - 1) * self.Pin
259
+
260
+ @system_property
261
+ def power_input(self) -> float:
262
+ return self.volumetric_flow * self.pressure_delta / self.efficiency
263
+
264
+ @system_property
265
+ def cost(self) -> float:
266
+ pwr = self.power_input
267
+ c1 = numpy.log(pwr)
268
+ c2 = -0.03195 * pwr**2.0
269
+ c3 = 467.2 * pwr
270
+ c4 = 2.048e4
271
+ return c1 + c2 + c3 + c4
@@ -0,0 +1,380 @@
1
+ from engforge.attributes import ATTR_BASE, AttributeInstance
2
+ from engforge.attr_dynamics import Time
3
+ from engforge.attr_solver import Solver
4
+ from engforge.attr_signals import Signal
5
+ from engforge.attr_slots import Slot
6
+ from engforge.attr_plotting import Plot, Trace
7
+ from engforge.logging import LoggingMixin, log
8
+ from engforge.typing import *
9
+
10
+ from contextlib import contextmanager
11
+ import deepdiff
12
+ import typing
13
+ import datetime
14
+
15
+
16
+ import attr, attrs
17
+
18
+ from attrs import Attribute
19
+
20
+
21
+ class EngAttr(LoggingMixin):
22
+ pass
23
+
24
+
25
+ log = EngAttr()
26
+
27
+
28
+ def get_attributes_of(cls, subclass_of: type = None, exclude=False):
29
+ choose = issubclass
30
+ if exclude:
31
+ choose = lambda ty, type_set: not issubclass(ty, type_set)
32
+
33
+ if subclass_of is None:
34
+ subclass_of = ATTR_BASE
35
+
36
+ # This handles the attrs class before or after compilation
37
+ attrval = {}
38
+ if "__attrs_attrs__" in cls.__dict__: # Handle Attrs Class
39
+ for k, v in attrs.fields_dict(cls).items():
40
+ if isinstance(v.type, type) and choose(v.type, subclass_of):
41
+ attrval[k] = v
42
+
43
+ # else: # Handle Pre-Attrs Class
44
+ # FIXME: should this run first?
45
+ for k, v in cls.__dict__.items():
46
+ if isinstance(v, type) and choose(v, subclass_of):
47
+ attrval[k] = v
48
+
49
+ return attrval
50
+
51
+
52
+ class AttributedBaseMixin(LoggingMixin):
53
+ """A mixin that adds the ability to configure all engforge.core attributes of a class"""
54
+
55
+ # Auto Configuration Methods
56
+ @classmethod
57
+ def collect_all_attributes(cls):
58
+ """collects all the attributes for a system"""
59
+ out = {}
60
+ for base_class in ATTR_BASE.subclasses():
61
+ nm = base_class.__name__.lower()
62
+ out[nm] = base_class.collect_cls(cls)
63
+ return out
64
+
65
+ def collect_inst_attributes(self, **kw):
66
+ """collects all the attributes for a system"""
67
+ out = {}
68
+ for base_class in ATTR_BASE.subclasses():
69
+ nm = base_class.__name__.lower()
70
+ out[nm] = base_class.collect_attr_inst(self, **kw)
71
+ return out
72
+
73
+ @classmethod
74
+ def _get_init_attrs_data(cls, subclass_of: type, exclude=False, attr_type=False):
75
+ choose = issubclass
76
+ if exclude:
77
+ choose = lambda ty, type_set: not issubclass(ty, type_set)
78
+
79
+ attrval = {}
80
+ if "__attrs_attrs__" in cls.__dict__: # Handle Attrs Class
81
+ for k, v in attrs.fields_dict(cls).items():
82
+ if isinstance(v.type, type) and choose(v.type, subclass_of):
83
+ attrval[k] = v.type if attr_type else v
84
+
85
+ # else: # Handle Pre-Attrs Class
86
+ # FIXME: should this run first?
87
+ for k, v in cls.__dict__.items():
88
+ if isinstance(v, type) and choose(v, subclass_of):
89
+ attrval[k] = v
90
+
91
+ return attrval
92
+
93
+ # Attribute Methods
94
+ @property
95
+ def attrs_fields(self) -> set:
96
+ return set(attr.fields(self.__class__))
97
+
98
+ @classmethod
99
+ def _extract_type(cls, typ):
100
+ """gathers valid types for an attribute.type"""
101
+ from engforge.attr_slots import Slot
102
+ from engforge.configuration import Configuration
103
+
104
+ if not isinstance(typ, type) or typ is None:
105
+ return list()
106
+
107
+ if issubclass(typ, Slot):
108
+ accept = typ.accepted
109
+ if isinstance(accept, (tuple, list)):
110
+ return list(accept)
111
+ return [accept]
112
+
113
+ elif issubclass(typ, Configuration):
114
+ return [typ]
115
+
116
+ elif issubclass(typ, TABLE_TYPES):
117
+ return [typ]
118
+
119
+ @classmethod
120
+ def check_ref_slot_type(cls, sys_key: str) -> list:
121
+ """recursively checks class slots for the key, and returns the slot type"""
122
+
123
+ from engforge.configuration import Configuration
124
+
125
+ slot_refs = cls.slot_refs()
126
+ if sys_key in slot_refs:
127
+ return slot_refs[sys_key]
128
+
129
+ slts = cls.input_attrs()
130
+ key_segs = sys_key.split(".")
131
+ out = []
132
+ # print(slts.keys(),sys_key)
133
+ if "." not in sys_key and sys_key not in slts:
134
+ pass
135
+
136
+ elif sys_key in slts:
137
+ # print(f'slt find {sys_key}')
138
+ return cls._extract_type(slts[sys_key].type)
139
+ else:
140
+ fst = key_segs[0]
141
+ rem = key_segs[1:]
142
+ if fst in slts:
143
+ sub_clss = cls._extract_type(slts[fst].type)
144
+ out = []
145
+ for acpt in sub_clss:
146
+ if isinstance(acpt, type) and issubclass(acpt, Configuration):
147
+ vals = acpt.check_ref_slot_type(".".join(rem))
148
+ # print(f'recursive find {acpt}.{rem} = {vals}')
149
+ if vals:
150
+ out.extend(vals)
151
+
152
+ elif isinstance(acpt, type):
153
+ out.append(acpt)
154
+
155
+ slot_refs[sys_key] = out
156
+
157
+ return out
158
+
159
+ @classmethod
160
+ def slot_refs(cls, recache=False):
161
+ """returns all slot references in this configuration"""
162
+ key = f"{cls.__name__}_prv_slot_sys_refs"
163
+ if recache == False and hasattr(cls, key):
164
+ return getattr(cls, key)
165
+ o = {}
166
+ setattr(cls, key, o)
167
+ return o
168
+
169
+ @classmethod
170
+ def slots_attributes(cls, attr_type=False) -> typing.Dict[str, "Attribute"]:
171
+ """Lists all slots attributes for class"""
172
+ return cls._get_init_attrs_data(Slot, attr_type=attr_type)
173
+
174
+ @classmethod
175
+ def signals_attributes(cls) -> typing.Dict[str, "Attribute"]:
176
+ """Lists all signals attributes for class"""
177
+ return cls._get_init_attrs_data(Signal)
178
+
179
+ @classmethod
180
+ def solvers_attributes(cls) -> typing.Dict[str, "Attribute"]:
181
+ """Lists all signals attributes for class"""
182
+ return cls._get_init_attrs_data(Solver)
183
+
184
+ @classmethod
185
+ def transients_attributes(cls) -> typing.Dict[str, "Attribute"]:
186
+ """Lists all signals attributes for class"""
187
+ return cls._get_init_attrs_data(Time)
188
+
189
+ @classmethod
190
+ def trace_attributes(cls) -> typing.Dict[str, "Attribute"]:
191
+ """Lists all trace attributes for class"""
192
+ return cls._get_init_attrs_data(Trace)
193
+
194
+ @classmethod
195
+ def plot_attributes(cls) -> typing.Dict[str, "Attribute"]:
196
+ """Lists all plot attributes for class"""
197
+ return cls._get_init_attrs_data(Plot)
198
+
199
+ @classmethod
200
+ def input_attrs(cls):
201
+ """Lists all input attributes for class"""
202
+ return attr.fields_dict(cls)
203
+
204
+ @classmethod
205
+ def input_fields(cls, add_ign_types: list = None):
206
+ """no attr base types, no tuples, no lists, no dicts"""
207
+ ignore_types = [
208
+ ATTR_BASE,
209
+ # tuple,
210
+ # list,
211
+ # dict,
212
+ ]
213
+ if add_ign_types:
214
+ ignore_types.extend(add_ign_types)
215
+ return cls._get_init_attrs_data(tuple(ignore_types), exclude=True)
216
+
217
+ @classmethod
218
+ def numeric_fields(cls):
219
+ """no tuples,lists, dicts, strings, or attr base types"""
220
+ ignore_types = (
221
+ ATTR_BASE,
222
+ str,
223
+ tuple,
224
+ list,
225
+ dict,
226
+ )
227
+ typ = cls._get_init_attrs_data(ignore_types, exclude=True)
228
+ return {k: v for k, v in typ.items() if v.type in (int, float)}
229
+
230
+ @classmethod
231
+ def table_fields(cls):
232
+ """the table attributes corresponding to"""
233
+ # TODO: add list/numpy fields with vector stats
234
+ keeps = (str, float, int)
235
+ typ = cls._get_init_attrs_data(keeps)
236
+ return {k: v for k, v in typ.items()}
237
+
238
+ # Dictonaries
239
+ @property
240
+ def as_dict(self):
241
+ """returns values as they are in the class instance"""
242
+ from engforge.configuration import Configuration
243
+
244
+ inputs = self.input_attrs()
245
+ # TODO: add signals?
246
+ properties = getattr(self, "system_properties_classdef", None)
247
+ if properties:
248
+ inputs.update(properties())
249
+
250
+ o = {k: getattr(self, k, None) for k, v in inputs.items()}
251
+ return o
252
+
253
+ # TODO: refactor this, allowing a nesting return option for sub components, by default True (later to be reverted to False, as a breaking change). this messes up hashing and we can just the other object hash
254
+ @property
255
+ def input_as_dict(self):
256
+ """returns values as they are in the class instance, but converts classes inputs to their input_as_dict"""
257
+ from engforge.configuration import Configuration
258
+
259
+ o = {k: getattr(self, k, None) for k in self.input_fields()}
260
+ o = {
261
+ k: v if not isinstance(v, Configuration) else v.input_as_dict
262
+ for k, v in o.items()
263
+ }
264
+ return o
265
+
266
+ @property
267
+ def table_row_dict(self):
268
+ """returns values as they would be put in a table row from this instance ignoring any sub components"""
269
+ from engforge.configuration import Configuration
270
+
271
+ o = {k: getattr(self, k, None) for k in self.table_fields()}
272
+ return o
273
+
274
+ @property
275
+ def numeric_as_dict(self):
276
+ """recursively gets internal components numeric_as_dict as well as its own numeric values"""
277
+ from engforge.configuration import Configuration
278
+
279
+ o = {k: getattr(self, k, None) for k in self.numeric_fields()}
280
+ o = {
281
+ k: v if not isinstance(v, Configuration) else v.numeric_as_dict
282
+ for k, v in o.items()
283
+ }
284
+ return o
285
+
286
+ # Hashes
287
+ # TODO: issue with logging sub-items
288
+ def hash(self, *args, **input_kw):
289
+ """hash by parm or by input_kw, only input can be hashed by lookup as system properties can create a recursive loop and should be deterministic from input"""
290
+ d = {k: v for k, v in self.input_as_dict.items() if k in args}
291
+ d.update(input_kw) # override with input_kw
292
+ return d, deepdiff.DeepHash(
293
+ d, ignore_encoding_errors=True, significant_digits=6
294
+ )
295
+
296
+ def hash_with(self, **input_kw):
297
+ """
298
+ Generates a hash for the object's dictionary representation, updated with additional keyword arguments.
299
+ Args:
300
+ **input_kw: Arbitrary keyword arguments to update the object's dictionary representation.
301
+ Returns:
302
+ The hash value of the updated dictionary.
303
+ Raises:
304
+ Any exceptions raised by deepdiff.DeepHash if hashing fails.
305
+ """
306
+ d = self.as_dict
307
+ d.update(input_kw)
308
+ return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d]
309
+
310
+ def hash_numeric_with(self, **input_kw):
311
+ d = self.numeric_as_dict
312
+ d.update(input_kw)
313
+ return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d]
314
+
315
+ @property
316
+ def unique_hash(self):
317
+ d = self.as_dict
318
+ return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d]
319
+
320
+ @property
321
+ def input_hash(self):
322
+ d = self.input_as_dict
323
+ return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d]
324
+
325
+ @property
326
+ def table_hash(self):
327
+ d, hsh = self.hash(**self.table_row_dict)
328
+ return hsh[d]
329
+
330
+ @property
331
+ def numeric_hash(self):
332
+ d = self.numeric_as_dict
333
+ return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d]
334
+
335
+ # Configuration Push/Pop methods
336
+ def setattrs(self, dict):
337
+ """sets attributes from a dictionary"""
338
+ msg = f"invalid keys {set(dict.keys()) - set(self.input_attrs())}"
339
+ assert set(dict.keys()).issubset(set(self.input_attrs())), msg
340
+ for k, v in dict.items():
341
+ setattr(self, k, v)
342
+
343
+ @contextmanager
344
+ def difference(self, **kwargs):
345
+ """a context manager that will allow you to dynamically change any information, then will change it back in a fail safe way.
346
+
347
+ with self.difference(name='new_name', value = new_value) as new_config:
348
+ #do stuff with config, ok to fail
349
+
350
+ you may not access any "private" variable that starts with an `_` as in _whatever
351
+
352
+ difference is useful for saving slight differences in configuration in conjunction with solve
353
+ you might create wrappers for eval, or implement a strategy pattern.
354
+
355
+ only attributes may be changed.
356
+
357
+ #TODO: allow recursive operation with sub comps or systems.
358
+ #TODO: make a full system copy so the system can be reverted later
359
+ """
360
+ _temp_vars = {}
361
+
362
+ _temp_vars.update(
363
+ {
364
+ arg: getattr(self, arg)
365
+ for arg in kwargs.keys()
366
+ if hasattr(self, arg)
367
+ if not arg.startswith("_")
368
+ }
369
+ )
370
+
371
+ bad_vars = set.difference(set(kwargs.keys()), set(_temp_vars.keys()))
372
+ if bad_vars:
373
+ self.warning("Could Not Change {}".format(",".join(list(bad_vars))))
374
+
375
+ try: # Change Variables To Input
376
+ self.setattrs(kwargs)
377
+ yield self
378
+ finally:
379
+ rstdict = {k: _temp_vars[k] for k, v in kwargs.items()}
380
+ self.setattrs(rstdict)