qnty 0.0.7__py3-none-any.whl → 0.0.9__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 (76) hide show
  1. qnty/__init__.py +140 -58
  2. qnty/_backup/problem_original.py +1251 -0
  3. qnty/_backup/quantity.py +63 -0
  4. qnty/codegen/cli.py +125 -0
  5. qnty/codegen/generators/data/unit_data.json +8807 -0
  6. qnty/codegen/generators/data_processor.py +345 -0
  7. qnty/codegen/generators/dimensions_gen.py +434 -0
  8. qnty/codegen/generators/doc_generator.py +141 -0
  9. qnty/codegen/generators/out/dimension_mapping.json +974 -0
  10. qnty/codegen/generators/out/dimension_metadata.json +123 -0
  11. qnty/codegen/generators/out/units_metadata.json +223 -0
  12. qnty/codegen/generators/quantities_gen.py +159 -0
  13. qnty/codegen/generators/setters_gen.py +178 -0
  14. qnty/codegen/generators/stubs_gen.py +167 -0
  15. qnty/codegen/generators/units_gen.py +295 -0
  16. qnty/codegen/generators/utils/__init__.py +0 -0
  17. qnty/equations/__init__.py +4 -0
  18. qnty/equations/equation.py +257 -0
  19. qnty/equations/system.py +127 -0
  20. qnty/expressions/__init__.py +61 -0
  21. qnty/expressions/cache.py +94 -0
  22. qnty/expressions/functions.py +96 -0
  23. qnty/expressions/nodes.py +546 -0
  24. qnty/generated/__init__.py +0 -0
  25. qnty/generated/dimensions.py +514 -0
  26. qnty/generated/quantities.py +6003 -0
  27. qnty/generated/quantities.pyi +4192 -0
  28. qnty/generated/setters.py +12210 -0
  29. qnty/generated/units.py +9798 -0
  30. qnty/problem/__init__.py +91 -0
  31. qnty/problem/base.py +142 -0
  32. qnty/problem/composition.py +385 -0
  33. qnty/problem/composition_mixin.py +382 -0
  34. qnty/problem/equations.py +413 -0
  35. qnty/problem/metaclass.py +302 -0
  36. qnty/problem/reconstruction.py +1016 -0
  37. qnty/problem/solving.py +180 -0
  38. qnty/problem/validation.py +64 -0
  39. qnty/problem/variables.py +239 -0
  40. qnty/quantities/__init__.py +6 -0
  41. qnty/quantities/expression_quantity.py +314 -0
  42. qnty/quantities/quantity.py +428 -0
  43. qnty/quantities/typed_quantity.py +215 -0
  44. qnty/solving/__init__.py +0 -0
  45. qnty/solving/manager.py +90 -0
  46. qnty/solving/order.py +355 -0
  47. qnty/solving/solvers/__init__.py +20 -0
  48. qnty/solving/solvers/base.py +92 -0
  49. qnty/solving/solvers/iterative.py +185 -0
  50. qnty/solving/solvers/simultaneous.py +547 -0
  51. qnty/units/__init__.py +0 -0
  52. qnty/{prefixes.py → units/prefixes.py} +54 -33
  53. qnty/{unit.py → units/registry.py} +73 -32
  54. qnty/utils/__init__.py +0 -0
  55. qnty/utils/logging.py +40 -0
  56. qnty/validation/__init__.py +0 -0
  57. qnty/validation/registry.py +0 -0
  58. qnty/validation/rules.py +167 -0
  59. qnty-0.0.9.dist-info/METADATA +199 -0
  60. qnty-0.0.9.dist-info/RECORD +63 -0
  61. qnty/dimension.py +0 -186
  62. qnty/equation.py +0 -216
  63. qnty/expression.py +0 -492
  64. qnty/unit_types/base.py +0 -47
  65. qnty/units.py +0 -8113
  66. qnty/variable.py +0 -263
  67. qnty/variable_types/base.py +0 -58
  68. qnty/variable_types/expression_variable.py +0 -68
  69. qnty/variable_types/typed_variable.py +0 -87
  70. qnty/variables.py +0 -2298
  71. qnty/variables.pyi +0 -6148
  72. qnty-0.0.7.dist-info/METADATA +0 -355
  73. qnty-0.0.7.dist-info/RECORD +0 -19
  74. /qnty/{unit_types → codegen}/__init__.py +0 -0
  75. /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
  76. {qnty-0.0.7.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,382 @@
1
+ """
2
+ Sub-problem composition functionality for Problem class.
3
+
4
+ This module contains all the sub-problem integration logic,
5
+ namespace handling, and composite equation creation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from qnty.quantities import TypeSafeVariable as Variable
14
+
15
+ # Constants for composition
16
+ MATHEMATICAL_OPERATORS = ['+', '-', '*', '/', ' / ', ' * ', ' + ', ' - ']
17
+ COMMON_COMPOSITE_VARIABLES = ['P', 'c', 'S', 'E', 'W', 'Y']
18
+
19
+
20
+ class CompositionMixin:
21
+ """Mixin class providing sub-problem composition functionality."""
22
+
23
+ # These attributes/methods will be provided by other mixins in the final Problem class
24
+ variables: dict[str, Variable]
25
+ sub_problems: dict[str, Any]
26
+ logger: Any
27
+
28
+ def add_variable(self, _variable: Variable) -> None:
29
+ """Will be provided by VariablesMixin."""
30
+ ...
31
+
32
+ def add_equation(self, _equation: Any) -> None:
33
+ """Will be provided by EquationsMixin."""
34
+ ...
35
+
36
+ def _clone_variable(self, _variable: Variable) -> Variable:
37
+ """Will be provided by VariablesMixin."""
38
+ ...
39
+
40
+ def _process_equation(self, _attr_name: str, _equation: Any) -> bool:
41
+ """Will be provided by EquationsMixin."""
42
+ ...
43
+
44
+ def _recreate_validation_checks(self) -> None:
45
+ """Will be provided by ValidationMixin."""
46
+ ...
47
+
48
+ def _is_conditional_equation(self, _equation: Any) -> bool:
49
+ """Will be provided by EquationsMixin."""
50
+ ...
51
+
52
+ def _get_equation_lhs_symbol(self, _equation: Any) -> str | None:
53
+ """Will be provided by EquationsMixin."""
54
+ ...
55
+
56
+ def _extract_from_class_variables(self):
57
+ """Extract variables, equations, and sub-problems from class-level definitions."""
58
+ self._extract_sub_problems()
59
+ self._extract_direct_variables()
60
+ self._recreate_validation_checks() # type: ignore[attr-defined]
61
+ self._create_composite_equations()
62
+ self._extract_equations()
63
+
64
+ def _extract_sub_problems(self):
65
+ """Extract and integrate sub-problems from class-level definitions."""
66
+ if hasattr(self.__class__, '_original_sub_problems'):
67
+ original_sub_problems = getattr(self.__class__, '_original_sub_problems', {})
68
+ for attr_name, sub_problem in original_sub_problems.items():
69
+ self._integrate_sub_problem(sub_problem, attr_name)
70
+
71
+ def _extract_direct_variables(self):
72
+ """Extract direct variables from class-level definitions."""
73
+ from qnty.quantities import TypeSafeVariable as Variable
74
+
75
+ processed_symbols = set()
76
+
77
+ # Single pass through class attributes to collect variables
78
+ for attr_name, attr_value in self._get_class_attributes():
79
+ if isinstance(attr_value, Variable):
80
+ # Set symbol based on attribute name (T_bar, P, etc.)
81
+ attr_value.symbol = attr_name
82
+
83
+ # Skip if we've already processed this symbol
84
+ if attr_value.symbol in processed_symbols:
85
+ continue
86
+ processed_symbols.add(attr_value.symbol)
87
+
88
+ # Clone variable to avoid shared state between instances
89
+ cloned_var = self._clone_variable(attr_value)
90
+ self.add_variable(cloned_var)
91
+ # Set the same cloned variable object as instance attribute
92
+ # Use super() to bypass our custom __setattr__ during initialization
93
+ super().__setattr__(attr_name, cloned_var)
94
+
95
+ def _extract_equations(self):
96
+ """Extract and process equations from class-level definitions."""
97
+
98
+ equations_to_process = self._collect_class_equations()
99
+
100
+ for attr_name, equation in equations_to_process:
101
+ try:
102
+ if self._process_equation(attr_name, equation):
103
+ setattr(self, attr_name, equation)
104
+ except Exception as e:
105
+ # Log but continue - some equations might fail during class definition
106
+ self.logger.warning(f"Failed to process equation {attr_name}: {e}")
107
+ # Still set the original equation as attribute
108
+ setattr(self, attr_name, equation)
109
+
110
+ def _get_class_attributes(self) -> list[tuple[str, Any]]:
111
+ """Get all non-private class attributes efficiently."""
112
+ return [(attr_name, getattr(self.__class__, attr_name))
113
+ for attr_name in dir(self.__class__)
114
+ if not attr_name.startswith('_')]
115
+
116
+ def _collect_class_equations(self) -> list[tuple[str, Any]]:
117
+ """Collect all equation objects from class attributes."""
118
+ from qnty.equations import Equation
119
+
120
+ equations_to_process = []
121
+ for attr_name, attr_value in self._get_class_attributes():
122
+ if isinstance(attr_value, Equation):
123
+ equations_to_process.append((attr_name, attr_value))
124
+ return equations_to_process
125
+
126
+ def _integrate_sub_problem(self, sub_problem, namespace: str) -> None:
127
+ """
128
+ Integrate a sub-problem by flattening its variables with namespace prefixes.
129
+ Creates a simple dotted access pattern: self.header.P becomes self.header_P
130
+ """
131
+ self.sub_problems[namespace] = sub_problem
132
+
133
+ # Get proxy configurations if available
134
+ proxy_configs = getattr(self.__class__, '_proxy_configurations', {}).get(namespace, {})
135
+
136
+ # Create a namespace object for dotted access (self.header.P)
137
+ namespace_obj = type('SubProblemNamespace', (), {})()
138
+
139
+ # Add all sub-problem variables with namespace prefixes
140
+ for var_symbol, var in sub_problem.variables.items():
141
+ namespaced_var = self._create_namespaced_variable(var, var_symbol, namespace, proxy_configs)
142
+ self.add_variable(namespaced_var)
143
+
144
+ # Set both namespaced access (self.header_P) and dotted access (self.header.P)
145
+ if namespaced_var.symbol is not None:
146
+ super().__setattr__(namespaced_var.symbol, namespaced_var)
147
+ setattr(namespace_obj, var_symbol, namespaced_var)
148
+
149
+ # Set the namespace object for dotted access
150
+ super().__setattr__(namespace, namespace_obj)
151
+
152
+ # Also add all sub-problem equations (they'll be namespaced automatically)
153
+ for equation in sub_problem.equations:
154
+ try:
155
+ # Skip conditional equations for variables that are overridden to known values in composition
156
+ if self._should_skip_subproblem_equation(equation, namespace):
157
+ continue
158
+
159
+ namespaced_equation = self._namespace_equation(equation, namespace)
160
+ if namespaced_equation:
161
+ self.add_equation(namespaced_equation)
162
+ except Exception as e:
163
+ self.logger.debug(f"Failed to namespace equation from {namespace}: {e}")
164
+
165
+ def _create_namespaced_variable(self, var: Variable, var_symbol: str, namespace: str, proxy_configs: dict) -> Variable:
166
+ """Create a namespaced variable with proper configuration."""
167
+ namespaced_symbol = f"{namespace}_{var_symbol}"
168
+ namespaced_var = self._clone_variable(var)
169
+ namespaced_var.symbol = namespaced_symbol
170
+ namespaced_var.name = f"{var.name} ({namespace.title()})"
171
+
172
+ # Apply proxy configuration if available
173
+ if var_symbol in proxy_configs:
174
+ config = proxy_configs[var_symbol]
175
+ namespaced_var.quantity = config['quantity']
176
+ namespaced_var.is_known = config['is_known']
177
+
178
+ return namespaced_var
179
+
180
+ def _namespace_equation(self, equation, namespace: str):
181
+ """
182
+ Create a namespaced version of an equation by prefixing all variable references.
183
+ """
184
+ try:
185
+ # Get all variable symbols in the equation
186
+ variables_in_eq = equation.get_all_variables()
187
+
188
+ # Create mapping from original symbols to namespaced symbols
189
+ symbol_mapping = {}
190
+ for var_symbol in variables_in_eq:
191
+ namespaced_symbol = f"{namespace}_{var_symbol}"
192
+ if namespaced_symbol in self.variables:
193
+ symbol_mapping[var_symbol] = namespaced_symbol
194
+
195
+ if not symbol_mapping:
196
+ return None
197
+
198
+ # Create new equation with namespaced references
199
+ # For LHS, we need a Variable object to call .equals()
200
+ # For RHS, we need proper expression structure
201
+ namespaced_lhs = self._namespace_expression_for_lhs(equation.lhs, symbol_mapping)
202
+ namespaced_rhs = self._namespace_expression(equation.rhs, symbol_mapping)
203
+
204
+ if namespaced_lhs and namespaced_rhs:
205
+ equals_method = getattr(namespaced_lhs, 'equals', None)
206
+ if equals_method:
207
+ return equals_method(namespaced_rhs)
208
+
209
+ return None
210
+
211
+ except Exception:
212
+ return None
213
+
214
+ def _namespace_expression(self, expr, symbol_mapping):
215
+ """
216
+ Create a namespaced version of an expression by replacing variable references.
217
+ """
218
+ from qnty.expressions import BinaryOperation, ConditionalExpression, Constant, VariableReference
219
+
220
+ # Handle variable references
221
+ if isinstance(expr, VariableReference):
222
+ return self._namespace_variable_reference(expr, symbol_mapping)
223
+ elif hasattr(expr, 'symbol') and expr.symbol in symbol_mapping:
224
+ return self._namespace_variable_object(expr, symbol_mapping)
225
+
226
+ # Handle operations
227
+ elif isinstance(expr, BinaryOperation):
228
+ return self._namespace_binary_operation(expr, symbol_mapping)
229
+ elif isinstance(expr, ConditionalExpression):
230
+ return self._namespace_conditional_expression(expr, symbol_mapping)
231
+ elif hasattr(expr, 'operand'):
232
+ return self._namespace_unary_operation(expr, symbol_mapping)
233
+ elif hasattr(expr, 'function_name'):
234
+ return self._namespace_binary_function(expr, symbol_mapping)
235
+ elif isinstance(expr, Constant):
236
+ return expr
237
+ else:
238
+ return expr
239
+
240
+ def _namespace_variable_reference(self, expr, symbol_mapping):
241
+ """Namespace a VariableReference object."""
242
+ from qnty.expressions import VariableReference
243
+
244
+ # VariableReference uses the 'name' property which returns the symbol if available
245
+ symbol = expr.name
246
+ if symbol in symbol_mapping:
247
+ namespaced_symbol = symbol_mapping[symbol]
248
+ if namespaced_symbol in self.variables:
249
+ return VariableReference(self.variables[namespaced_symbol])
250
+ return expr
251
+
252
+ def _namespace_variable_object(self, expr, symbol_mapping):
253
+ """Namespace a Variable object."""
254
+ from qnty.expressions import VariableReference
255
+
256
+ namespaced_symbol = symbol_mapping[expr.symbol]
257
+ if namespaced_symbol in self.variables:
258
+ # Return VariableReference for use in expressions, not the Variable itself
259
+ return VariableReference(self.variables[namespaced_symbol])
260
+ return expr
261
+
262
+ def _namespace_binary_operation(self, expr, symbol_mapping):
263
+ """Namespace a BinaryOperation."""
264
+ from qnty.expressions import BinaryOperation
265
+
266
+ namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
267
+ namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
268
+ return BinaryOperation(expr.operator, namespaced_left, namespaced_right)
269
+
270
+ def _namespace_unary_operation(self, expr, symbol_mapping):
271
+ """Namespace a UnaryFunction."""
272
+ namespaced_operand = self._namespace_expression(expr.operand, symbol_mapping)
273
+ return type(expr)(expr.operator, namespaced_operand)
274
+
275
+ def _namespace_binary_function(self, expr, symbol_mapping):
276
+ """Namespace a BinaryFunction."""
277
+ namespaced_left = self._namespace_expression(expr.left, symbol_mapping)
278
+ namespaced_right = self._namespace_expression(expr.right, symbol_mapping)
279
+ return type(expr)(expr.function_name, namespaced_left, namespaced_right)
280
+
281
+ def _namespace_conditional_expression(self, expr, symbol_mapping):
282
+ """Namespace a ConditionalExpression."""
283
+ from qnty.expressions import ConditionalExpression
284
+
285
+ namespaced_condition = self._namespace_expression(expr.condition, symbol_mapping)
286
+ namespaced_true_expr = self._namespace_expression(expr.true_expr, symbol_mapping)
287
+ namespaced_false_expr = self._namespace_expression(expr.false_expr, symbol_mapping)
288
+
289
+ return ConditionalExpression(namespaced_condition, namespaced_true_expr, namespaced_false_expr)
290
+
291
+ def _namespace_expression_for_lhs(self, expr, symbol_mapping):
292
+ """
293
+ Create a namespaced version of an expression for LHS, returning Variable objects.
294
+ """
295
+ from qnty.expressions import VariableReference
296
+
297
+ if isinstance(expr, VariableReference):
298
+ # VariableReference uses the 'name' property which returns the symbol if available
299
+ symbol = expr.name
300
+ if symbol and symbol in symbol_mapping:
301
+ namespaced_symbol = symbol_mapping[symbol]
302
+ if namespaced_symbol in self.variables:
303
+ return self.variables[namespaced_symbol]
304
+ # If we can't find a mapping, return None since VariableReference doesn't have .equals()
305
+ return None
306
+ elif hasattr(expr, 'symbol') and expr.symbol in symbol_mapping:
307
+ # This is a Variable object
308
+ namespaced_symbol = symbol_mapping[expr.symbol]
309
+ if namespaced_symbol in self.variables:
310
+ return self.variables[namespaced_symbol]
311
+ return expr
312
+ else:
313
+ return expr
314
+
315
+ def _should_skip_subproblem_equation(self, equation, namespace: str) -> bool:
316
+ """
317
+ Check if an equation from a sub-problem should be skipped during integration.
318
+
319
+ Skip conditional equations for variables that are set to known values in the composed problem.
320
+ """
321
+ try:
322
+ # Check if this is a conditional equation
323
+ if not self._is_conditional_equation(equation):
324
+ return False
325
+
326
+ # Check if the LHS variable would be set to a known value in composition
327
+ original_symbol = self._get_equation_lhs_symbol(equation)
328
+ if original_symbol is not None:
329
+ namespaced_symbol = f"{namespace}_{original_symbol}"
330
+
331
+ # Check if this namespaced variable exists and is already known
332
+ if namespaced_symbol in self.variables:
333
+ var = self.variables[namespaced_symbol]
334
+ if var.is_known:
335
+ # The variable is already set to a known value in composition,
336
+ # so skip the conditional equation that would override it
337
+ self.logger.debug(f"Skipping conditional equation for {namespaced_symbol} (already known: {var.quantity})")
338
+ return True
339
+
340
+ return False
341
+
342
+ except Exception:
343
+ return False
344
+
345
+ def _create_composite_equations(self):
346
+ """
347
+ Create composite equations for common patterns in sub-problems.
348
+ This handles equations like P = min(header.P, branch.P) automatically.
349
+ """
350
+ if not self.sub_problems:
351
+ return
352
+
353
+ # Common composite patterns to auto-generate
354
+ for var_name in COMMON_COMPOSITE_VARIABLES:
355
+ # Check if this variable exists in multiple sub-problems
356
+ sub_problem_vars = []
357
+ for namespace in self.sub_problems:
358
+ namespaced_symbol = f"{namespace}_{var_name}"
359
+ if namespaced_symbol in self.variables:
360
+ sub_problem_vars.append(self.variables[namespaced_symbol])
361
+
362
+ # If we have the variable in multiple sub-problems and no direct variable exists
363
+ if len(sub_problem_vars) >= 2 and var_name in self.variables:
364
+ # Check if a composite equation already exists
365
+ equation_attr_name = f"{var_name}_eqn"
366
+ if hasattr(self.__class__, equation_attr_name):
367
+ # Skip auto-creation since explicit equation exists
368
+ continue
369
+
370
+ # Auto-create composite equation
371
+ try:
372
+ from qnty.expressions import min_expr
373
+ composite_var = self.variables[var_name]
374
+ if not composite_var.is_known: # Only for unknown variables
375
+ composite_expr = min_expr(*sub_problem_vars)
376
+ equals_method = getattr(composite_var, 'equals', None)
377
+ if equals_method:
378
+ composite_eq = equals_method(composite_expr)
379
+ self.add_equation(composite_eq)
380
+ setattr(self, f"{var_name}_eqn", composite_eq)
381
+ except Exception as e:
382
+ self.logger.debug(f"Failed to create composite equation for {var_name}: {e}")