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.
Files changed (92) hide show
  1. qnty/__init__.py +2 -3
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +540 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1101 -0
  21. qnty/problems/problem.py +737 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
  54. qnty-0.1.1.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {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
+ ]