qnty 0.0.9__py3-none-any.whl → 0.1.1__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.
- qnty/__init__.py +2 -3
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +1 -1
- qnty/equations/equation.py +118 -155
- qnty/equations/system.py +68 -65
- qnty/expressions/__init__.py +25 -46
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +46 -68
- qnty/expressions/nodes.py +540 -384
- qnty/expressions/types.py +70 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1101 -0
- qnty/problems/problem.py +737 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +28 -5
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
- qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
- qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
- qnty/solving/manager.py +50 -44
- qnty/solving/order.py +181 -133
- qnty/solving/solvers/__init__.py +2 -9
- qnty/solving/solvers/base.py +27 -37
- qnty/solving/solvers/iterative.py +115 -135
- qnty/solving/solvers/simultaneous.py +93 -165
- qnty/units/__init__.py +1 -0
- qnty/{generated/units.py → units/field_units.py} +1700 -991
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +58 -105
- qnty/units/registry.py +76 -89
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +4 -4
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
- qnty-0.1.1.dist-info/RECORD +60 -0
- qnty/_backup/problem_original.py +0 -1251
- qnty/_backup/quantity.py +0 -63
- qnty/codegen/cli.py +0 -125
- qnty/codegen/generators/data/unit_data.json +0 -8807
- qnty/codegen/generators/data_processor.py +0 -345
- qnty/codegen/generators/dimensions_gen.py +0 -434
- qnty/codegen/generators/doc_generator.py +0 -141
- qnty/codegen/generators/out/dimension_mapping.json +0 -974
- qnty/codegen/generators/out/dimension_metadata.json +0 -123
- qnty/codegen/generators/out/units_metadata.json +0 -223
- qnty/codegen/generators/quantities_gen.py +0 -159
- qnty/codegen/generators/setters_gen.py +0 -178
- qnty/codegen/generators/stubs_gen.py +0 -167
- qnty/codegen/generators/units_gen.py +0 -295
- qnty/expressions/cache.py +0 -94
- qnty/generated/dimensions.py +0 -514
- qnty/problem/__init__.py +0 -91
- qnty/problem/base.py +0 -142
- qnty/problem/composition.py +0 -385
- qnty/problem/composition_mixin.py +0 -382
- qnty/problem/equations.py +0 -413
- qnty/problem/metaclass.py +0 -302
- qnty/problem/reconstruction.py +0 -1016
- qnty/problem/solving.py +0 -180
- qnty/problem/validation.py +0 -64
- qnty/problem/variables.py +0 -239
- qnty/quantities/expression_quantity.py +0 -314
- qnty/quantities/quantity.py +0 -428
- qnty/quantities/typed_quantity.py +0 -215
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +0 -167
- qnty-0.0.9.dist-info/RECORD +0 -63
- /qnty/{codegen → extensions}/__init__.py +0 -0
- /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
- /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
- /qnty/{generated → extensions/reporting}/__init__.py +0 -0
- {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1101 @@
|
|
1
|
+
"""
|
2
|
+
Sub-problem composition system for EngineeringProblem.
|
3
|
+
|
4
|
+
This module provides the complete sub-problem integration system including:
|
5
|
+
- Sub-problem proxy objects for clean composition syntax
|
6
|
+
- Namespace handling and variable mapping
|
7
|
+
- Composite equation creation
|
8
|
+
- Metaclass system for automatic proxy creation
|
9
|
+
|
10
|
+
Combined from composition.py and composition_mixin.py for focused functionality.
|
11
|
+
"""
|
12
|
+
|
13
|
+
from __future__ import annotations
|
14
|
+
|
15
|
+
from typing import Any
|
16
|
+
|
17
|
+
from ..equations import Equation
|
18
|
+
from ..expressions import BinaryOperation, ConditionalExpression, Constant, VariableReference, max_expr, min_expr, sin
|
19
|
+
from ..expressions.nodes import wrap_operand
|
20
|
+
from ..quantities import Dimensionless, FieldQnty
|
21
|
+
from .rules import Rules
|
22
|
+
|
23
|
+
# Constants for composition
|
24
|
+
MATHEMATICAL_OPERATORS = ["+", "-", "*", "/", " / ", " * ", " + ", " - "]
|
25
|
+
COMMON_COMPOSITE_VARIABLES = ["P", "c", "S", "E", "W", "Y"]
|
26
|
+
|
27
|
+
# Constants for metaclass
|
28
|
+
RESERVED_ATTRIBUTES: set[str] = {"name", "description"}
|
29
|
+
PRIVATE_ATTRIBUTE_PREFIX = "_"
|
30
|
+
SUB_PROBLEM_REQUIRED_ATTRIBUTES: tuple[str, ...] = ("variables", "equations")
|
31
|
+
|
32
|
+
|
33
|
+
# ========== COMPOSITION CLASSES ==========
|
34
|
+
|
35
|
+
|
36
|
+
class ArithmeticOperationsMixin:
|
37
|
+
"""Mixin providing common arithmetic operations that create DelayedExpression objects."""
|
38
|
+
|
39
|
+
def __add__(self, other):
|
40
|
+
return DelayedExpression("+", self, other)
|
41
|
+
|
42
|
+
def __radd__(self, other):
|
43
|
+
return DelayedExpression("+", other, self)
|
44
|
+
|
45
|
+
def __sub__(self, other):
|
46
|
+
return DelayedExpression("-", self, other)
|
47
|
+
|
48
|
+
def __rsub__(self, other):
|
49
|
+
return DelayedExpression("-", other, self)
|
50
|
+
|
51
|
+
def __mul__(self, other):
|
52
|
+
return DelayedExpression("*", self, other)
|
53
|
+
|
54
|
+
def __rmul__(self, other):
|
55
|
+
return DelayedExpression("*", other, self)
|
56
|
+
|
57
|
+
def __truediv__(self, other):
|
58
|
+
return DelayedExpression("/", self, other)
|
59
|
+
|
60
|
+
def __rtruediv__(self, other):
|
61
|
+
return DelayedExpression("/", other, self)
|
62
|
+
|
63
|
+
|
64
|
+
class DelayedEquation:
|
65
|
+
"""
|
66
|
+
Stores an equation definition that will be evaluated later when proper context is available.
|
67
|
+
"""
|
68
|
+
|
69
|
+
def __init__(self, lhs_symbol, rhs_factory, name=None):
|
70
|
+
self.lhs_symbol = lhs_symbol
|
71
|
+
self.rhs_factory = rhs_factory # Function that creates the RHS expression
|
72
|
+
self.name = name or f"{lhs_symbol}_equation"
|
73
|
+
|
74
|
+
def evaluate(self, context):
|
75
|
+
"""Evaluate the equation with the given context (namespace with variables)."""
|
76
|
+
if self.lhs_symbol not in context:
|
77
|
+
return None
|
78
|
+
|
79
|
+
lhs_var = context[self.lhs_symbol]
|
80
|
+
|
81
|
+
try:
|
82
|
+
# Call the factory function with the context to create the RHS
|
83
|
+
rhs_expr = self.rhs_factory(context)
|
84
|
+
return lhs_var.equals(rhs_expr)
|
85
|
+
except Exception:
|
86
|
+
return None
|
87
|
+
|
88
|
+
|
89
|
+
class SubProblemProxy:
|
90
|
+
"""
|
91
|
+
Proxy object that represents a sub-problem and provides namespaced variable access
|
92
|
+
during class definition. Returns properly namespaced variables immediately to prevent
|
93
|
+
malformed expressions.
|
94
|
+
"""
|
95
|
+
|
96
|
+
def __init__(self, sub_problem, namespace):
|
97
|
+
self._sub_problem = sub_problem
|
98
|
+
self._namespace = namespace
|
99
|
+
self._variable_cache = {}
|
100
|
+
self._variable_configurations = {} # Track configurations applied to variables
|
101
|
+
# Global registry to track which expressions involve proxy variables
|
102
|
+
if not hasattr(SubProblemProxy, "_expressions_with_proxies"):
|
103
|
+
SubProblemProxy._expressions_with_proxies = set()
|
104
|
+
|
105
|
+
def __getattr__(self, name):
|
106
|
+
# Handle internal Python attributes to prevent recursion during deepcopy
|
107
|
+
if name.startswith("_"):
|
108
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
109
|
+
|
110
|
+
if name in self._variable_cache:
|
111
|
+
return self._variable_cache[name]
|
112
|
+
|
113
|
+
try:
|
114
|
+
attr_value = getattr(self._sub_problem, name)
|
115
|
+
if isinstance(attr_value, FieldQnty):
|
116
|
+
# Create a properly namespaced variable immediately
|
117
|
+
namespaced_var = self._create_namespaced_variable(attr_value)
|
118
|
+
self._variable_cache[name] = namespaced_var
|
119
|
+
return namespaced_var
|
120
|
+
return attr_value
|
121
|
+
except AttributeError as e:
|
122
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") from e
|
123
|
+
|
124
|
+
def _create_namespaced_variable(self, original_var):
|
125
|
+
"""Create a Variable with namespaced symbol for proper expression creation."""
|
126
|
+
namespaced_symbol = f"{self._namespace}_{original_var.symbol}"
|
127
|
+
|
128
|
+
# Create new Variable with namespaced symbol that tracks modifications
|
129
|
+
namespaced_var = ConfigurableVariable(
|
130
|
+
symbol=namespaced_symbol,
|
131
|
+
name=f"{original_var.name} ({self._namespace.title()})",
|
132
|
+
quantity=original_var.quantity,
|
133
|
+
is_known=original_var.is_known,
|
134
|
+
proxy=self,
|
135
|
+
original_symbol=original_var.symbol,
|
136
|
+
original_variable=original_var, # Pass the original variable for type preservation
|
137
|
+
)
|
138
|
+
|
139
|
+
return namespaced_var
|
140
|
+
|
141
|
+
def track_configuration(self, original_symbol, quantity, is_known):
|
142
|
+
"""Track a configuration change made to a variable."""
|
143
|
+
self._variable_configurations[original_symbol] = {"quantity": quantity, "is_known": is_known}
|
144
|
+
|
145
|
+
def get_configurations(self):
|
146
|
+
"""Get all tracked configurations."""
|
147
|
+
return self._variable_configurations.copy()
|
148
|
+
|
149
|
+
|
150
|
+
class ConfigurableVariable:
|
151
|
+
"""
|
152
|
+
A Variable wrapper that can track configuration changes and report them back to its proxy.
|
153
|
+
This acts as a proxy around the actual qnty Variable rather than inheriting from it.
|
154
|
+
"""
|
155
|
+
|
156
|
+
def __init__(self, symbol, name, quantity, is_known=True, proxy=None, original_symbol=None, original_variable=None):
|
157
|
+
# Store the actual variable (we'll delegate to it)
|
158
|
+
# Create a variable of the appropriate type based on the original
|
159
|
+
if original_variable is not None:
|
160
|
+
# Preserve the original variable type
|
161
|
+
self._variable = type(original_variable)(name)
|
162
|
+
else:
|
163
|
+
# Fallback to Dimensionless if no original variable provided
|
164
|
+
self._variable = Dimensionless(name)
|
165
|
+
|
166
|
+
# Set the properties
|
167
|
+
self._variable.symbol = symbol
|
168
|
+
self._variable.quantity = quantity
|
169
|
+
self._variable.is_known = is_known
|
170
|
+
|
171
|
+
# Ensure the arithmetic mode is properly initialized
|
172
|
+
# The __init__ should have set it, but in case it didn't
|
173
|
+
if not hasattr(self._variable, '_arithmetic_mode'):
|
174
|
+
self._variable._arithmetic_mode = "expression"
|
175
|
+
|
176
|
+
# Store proxy information
|
177
|
+
self._proxy = proxy
|
178
|
+
self._original_symbol = original_symbol
|
179
|
+
|
180
|
+
def __getattr__(self, name):
|
181
|
+
"""Delegate all other attributes to the wrapped variable."""
|
182
|
+
# Handle special case for _arithmetic_mode since it's accessed in __mul__ etc.
|
183
|
+
if name == '_arithmetic_mode':
|
184
|
+
# First check if we have it directly
|
185
|
+
if hasattr(self._variable, '_arithmetic_mode'):
|
186
|
+
return self._variable._arithmetic_mode
|
187
|
+
# Otherwise default to 'expression'
|
188
|
+
return 'expression'
|
189
|
+
return getattr(self._variable, name)
|
190
|
+
|
191
|
+
# Delegate arithmetic operations to the wrapped variable
|
192
|
+
def __add__(self, other):
|
193
|
+
return self._variable.__add__(other)
|
194
|
+
|
195
|
+
def __radd__(self, other):
|
196
|
+
return self._variable.__radd__(other)
|
197
|
+
|
198
|
+
def __sub__(self, other):
|
199
|
+
return self._variable.__sub__(other)
|
200
|
+
|
201
|
+
def __rsub__(self, other):
|
202
|
+
return self._variable.__rsub__(other)
|
203
|
+
|
204
|
+
def __mul__(self, other):
|
205
|
+
return self._variable.__mul__(other)
|
206
|
+
|
207
|
+
def __rmul__(self, other):
|
208
|
+
return self._variable.__rmul__(other)
|
209
|
+
|
210
|
+
def __truediv__(self, other):
|
211
|
+
return self._variable.__truediv__(other)
|
212
|
+
|
213
|
+
def __rtruediv__(self, other):
|
214
|
+
return self._variable.__rtruediv__(other)
|
215
|
+
|
216
|
+
def __pow__(self, other):
|
217
|
+
return self._variable.__pow__(other)
|
218
|
+
|
219
|
+
def __neg__(self):
|
220
|
+
# Implement negation as multiplication by -1, consistent with other arithmetic operations
|
221
|
+
return self._variable * (-1)
|
222
|
+
|
223
|
+
# Comparison operations
|
224
|
+
def __lt__(self, other):
|
225
|
+
return self._variable.__lt__(other)
|
226
|
+
|
227
|
+
def __le__(self, other):
|
228
|
+
return self._variable.__le__(other)
|
229
|
+
|
230
|
+
def __gt__(self, other):
|
231
|
+
return self._variable.__gt__(other)
|
232
|
+
|
233
|
+
def __ge__(self, other):
|
234
|
+
return self._variable.__ge__(other)
|
235
|
+
|
236
|
+
def __eq__(self, other):
|
237
|
+
return self._variable.__eq__(other)
|
238
|
+
|
239
|
+
def __ne__(self, other):
|
240
|
+
return self._variable.__ne__(other)
|
241
|
+
|
242
|
+
def __setattr__(self, name, value):
|
243
|
+
"""Delegate attribute setting to the wrapped variable when appropriate."""
|
244
|
+
if name.startswith("_") or name in ("_variable", "_proxy", "_original_symbol"):
|
245
|
+
super().__setattr__(name, value)
|
246
|
+
else:
|
247
|
+
setattr(self._variable, name, value)
|
248
|
+
|
249
|
+
def set(self, value):
|
250
|
+
"""Override set method to track configuration changes and return the correct setter."""
|
251
|
+
# Create a wrapper setter that tracks changes after unit application
|
252
|
+
original_setter = self._variable.set(value)
|
253
|
+
|
254
|
+
if self._proxy and self._original_symbol:
|
255
|
+
# Create tracking wrapper
|
256
|
+
return TrackingSetterWrapper(original_setter, self._proxy, self._original_symbol, self._variable)
|
257
|
+
else:
|
258
|
+
return original_setter
|
259
|
+
|
260
|
+
|
261
|
+
class TrackingSetterWrapper:
|
262
|
+
"""
|
263
|
+
A wrapper around setter objects that tracks configuration changes after unit application.
|
264
|
+
This ensures that when `proxy.set(value).unit` is called, the proxy tracks the final
|
265
|
+
configuration after the unit is applied.
|
266
|
+
"""
|
267
|
+
|
268
|
+
def __init__(self, original_setter, proxy, original_symbol, variable):
|
269
|
+
self._original_setter = original_setter
|
270
|
+
self._proxy = proxy
|
271
|
+
self._original_symbol = original_symbol
|
272
|
+
self._variable = variable
|
273
|
+
|
274
|
+
def __getattr__(self, name):
|
275
|
+
"""
|
276
|
+
Intercept property access for unit properties and track configuration after application.
|
277
|
+
"""
|
278
|
+
# Get the property from the original setter
|
279
|
+
attr = getattr(self._original_setter, name)
|
280
|
+
|
281
|
+
# If it's a property (unit setter), wrap it to track configuration
|
282
|
+
if hasattr(attr, '__call__'):
|
283
|
+
# It's a method, just delegate
|
284
|
+
return attr
|
285
|
+
else:
|
286
|
+
# It's a property access, call it and then track configuration
|
287
|
+
result = attr # This will set the variable quantity and return the variable
|
288
|
+
|
289
|
+
# Track the configuration after the unit is applied
|
290
|
+
if self._proxy and self._original_symbol:
|
291
|
+
self._proxy.track_configuration(
|
292
|
+
self._original_symbol,
|
293
|
+
self._variable.quantity,
|
294
|
+
self._variable.is_known
|
295
|
+
)
|
296
|
+
|
297
|
+
return result
|
298
|
+
|
299
|
+
def with_unit(self, unit):
|
300
|
+
"""Delegate with_unit method and track configuration."""
|
301
|
+
result = self._original_setter.with_unit(unit)
|
302
|
+
if self._proxy and self._original_symbol:
|
303
|
+
self._proxy.track_configuration(
|
304
|
+
self._original_symbol,
|
305
|
+
self._variable.quantity,
|
306
|
+
self._variable.is_known
|
307
|
+
)
|
308
|
+
return result
|
309
|
+
|
310
|
+
def update(self, value=None, unit=None, quantity=None, is_known=None):
|
311
|
+
"""Override update method to track configuration changes."""
|
312
|
+
result = self._variable.update(value, unit, quantity, is_known)
|
313
|
+
if self._proxy and self._original_symbol:
|
314
|
+
# Track this configuration change
|
315
|
+
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
316
|
+
return result
|
317
|
+
|
318
|
+
def mark_known(self):
|
319
|
+
"""Override mark_known to track configuration changes."""
|
320
|
+
result = self._variable.mark_known()
|
321
|
+
if self._proxy and self._original_symbol:
|
322
|
+
# Track this configuration change
|
323
|
+
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
324
|
+
return result
|
325
|
+
|
326
|
+
def mark_unknown(self):
|
327
|
+
"""Override mark_unknown to track configuration changes."""
|
328
|
+
result = self._variable.mark_unknown()
|
329
|
+
if self._proxy and self._original_symbol:
|
330
|
+
# Track this configuration change
|
331
|
+
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
332
|
+
return result
|
333
|
+
|
334
|
+
|
335
|
+
class DelayedVariableReference(ArithmeticOperationsMixin):
|
336
|
+
"""
|
337
|
+
A placeholder for a variable that will be resolved to its namespaced version later.
|
338
|
+
Supports arithmetic operations that create delayed expressions.
|
339
|
+
"""
|
340
|
+
|
341
|
+
def __init__(self, namespace, symbol, original_var):
|
342
|
+
self.namespace = namespace
|
343
|
+
self.symbol = symbol
|
344
|
+
self.original_var = original_var
|
345
|
+
self._namespaced_symbol = f"{namespace}_{symbol}"
|
346
|
+
|
347
|
+
def resolve(self, context):
|
348
|
+
"""Resolve to the actual namespaced variable from context."""
|
349
|
+
return context.get(self._namespaced_symbol)
|
350
|
+
|
351
|
+
|
352
|
+
class DelayedExpression(ArithmeticOperationsMixin):
|
353
|
+
"""
|
354
|
+
Represents an arithmetic expression that will be resolved later when context is available.
|
355
|
+
Supports chaining of operations.
|
356
|
+
"""
|
357
|
+
|
358
|
+
def __init__(self, operation, left, right):
|
359
|
+
self.operation = operation
|
360
|
+
self.left = left
|
361
|
+
self.right = right
|
362
|
+
|
363
|
+
def resolve(self, context):
|
364
|
+
"""Resolve this expression to actual Variable/Expression objects."""
|
365
|
+
left_resolved = self._resolve_operand(self.left, context)
|
366
|
+
right_resolved = self._resolve_operand(self.right, context)
|
367
|
+
|
368
|
+
if left_resolved is None or right_resolved is None:
|
369
|
+
return None
|
370
|
+
|
371
|
+
# Create the actual expression
|
372
|
+
if self.operation == "+":
|
373
|
+
return left_resolved + right_resolved
|
374
|
+
elif self.operation == "-":
|
375
|
+
return left_resolved - right_resolved
|
376
|
+
elif self.operation == "*":
|
377
|
+
return left_resolved * right_resolved
|
378
|
+
elif self.operation == "/":
|
379
|
+
return left_resolved / right_resolved
|
380
|
+
else:
|
381
|
+
return BinaryOperation(self.operation, left_resolved, right_resolved)
|
382
|
+
|
383
|
+
def _resolve_operand(self, operand, context):
|
384
|
+
"""Resolve a single operand to a Variable/Expression."""
|
385
|
+
if isinstance(operand, DelayedVariableReference | DelayedExpression | DelayedFunction):
|
386
|
+
return operand.resolve(context)
|
387
|
+
else:
|
388
|
+
# It's a literal value or Variable
|
389
|
+
return operand
|
390
|
+
|
391
|
+
|
392
|
+
class DelayedFunction(ArithmeticOperationsMixin):
|
393
|
+
"""
|
394
|
+
Represents a function call that will be resolved later when context is available.
|
395
|
+
"""
|
396
|
+
|
397
|
+
def __init__(self, func_name, *args):
|
398
|
+
self.func_name = func_name
|
399
|
+
self.args = args
|
400
|
+
|
401
|
+
def resolve(self, context):
|
402
|
+
"""Resolve function call with given context."""
|
403
|
+
# Resolve all arguments
|
404
|
+
resolved_args = []
|
405
|
+
for arg in self.args:
|
406
|
+
if isinstance(arg, DelayedVariableReference | DelayedExpression | DelayedFunction):
|
407
|
+
resolved_arg = arg.resolve(context)
|
408
|
+
if resolved_arg is None:
|
409
|
+
return None
|
410
|
+
resolved_args.append(resolved_arg)
|
411
|
+
else:
|
412
|
+
resolved_args.append(arg)
|
413
|
+
|
414
|
+
# Call the appropriate function
|
415
|
+
if self.func_name == "sin":
|
416
|
+
return sin(resolved_args[0])
|
417
|
+
elif self.func_name == "min_expr":
|
418
|
+
return min_expr(*resolved_args)
|
419
|
+
elif self.func_name == "max_expr":
|
420
|
+
return max_expr(*resolved_args)
|
421
|
+
else:
|
422
|
+
# Generic function call
|
423
|
+
return None
|
424
|
+
|
425
|
+
|
426
|
+
# Delayed function factories
|
427
|
+
def delayed_sin(expr):
|
428
|
+
return DelayedFunction("sin", expr)
|
429
|
+
|
430
|
+
|
431
|
+
def delayed_min_expr(*args):
|
432
|
+
return DelayedFunction("min_expr", *args)
|
433
|
+
|
434
|
+
|
435
|
+
def delayed_max_expr(*args):
|
436
|
+
return DelayedFunction("max_expr", *args)
|
437
|
+
|
438
|
+
|
439
|
+
# ========== COMPOSITION MIXIN ==========
|
440
|
+
|
441
|
+
|
442
|
+
class CompositionMixin:
|
443
|
+
"""Mixin class providing sub-problem composition functionality."""
|
444
|
+
|
445
|
+
# These attributes/methods will be provided by other mixins in the final Problem class
|
446
|
+
variables: dict[str, FieldQnty]
|
447
|
+
sub_problems: dict[str, Any]
|
448
|
+
logger: Any
|
449
|
+
|
450
|
+
def add_variable(self, variable: FieldQnty) -> None:
|
451
|
+
"""Will be provided by Problem class."""
|
452
|
+
del variable # Unused in stub method
|
453
|
+
...
|
454
|
+
|
455
|
+
def add_equation(self, equation: Equation) -> None:
|
456
|
+
"""Will be provided by Problem class."""
|
457
|
+
del equation # Unused in stub method
|
458
|
+
...
|
459
|
+
|
460
|
+
def _clone_variable(self, variable: FieldQnty) -> FieldQnty:
|
461
|
+
"""Will be provided by Problem class."""
|
462
|
+
return variable # Stub method - return input as placeholder
|
463
|
+
|
464
|
+
def _recreate_validation_checks(self) -> None:
|
465
|
+
"""Will be provided by ValidationMixin."""
|
466
|
+
...
|
467
|
+
|
468
|
+
def _extract_from_class_variables(self):
|
469
|
+
"""Extract variables, equations, and sub-problems from class-level definitions."""
|
470
|
+
self._extract_sub_problems()
|
471
|
+
self._extract_direct_variables()
|
472
|
+
self._recreate_validation_checks()
|
473
|
+
self._create_composite_equations()
|
474
|
+
self._extract_equations()
|
475
|
+
|
476
|
+
def _extract_sub_problems(self):
|
477
|
+
"""Extract and integrate sub-problems from class-level definitions."""
|
478
|
+
if hasattr(self.__class__, "_original_sub_problems"):
|
479
|
+
original_sub_problems = getattr(self.__class__, "_original_sub_problems", {})
|
480
|
+
for attr_name, sub_problem in original_sub_problems.items():
|
481
|
+
self._integrate_sub_problem(sub_problem, attr_name)
|
482
|
+
|
483
|
+
def _extract_direct_variables(self):
|
484
|
+
"""Extract direct variables from class-level definitions."""
|
485
|
+
processed_symbols = set()
|
486
|
+
|
487
|
+
# Single pass through class attributes to collect variables
|
488
|
+
for attr_name, attr_value in self._get_class_attributes():
|
489
|
+
if isinstance(attr_value, FieldQnty):
|
490
|
+
# Set symbol based on attribute name (T_bar, P, etc.)
|
491
|
+
attr_value.symbol = attr_name
|
492
|
+
|
493
|
+
# Skip if we've already processed this symbol
|
494
|
+
if attr_value.symbol in processed_symbols:
|
495
|
+
continue
|
496
|
+
processed_symbols.add(attr_value.symbol)
|
497
|
+
|
498
|
+
# Clone variable to avoid shared state between instances
|
499
|
+
cloned_var = self._clone_variable(attr_value)
|
500
|
+
self.add_variable(cloned_var)
|
501
|
+
# Set the same cloned variable object as instance attribute
|
502
|
+
# Use super() to bypass our custom __setattr__ during initialization
|
503
|
+
super().__setattr__(attr_name, cloned_var)
|
504
|
+
|
505
|
+
def _extract_equations(self):
|
506
|
+
"""Extract and process equations from class-level definitions."""
|
507
|
+
equations_to_process = self._collect_class_equations()
|
508
|
+
|
509
|
+
for attr_name, equation in equations_to_process:
|
510
|
+
try:
|
511
|
+
# Add equation to the problem
|
512
|
+
self.add_equation(equation)
|
513
|
+
# Set the equation as an instance attribute
|
514
|
+
setattr(self, attr_name, equation)
|
515
|
+
except Exception as e:
|
516
|
+
# Log but continue - some equations might fail during class definition
|
517
|
+
self.logger.warning(f"Failed to process equation {attr_name}: {e}")
|
518
|
+
# Still set the original equation as attribute
|
519
|
+
setattr(self, attr_name, equation)
|
520
|
+
|
521
|
+
def _get_class_attributes(self) -> list[tuple[str, Any]]:
|
522
|
+
"""Get all non-private class attributes efficiently."""
|
523
|
+
return [(attr_name, getattr(self.__class__, attr_name)) for attr_name in dir(self.__class__) if not attr_name.startswith("_")]
|
524
|
+
|
525
|
+
def _collect_class_equations(self) -> list[tuple[str, Any]]:
|
526
|
+
"""Collect all equation objects from class attributes."""
|
527
|
+
equations_to_process = []
|
528
|
+
for attr_name, attr_value in self._get_class_attributes():
|
529
|
+
if isinstance(attr_value, Equation):
|
530
|
+
equations_to_process.append((attr_name, attr_value))
|
531
|
+
return equations_to_process
|
532
|
+
|
533
|
+
def _integrate_sub_problem(self, sub_problem, namespace: str) -> None:
|
534
|
+
"""
|
535
|
+
Integrate a sub-problem by flattening its variables with namespace prefixes.
|
536
|
+
Creates a simple dotted access pattern: self.header.P becomes self.header_P
|
537
|
+
"""
|
538
|
+
self.sub_problems[namespace] = sub_problem
|
539
|
+
proxy_configs = getattr(self.__class__, "_proxy_configurations", {}).get(namespace, {})
|
540
|
+
|
541
|
+
namespace_obj = self._create_namespace_object(sub_problem, namespace, proxy_configs)
|
542
|
+
super().__setattr__(namespace, namespace_obj)
|
543
|
+
|
544
|
+
self._integrate_sub_problem_equations(sub_problem, namespace)
|
545
|
+
|
546
|
+
def _create_namespace_object(self, sub_problem, namespace: str, proxy_configs: dict):
|
547
|
+
"""Create namespace object with all sub-problem variables."""
|
548
|
+
namespace_obj = type("SubProblemNamespace", (), {})()
|
549
|
+
|
550
|
+
for var_symbol, var in sub_problem.variables.items():
|
551
|
+
namespaced_var = self._create_namespaced_variable(var, var_symbol, namespace, proxy_configs)
|
552
|
+
self.add_variable(namespaced_var)
|
553
|
+
|
554
|
+
# Set both namespaced access (self.header_P) and dotted access (self.header.P)
|
555
|
+
if namespaced_var.symbol is not None:
|
556
|
+
super().__setattr__(namespaced_var.symbol, namespaced_var)
|
557
|
+
setattr(namespace_obj, var_symbol, namespaced_var)
|
558
|
+
|
559
|
+
return namespace_obj
|
560
|
+
|
561
|
+
def _integrate_sub_problem_equations(self, sub_problem, namespace: str):
|
562
|
+
"""Integrate equations from sub-problem with proper namespacing."""
|
563
|
+
for equation in sub_problem.equations:
|
564
|
+
try:
|
565
|
+
# Skip conditional equations for variables that are overridden to known values in composition
|
566
|
+
if self._should_skip_subproblem_equation(equation, namespace):
|
567
|
+
continue
|
568
|
+
|
569
|
+
namespaced_equation = self._namespace_equation(equation, namespace)
|
570
|
+
if namespaced_equation:
|
571
|
+
self.add_equation(namespaced_equation)
|
572
|
+
except Exception as e:
|
573
|
+
self.logger.debug(f"Failed to namespace equation from {namespace}: {e}")
|
574
|
+
|
575
|
+
def _create_namespaced_variable(self, var: FieldQnty, var_symbol: str, namespace: str, proxy_configs: dict) -> FieldQnty:
|
576
|
+
"""Create a namespaced variable with proper configuration."""
|
577
|
+
namespaced_symbol = f"{namespace}_{var_symbol}"
|
578
|
+
namespaced_var = self._clone_variable(var)
|
579
|
+
namespaced_var.symbol = namespaced_symbol
|
580
|
+
namespaced_var.name = f"{var.name} ({namespace.title()})"
|
581
|
+
|
582
|
+
# Apply proxy configuration if available
|
583
|
+
if var_symbol in proxy_configs:
|
584
|
+
config = proxy_configs[var_symbol]
|
585
|
+
namespaced_var.quantity = config["quantity"]
|
586
|
+
namespaced_var.is_known = config["is_known"]
|
587
|
+
|
588
|
+
return namespaced_var
|
589
|
+
|
590
|
+
def _namespace_equation(self, equation, namespace: str):
|
591
|
+
"""
|
592
|
+
Create a namespaced version of an equation by prefixing all variable references.
|
593
|
+
"""
|
594
|
+
try:
|
595
|
+
# Get all variable symbols in the equation
|
596
|
+
variables_in_eq = equation.get_all_variables()
|
597
|
+
|
598
|
+
# Create mapping from original symbols to namespaced symbols
|
599
|
+
symbol_mapping = self._create_symbol_mapping(variables_in_eq, namespace)
|
600
|
+
if not symbol_mapping:
|
601
|
+
return None
|
602
|
+
|
603
|
+
# Create new equation with namespaced references
|
604
|
+
return self._create_namespaced_equation(equation, symbol_mapping)
|
605
|
+
|
606
|
+
except Exception:
|
607
|
+
return None
|
608
|
+
|
609
|
+
def _create_symbol_mapping(self, variables_in_eq: set[str], namespace: str) -> dict[str, str]:
|
610
|
+
"""Create mapping from original symbols to namespaced symbols."""
|
611
|
+
symbol_mapping = {}
|
612
|
+
for var_symbol in variables_in_eq:
|
613
|
+
namespaced_symbol = f"{namespace}_{var_symbol}"
|
614
|
+
if namespaced_symbol in self.variables:
|
615
|
+
symbol_mapping[var_symbol] = namespaced_symbol
|
616
|
+
return symbol_mapping
|
617
|
+
|
618
|
+
def _create_namespaced_equation(self, equation: Equation, symbol_mapping: dict[str, str]) -> Equation | None:
|
619
|
+
"""Create new equation with namespaced references."""
|
620
|
+
# For LHS, we need a Variable object to call .equals()
|
621
|
+
# For RHS, we need proper expression structure
|
622
|
+
namespaced_lhs = self._namespace_expression_for_lhs(equation.lhs, symbol_mapping)
|
623
|
+
namespaced_rhs = self._namespace_expression(equation.rhs, symbol_mapping)
|
624
|
+
|
625
|
+
if namespaced_lhs and namespaced_rhs and hasattr(namespaced_lhs, "equals"):
|
626
|
+
return namespaced_lhs.equals(namespaced_rhs)
|
627
|
+
return None
|
628
|
+
|
629
|
+
def _namespace_expression(self, expr, symbol_mapping):
|
630
|
+
"""
|
631
|
+
Create a namespaced version of an expression by replacing variable references.
|
632
|
+
"""
|
633
|
+
# Handle variable references
|
634
|
+
if isinstance(expr, VariableReference):
|
635
|
+
return self._namespace_variable_reference(expr, symbol_mapping)
|
636
|
+
elif isinstance(expr, FieldQnty) and expr.symbol in symbol_mapping:
|
637
|
+
return self._namespace_variable_object(expr, symbol_mapping)
|
638
|
+
|
639
|
+
# Handle operations
|
640
|
+
elif isinstance(expr, BinaryOperation):
|
641
|
+
return self._namespace_binary_operation(expr, symbol_mapping)
|
642
|
+
elif isinstance(expr, ConditionalExpression):
|
643
|
+
return self._namespace_conditional_expression(expr, symbol_mapping)
|
644
|
+
elif self._is_unary_operation(expr):
|
645
|
+
return self._namespace_unary_operation(expr, symbol_mapping)
|
646
|
+
elif self._is_binary_function(expr):
|
647
|
+
return self._namespace_binary_function(expr, symbol_mapping)
|
648
|
+
elif isinstance(expr, Constant):
|
649
|
+
return expr
|
650
|
+
else:
|
651
|
+
return expr
|
652
|
+
|
653
|
+
def _namespace_variable_reference(self, expr: VariableReference, symbol_mapping: dict[str, str]) -> VariableReference:
|
654
|
+
"""Namespace a VariableReference object."""
|
655
|
+
# VariableReference uses the 'name' property which returns the symbol if available
|
656
|
+
symbol = expr.name
|
657
|
+
if symbol in symbol_mapping:
|
658
|
+
namespaced_symbol = symbol_mapping[symbol]
|
659
|
+
if namespaced_symbol in self.variables:
|
660
|
+
return VariableReference(self.variables[namespaced_symbol])
|
661
|
+
return expr
|
662
|
+
|
663
|
+
def _namespace_variable_object(self, expr: FieldQnty, symbol_mapping: dict[str, str]) -> VariableReference | FieldQnty:
|
664
|
+
"""Namespace a Variable object."""
|
665
|
+
if expr.symbol is None:
|
666
|
+
return expr
|
667
|
+
namespaced_symbol = symbol_mapping[expr.symbol]
|
668
|
+
if namespaced_symbol in self.variables:
|
669
|
+
# Return VariableReference for use in expressions, not the Variable itself
|
670
|
+
return VariableReference(self.variables[namespaced_symbol])
|
671
|
+
return expr
|
672
|
+
|
673
|
+
def _namespace_binary_operation(self, expr: BinaryOperation, symbol_mapping: dict[str, str]) -> BinaryOperation:
|
674
|
+
"""Namespace a BinaryOperation."""
|
675
|
+
namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
|
676
|
+
namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
|
677
|
+
return BinaryOperation(expr.operator, wrap_operand(namespaced_left), wrap_operand(namespaced_right))
|
678
|
+
|
679
|
+
def _namespace_unary_operation(self, expr, symbol_mapping):
|
680
|
+
"""Namespace a UnaryFunction."""
|
681
|
+
namespaced_operand = self._namespace_expression(expr.operand, symbol_mapping)
|
682
|
+
return type(expr)(expr.operator, namespaced_operand)
|
683
|
+
|
684
|
+
def _namespace_binary_function(self, expr, symbol_mapping):
|
685
|
+
"""Namespace a BinaryFunction."""
|
686
|
+
namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
|
687
|
+
namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
|
688
|
+
return type(expr)(expr.function_name, namespaced_left, namespaced_right)
|
689
|
+
|
690
|
+
def _namespace_conditional_expression(self, expr: ConditionalExpression, symbol_mapping: dict[str, str]) -> ConditionalExpression:
|
691
|
+
"""Namespace a ConditionalExpression."""
|
692
|
+
namespaced_condition = self._namespace_expression(expr.condition, symbol_mapping)
|
693
|
+
namespaced_true_expr = self._namespace_expression(expr.true_expr, symbol_mapping)
|
694
|
+
namespaced_false_expr = self._namespace_expression(expr.false_expr, symbol_mapping)
|
695
|
+
|
696
|
+
return ConditionalExpression(wrap_operand(namespaced_condition), wrap_operand(namespaced_true_expr), wrap_operand(namespaced_false_expr))
|
697
|
+
|
698
|
+
def _namespace_expression_for_lhs(self, expr, symbol_mapping: dict[str, str]) -> FieldQnty | None:
|
699
|
+
"""
|
700
|
+
Create a namespaced version of an expression for LHS, returning Variable objects.
|
701
|
+
"""
|
702
|
+
if isinstance(expr, VariableReference):
|
703
|
+
# VariableReference uses the 'name' property which returns the symbol if available
|
704
|
+
symbol = expr.name
|
705
|
+
if symbol and symbol in symbol_mapping:
|
706
|
+
namespaced_symbol = symbol_mapping[symbol]
|
707
|
+
if namespaced_symbol in self.variables:
|
708
|
+
return self.variables[namespaced_symbol]
|
709
|
+
# If we can't find a mapping, return None since VariableReference doesn't have .equals()
|
710
|
+
return None
|
711
|
+
elif isinstance(expr, FieldQnty) and expr.symbol in symbol_mapping:
|
712
|
+
# This is a Variable object
|
713
|
+
namespaced_symbol = symbol_mapping[expr.symbol]
|
714
|
+
if namespaced_symbol in self.variables:
|
715
|
+
return self.variables[namespaced_symbol]
|
716
|
+
return expr
|
717
|
+
else:
|
718
|
+
return expr
|
719
|
+
|
720
|
+
def _is_unary_operation(self, expr) -> bool:
|
721
|
+
"""Check if expression is a unary operation."""
|
722
|
+
# UnaryFunction and similar classes have an 'operand' attribute
|
723
|
+
return hasattr(expr, "operand") and hasattr(expr, "operator")
|
724
|
+
|
725
|
+
def _is_binary_function(self, expr) -> bool:
|
726
|
+
"""Check if expression is a binary function."""
|
727
|
+
# BinaryFunction classes have 'function_name', 'left', and 'right' attributes
|
728
|
+
return hasattr(expr, "function_name") and hasattr(expr, "left") and hasattr(expr, "right")
|
729
|
+
|
730
|
+
def _should_skip_subproblem_equation(self, equation, namespace: str) -> bool:
|
731
|
+
"""
|
732
|
+
Check if an equation from a sub-problem should be skipped during integration.
|
733
|
+
|
734
|
+
Skip conditional equations for variables that are set to known values in the composed problem.
|
735
|
+
"""
|
736
|
+
try:
|
737
|
+
# Check if this is a conditional equation
|
738
|
+
if not self._is_conditional_equation(equation):
|
739
|
+
return False
|
740
|
+
|
741
|
+
# Check if the LHS variable would be set to a known value in composition
|
742
|
+
original_symbol = self._get_equation_lhs_symbol(equation)
|
743
|
+
if original_symbol is not None:
|
744
|
+
namespaced_symbol = f"{namespace}_{original_symbol}"
|
745
|
+
|
746
|
+
# Check if this namespaced variable exists and is already known
|
747
|
+
if namespaced_symbol in self.variables:
|
748
|
+
var = self.variables[namespaced_symbol]
|
749
|
+
if var.is_known:
|
750
|
+
# The variable is already set to a known value in composition,
|
751
|
+
# so skip the conditional equation that would override it
|
752
|
+
self.logger.debug(f"Skipping conditional equation for {namespaced_symbol} (already known: {var.quantity})")
|
753
|
+
return True
|
754
|
+
|
755
|
+
return False
|
756
|
+
|
757
|
+
except Exception:
|
758
|
+
return False
|
759
|
+
|
760
|
+
def _create_composite_equations(self):
|
761
|
+
"""
|
762
|
+
Create composite equations for common patterns in sub-problems.
|
763
|
+
This handles equations like P = min(header.P, branch.P) automatically.
|
764
|
+
"""
|
765
|
+
if not self.sub_problems:
|
766
|
+
return
|
767
|
+
|
768
|
+
# Common composite patterns to auto-generate
|
769
|
+
for var_name in COMMON_COMPOSITE_VARIABLES:
|
770
|
+
# Check if this variable exists in multiple sub-problems
|
771
|
+
sub_problem_vars = []
|
772
|
+
for namespace in self.sub_problems:
|
773
|
+
namespaced_symbol = f"{namespace}_{var_name}"
|
774
|
+
if namespaced_symbol in self.variables:
|
775
|
+
sub_problem_vars.append(self.variables[namespaced_symbol])
|
776
|
+
|
777
|
+
# If we have the variable in multiple sub-problems and no direct variable exists
|
778
|
+
if len(sub_problem_vars) >= 2 and var_name in self.variables:
|
779
|
+
# Check if a composite equation already exists
|
780
|
+
equation_attr_name = f"{var_name}_eqn"
|
781
|
+
if hasattr(self.__class__, equation_attr_name):
|
782
|
+
# Skip auto-creation since explicit equation exists
|
783
|
+
continue
|
784
|
+
|
785
|
+
# Auto-create composite equation
|
786
|
+
try:
|
787
|
+
composite_var = self.variables[var_name]
|
788
|
+
if not composite_var.is_known: # Only for unknown variables
|
789
|
+
composite_expr = min_expr(*sub_problem_vars)
|
790
|
+
equals_method = getattr(composite_var, "equals", None)
|
791
|
+
if equals_method:
|
792
|
+
composite_eq = equals_method(composite_expr)
|
793
|
+
self.add_equation(composite_eq)
|
794
|
+
setattr(self, f"{var_name}_eqn", composite_eq)
|
795
|
+
except Exception as e:
|
796
|
+
self.logger.debug(f"Failed to create composite equation for {var_name}: {e}")
|
797
|
+
|
798
|
+
# Placeholder methods that will be provided by Problem class
|
799
|
+
def _process_equation(self, attr_name: str, equation: Equation) -> bool:
|
800
|
+
"""Will be provided by Problem class."""
|
801
|
+
del attr_name, equation # Unused in stub method
|
802
|
+
return True
|
803
|
+
|
804
|
+
def _is_conditional_equation(self, _equation: Equation) -> bool:
|
805
|
+
"""Will be provided by Problem class."""
|
806
|
+
return "cond(" in str(_equation)
|
807
|
+
|
808
|
+
def _get_equation_lhs_symbol(self, equation: Equation) -> str | None:
|
809
|
+
"""Will be provided by Problem class."""
|
810
|
+
return getattr(equation.lhs, "symbol", None)
|
811
|
+
|
812
|
+
|
813
|
+
# ========== METACLASS SYSTEM ==========
|
814
|
+
|
815
|
+
|
816
|
+
# Custom exceptions for better error handling
|
817
|
+
class MetaclassError(Exception):
|
818
|
+
"""Base exception for metaclass-related errors."""
|
819
|
+
|
820
|
+
pass
|
821
|
+
|
822
|
+
|
823
|
+
class SubProblemProxyError(MetaclassError):
|
824
|
+
"""Raised when sub-problem proxy creation fails."""
|
825
|
+
|
826
|
+
pass
|
827
|
+
|
828
|
+
|
829
|
+
class NamespaceError(MetaclassError):
|
830
|
+
"""Raised when namespace operations fail."""
|
831
|
+
|
832
|
+
pass
|
833
|
+
|
834
|
+
|
835
|
+
class ProblemMeta(type):
|
836
|
+
"""
|
837
|
+
Metaclass that processes class-level sub-problems to create proper namespace proxies
|
838
|
+
BEFORE any equations are evaluated.
|
839
|
+
|
840
|
+
This metaclass enables clean composition syntax like:
|
841
|
+
class MyProblem(EngineeringProblem):
|
842
|
+
header = create_pipe_problem()
|
843
|
+
branch = create_pipe_problem()
|
844
|
+
# Equations can reference header.P, branch.T, etc.
|
845
|
+
"""
|
846
|
+
|
847
|
+
# Declare the attributes that will be dynamically added to created classes
|
848
|
+
_original_sub_problems: dict[str, Any]
|
849
|
+
_proxy_configurations: dict[str, dict[str, Any]]
|
850
|
+
_class_checks: dict[str, Any]
|
851
|
+
|
852
|
+
@classmethod
|
853
|
+
def __prepare__(mcs, *args, **kwargs) -> ProxiedNamespace:
|
854
|
+
"""
|
855
|
+
Called before the class body is evaluated.
|
856
|
+
Returns a custom namespace that proxies sub-problems.
|
857
|
+
|
858
|
+
Args:
|
859
|
+
*args: Positional arguments (name, bases) - unused but required by protocol
|
860
|
+
**kwargs: Additional keyword arguments - unused but required by protocol
|
861
|
+
|
862
|
+
Returns:
|
863
|
+
ProxiedNamespace that will handle sub-problem proxying
|
864
|
+
"""
|
865
|
+
# Parameters are required by metaclass protocol but not used in this implementation
|
866
|
+
del args, kwargs # Explicitly acknowledge unused parameters
|
867
|
+
return ProxiedNamespace()
|
868
|
+
|
869
|
+
def __new__(mcs, name: str, bases: tuple[type, ...], namespace: ProxiedNamespace, **kwargs) -> type:
|
870
|
+
"""
|
871
|
+
Create the new class with properly integrated sub-problems.
|
872
|
+
|
873
|
+
Args:
|
874
|
+
name: Name of the class being created
|
875
|
+
bases: Base classes
|
876
|
+
namespace: The ProxiedNamespace containing proxied sub-problems
|
877
|
+
**kwargs: Additional keyword arguments - unused but required by protocol
|
878
|
+
|
879
|
+
Returns:
|
880
|
+
The newly created class with metaclass attributes
|
881
|
+
|
882
|
+
Raises:
|
883
|
+
MetaclassError: If class creation fails due to metaclass issues
|
884
|
+
"""
|
885
|
+
# kwargs is required by metaclass protocol but not used in this implementation
|
886
|
+
del kwargs # Explicitly acknowledge unused parameter
|
887
|
+
try:
|
888
|
+
# Validate the namespace
|
889
|
+
if not isinstance(namespace, ProxiedNamespace):
|
890
|
+
raise MetaclassError(f"Expected ProxiedNamespace, got {type(namespace)}")
|
891
|
+
|
892
|
+
# Extract the original sub-problems and proxy objects from the namespace
|
893
|
+
sub_problem_proxies = getattr(namespace, "_sub_problem_proxies", {})
|
894
|
+
proxy_objects = getattr(namespace, "_proxy_objects", {})
|
895
|
+
|
896
|
+
# Validate that proxy objects are consistent
|
897
|
+
if set(sub_problem_proxies.keys()) != set(proxy_objects.keys()):
|
898
|
+
raise MetaclassError("Inconsistent proxy state: sub-problem and proxy object keys don't match")
|
899
|
+
|
900
|
+
# Create the class normally
|
901
|
+
cls = super().__new__(mcs, name, bases, dict(namespace))
|
902
|
+
|
903
|
+
# Store the original sub-problems and proxy configurations for later integration
|
904
|
+
cls._original_sub_problems = sub_problem_proxies
|
905
|
+
|
906
|
+
# Extract configurations safely with error handling
|
907
|
+
proxy_configurations = {}
|
908
|
+
for proxy_name, proxy in proxy_objects.items():
|
909
|
+
try:
|
910
|
+
# Cache configurations to avoid recomputation
|
911
|
+
if not hasattr(proxy, "_cached_configurations"):
|
912
|
+
proxy._cached_configurations = proxy.get_configurations()
|
913
|
+
proxy_configurations[proxy_name] = proxy._cached_configurations
|
914
|
+
except Exception as e:
|
915
|
+
raise SubProblemProxyError(f"Failed to get configurations from proxy '{proxy_name}': {e}") from e
|
916
|
+
|
917
|
+
cls._proxy_configurations = proxy_configurations
|
918
|
+
|
919
|
+
# Collect Check objects from class attributes
|
920
|
+
checks = {}
|
921
|
+
for attr_name, attr_value in namespace.items():
|
922
|
+
if isinstance(attr_value, Rules):
|
923
|
+
checks[attr_name] = attr_value
|
924
|
+
|
925
|
+
cls._class_checks = checks
|
926
|
+
|
927
|
+
return cls
|
928
|
+
|
929
|
+
except Exception as e:
|
930
|
+
# Re-raise MetaclassError and SubProblemProxyError as-is
|
931
|
+
if isinstance(e, MetaclassError | SubProblemProxyError):
|
932
|
+
raise
|
933
|
+
# Wrap other exceptions
|
934
|
+
raise MetaclassError(f"Failed to create class '{name}': {e}") from e
|
935
|
+
|
936
|
+
|
937
|
+
class ProxiedNamespace(dict):
|
938
|
+
"""
|
939
|
+
Custom namespace that automatically proxies sub-problems as they're added.
|
940
|
+
|
941
|
+
This namespace intercepts class attribute assignments during class creation
|
942
|
+
and automatically wraps EngineeringProblem objects in SubProblemProxy objects.
|
943
|
+
This enables clean composition syntax where sub-problems can be referenced
|
944
|
+
with dot notation in equations.
|
945
|
+
|
946
|
+
Example:
|
947
|
+
class ComposedProblem(EngineeringProblem):
|
948
|
+
header = create_pipe_problem() # Gets proxied automatically
|
949
|
+
branch = create_pipe_problem() # Gets proxied automatically
|
950
|
+
# Now equations can use header.P, branch.T, etc.
|
951
|
+
"""
|
952
|
+
|
953
|
+
def __init__(self) -> None:
|
954
|
+
"""Initialize the proxied namespace with empty storage."""
|
955
|
+
super().__init__()
|
956
|
+
self._sub_problem_proxies: dict[str, Any] = {}
|
957
|
+
self._proxy_objects: dict[str, SubProblemProxy] = {}
|
958
|
+
|
959
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
960
|
+
"""
|
961
|
+
Intercept attribute assignment and proxy sub-problems automatically.
|
962
|
+
|
963
|
+
Args:
|
964
|
+
key: The attribute name being set
|
965
|
+
value: The value being assigned
|
966
|
+
|
967
|
+
Raises:
|
968
|
+
NamespaceError: If namespace operation fails
|
969
|
+
SubProblemProxyError: If proxy creation fails
|
970
|
+
"""
|
971
|
+
try:
|
972
|
+
if self._is_sub_problem(key, value):
|
973
|
+
self._create_and_store_proxy(key, value)
|
974
|
+
elif self._is_variable_with_auto_symbol(value):
|
975
|
+
self._set_variable_symbol_and_store(key, value)
|
976
|
+
else:
|
977
|
+
super().__setitem__(key, value)
|
978
|
+
except Exception as e:
|
979
|
+
if isinstance(e, NamespaceError | SubProblemProxyError):
|
980
|
+
raise
|
981
|
+
raise NamespaceError(f"Failed to set attribute '{key}': {e}") from e
|
982
|
+
|
983
|
+
def _is_sub_problem(self, key: str, value: Any) -> bool:
|
984
|
+
"""
|
985
|
+
Determine if a value should be treated as a sub-problem.
|
986
|
+
|
987
|
+
Args:
|
988
|
+
key: The attribute name
|
989
|
+
value: The value being assigned
|
990
|
+
|
991
|
+
Returns:
|
992
|
+
True if this should be proxied as a sub-problem
|
993
|
+
"""
|
994
|
+
# Quick checks first (fail fast)
|
995
|
+
if key.startswith(PRIVATE_ATTRIBUTE_PREFIX) or key in RESERVED_ATTRIBUTES:
|
996
|
+
return False
|
997
|
+
|
998
|
+
# Check for None or basic types that definitely aren't sub-problems
|
999
|
+
if value is None or isinstance(value, str | int | float | bool | list | dict):
|
1000
|
+
return False
|
1001
|
+
|
1002
|
+
# Cache hasattr results to avoid repeated attribute lookups
|
1003
|
+
if not hasattr(self, "_attr_cache"):
|
1004
|
+
self._attr_cache = {}
|
1005
|
+
|
1006
|
+
# Use object id as cache key since objects are unique
|
1007
|
+
cache_key = (id(value), tuple(SUB_PROBLEM_REQUIRED_ATTRIBUTES))
|
1008
|
+
if cache_key not in self._attr_cache:
|
1009
|
+
self._attr_cache[cache_key] = all(hasattr(value, attr) for attr in SUB_PROBLEM_REQUIRED_ATTRIBUTES)
|
1010
|
+
|
1011
|
+
return self._attr_cache[cache_key]
|
1012
|
+
|
1013
|
+
def _is_variable_with_auto_symbol(self, value: Any) -> bool:
|
1014
|
+
"""
|
1015
|
+
Determine if a value is a Variable that needs automatic symbol assignment.
|
1016
|
+
|
1017
|
+
Args:
|
1018
|
+
value: The value being assigned
|
1019
|
+
|
1020
|
+
Returns:
|
1021
|
+
True if this is a Variable that needs automatic symbol assignment
|
1022
|
+
"""
|
1023
|
+
# Import Variable here to avoid circular imports
|
1024
|
+
try:
|
1025
|
+
if not isinstance(value, FieldQnty):
|
1026
|
+
return False
|
1027
|
+
# Auto-assign symbol if:
|
1028
|
+
# 1. Symbol is explicitly "<auto>", OR
|
1029
|
+
# 2. Symbol equals the variable name (default behavior, no explicit symbol set)
|
1030
|
+
return value.symbol == "<auto>" or value.symbol == value.name
|
1031
|
+
except ImportError:
|
1032
|
+
return False
|
1033
|
+
|
1034
|
+
def _set_variable_symbol_and_store(self, key: str, value: Any) -> None:
|
1035
|
+
"""
|
1036
|
+
Set the variable's symbol to the attribute name and store it.
|
1037
|
+
|
1038
|
+
Args:
|
1039
|
+
key: The attribute name to use as symbol
|
1040
|
+
value: The Variable object
|
1041
|
+
"""
|
1042
|
+
try:
|
1043
|
+
# Set the symbol to the attribute name
|
1044
|
+
value.symbol = key
|
1045
|
+
# Store the modified variable
|
1046
|
+
super().__setitem__(key, value)
|
1047
|
+
except Exception as e:
|
1048
|
+
raise NamespaceError(f"Failed to set symbol for variable '{key}': {e}") from e
|
1049
|
+
|
1050
|
+
def _create_and_store_proxy(self, key: str, value: Any) -> None:
|
1051
|
+
"""
|
1052
|
+
Create a proxy for the sub-problem and store references.
|
1053
|
+
|
1054
|
+
Args:
|
1055
|
+
key: The attribute name for the sub-problem
|
1056
|
+
value: The sub-problem object to proxy
|
1057
|
+
|
1058
|
+
Raises:
|
1059
|
+
SubProblemProxyError: If proxy creation fails
|
1060
|
+
NamespaceError: If key already exists as a sub-problem
|
1061
|
+
"""
|
1062
|
+
# Check for conflicts
|
1063
|
+
if key in self._sub_problem_proxies:
|
1064
|
+
raise NamespaceError(f"Sub-problem '{key}' already exists in namespace")
|
1065
|
+
|
1066
|
+
try:
|
1067
|
+
# Store the original sub-problem
|
1068
|
+
self._sub_problem_proxies[key] = value
|
1069
|
+
|
1070
|
+
# Create and store the proxy
|
1071
|
+
proxy = SubProblemProxy(value, key)
|
1072
|
+
self._proxy_objects[key] = proxy
|
1073
|
+
|
1074
|
+
# Set the proxy in the namespace
|
1075
|
+
super().__setitem__(key, proxy)
|
1076
|
+
|
1077
|
+
except Exception as e:
|
1078
|
+
# Clean up partial state on failure
|
1079
|
+
self._sub_problem_proxies.pop(key, None)
|
1080
|
+
self._proxy_objects.pop(key, None)
|
1081
|
+
raise SubProblemProxyError(f"Failed to create proxy for sub-problem '{key}': {e}") from e
|
1082
|
+
|
1083
|
+
|
1084
|
+
# Export all relevant classes
|
1085
|
+
__all__ = [
|
1086
|
+
"CompositionMixin",
|
1087
|
+
"ProblemMeta",
|
1088
|
+
"ProxiedNamespace",
|
1089
|
+
"SubProblemProxy",
|
1090
|
+
"ConfigurableVariable",
|
1091
|
+
"DelayedEquation",
|
1092
|
+
"DelayedVariableReference",
|
1093
|
+
"DelayedExpression",
|
1094
|
+
"DelayedFunction",
|
1095
|
+
"delayed_sin",
|
1096
|
+
"delayed_min_expr",
|
1097
|
+
"delayed_max_expr",
|
1098
|
+
"MetaclassError",
|
1099
|
+
"SubProblemProxyError",
|
1100
|
+
"NamespaceError",
|
1101
|
+
]
|