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.
- qnty/__init__.py +140 -58
- qnty/_backup/problem_original.py +1251 -0
- qnty/_backup/quantity.py +63 -0
- qnty/codegen/cli.py +125 -0
- qnty/codegen/generators/data/unit_data.json +8807 -0
- qnty/codegen/generators/data_processor.py +345 -0
- qnty/codegen/generators/dimensions_gen.py +434 -0
- qnty/codegen/generators/doc_generator.py +141 -0
- qnty/codegen/generators/out/dimension_mapping.json +974 -0
- qnty/codegen/generators/out/dimension_metadata.json +123 -0
- qnty/codegen/generators/out/units_metadata.json +223 -0
- qnty/codegen/generators/quantities_gen.py +159 -0
- qnty/codegen/generators/setters_gen.py +178 -0
- qnty/codegen/generators/stubs_gen.py +167 -0
- qnty/codegen/generators/units_gen.py +295 -0
- qnty/codegen/generators/utils/__init__.py +0 -0
- qnty/equations/__init__.py +4 -0
- qnty/equations/equation.py +257 -0
- qnty/equations/system.py +127 -0
- qnty/expressions/__init__.py +61 -0
- qnty/expressions/cache.py +94 -0
- qnty/expressions/functions.py +96 -0
- qnty/expressions/nodes.py +546 -0
- qnty/generated/__init__.py +0 -0
- qnty/generated/dimensions.py +514 -0
- qnty/generated/quantities.py +6003 -0
- qnty/generated/quantities.pyi +4192 -0
- qnty/generated/setters.py +12210 -0
- qnty/generated/units.py +9798 -0
- qnty/problem/__init__.py +91 -0
- qnty/problem/base.py +142 -0
- qnty/problem/composition.py +385 -0
- qnty/problem/composition_mixin.py +382 -0
- qnty/problem/equations.py +413 -0
- qnty/problem/metaclass.py +302 -0
- qnty/problem/reconstruction.py +1016 -0
- qnty/problem/solving.py +180 -0
- qnty/problem/validation.py +64 -0
- qnty/problem/variables.py +239 -0
- qnty/quantities/__init__.py +6 -0
- qnty/quantities/expression_quantity.py +314 -0
- qnty/quantities/quantity.py +428 -0
- qnty/quantities/typed_quantity.py +215 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +90 -0
- qnty/solving/order.py +355 -0
- qnty/solving/solvers/__init__.py +20 -0
- qnty/solving/solvers/base.py +92 -0
- qnty/solving/solvers/iterative.py +185 -0
- qnty/solving/solvers/simultaneous.py +547 -0
- qnty/units/__init__.py +0 -0
- qnty/{prefixes.py → units/prefixes.py} +54 -33
- qnty/{unit.py → units/registry.py} +73 -32
- qnty/utils/__init__.py +0 -0
- qnty/utils/logging.py +40 -0
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +167 -0
- qnty-0.0.9.dist-info/METADATA +199 -0
- qnty-0.0.9.dist-info/RECORD +63 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -216
- qnty/expression.py +0 -492
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -263
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -68
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.7.dist-info/METADATA +0 -355
- qnty-0.0.7.dist-info/RECORD +0 -19
- /qnty/{unit_types → codegen}/__init__.py +0 -0
- /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
- {qnty-0.0.7.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
qnty/problem/solving.py
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
"""
|
2
|
+
High-level solve orchestration for Problem class.
|
3
|
+
|
4
|
+
This module contains the main solving logic, dependency graph building,
|
5
|
+
solution verification, and system analysis methods.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from typing import TYPE_CHECKING, Any
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from qnty.equations import Equation
|
14
|
+
from qnty.quantities import TypeSafeVariable as Variable
|
15
|
+
|
16
|
+
# Constants
|
17
|
+
MAX_ITERATIONS_DEFAULT = 100
|
18
|
+
TOLERANCE_DEFAULT = 1e-10
|
19
|
+
|
20
|
+
|
21
|
+
# Custom Exceptions
|
22
|
+
class SolverError(RuntimeError):
|
23
|
+
"""Raised when the solving process fails."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
27
|
+
class SolvingMixin:
|
28
|
+
"""Mixin class providing solving orchestration functionality."""
|
29
|
+
|
30
|
+
# These attributes/methods will be provided by other mixins in the final Problem class
|
31
|
+
name: str
|
32
|
+
logger: Any
|
33
|
+
equations: list[Equation]
|
34
|
+
solver_manager: Any
|
35
|
+
|
36
|
+
def get_known_symbols(self) -> set[str]:
|
37
|
+
"""Will be provided by VariablesMixin."""
|
38
|
+
...
|
39
|
+
|
40
|
+
def get_known_variables(self) -> dict[str, Variable]:
|
41
|
+
"""Will be provided by VariablesMixin."""
|
42
|
+
...
|
43
|
+
|
44
|
+
def get_unknown_variables(self) -> dict[str, Variable]:
|
45
|
+
"""Will be provided by VariablesMixin."""
|
46
|
+
...
|
47
|
+
|
48
|
+
def _sync_variables_to_instance_attributes(self) -> None:
|
49
|
+
"""Will be provided by VariablesMixin."""
|
50
|
+
...
|
51
|
+
|
52
|
+
def solve(self, max_iterations: int = MAX_ITERATIONS_DEFAULT, tolerance: float = TOLERANCE_DEFAULT) -> dict[str, Any]:
|
53
|
+
"""
|
54
|
+
Solve the engineering problem by finding values for all unknown variables.
|
55
|
+
|
56
|
+
This method orchestrates the complete solving process:
|
57
|
+
1. Builds dependency graph from equations
|
58
|
+
2. Determines optimal solving order using topological sorting
|
59
|
+
3. Solves equations iteratively using symbolic/numerical methods
|
60
|
+
4. Verifies solution against all equations
|
61
|
+
5. Updates variable states and synchronizes instance attributes
|
62
|
+
|
63
|
+
Args:
|
64
|
+
max_iterations: Maximum number of solving iterations (default: 100)
|
65
|
+
tolerance: Numerical tolerance for convergence (default: 1e-10)
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
dict mapping variable symbols to solved Variable objects
|
69
|
+
|
70
|
+
Raises:
|
71
|
+
SolverError: If solving fails or times out
|
72
|
+
|
73
|
+
Example:
|
74
|
+
>>> problem = MyEngineeringProblem()
|
75
|
+
>>> solution = problem.solve()
|
76
|
+
>>> print(f"Force = {solution['F'].quantity}")
|
77
|
+
"""
|
78
|
+
self.logger.info(f"Solving problem: {self.name}")
|
79
|
+
|
80
|
+
try:
|
81
|
+
# Clear previous solution
|
82
|
+
self.solution = {}
|
83
|
+
self.is_solved = False
|
84
|
+
self.solving_history = []
|
85
|
+
|
86
|
+
# Build dependency graph
|
87
|
+
self._build_dependency_graph()
|
88
|
+
|
89
|
+
# Use solver manager to solve the system
|
90
|
+
solve_result = self.solver_manager.solve(
|
91
|
+
self.equations,
|
92
|
+
self.variables,
|
93
|
+
self.dependency_graph,
|
94
|
+
max_iterations,
|
95
|
+
tolerance
|
96
|
+
)
|
97
|
+
|
98
|
+
if solve_result.success:
|
99
|
+
# Update variables with the result
|
100
|
+
self.variables = solve_result.variables
|
101
|
+
self.solving_history.extend(solve_result.steps)
|
102
|
+
|
103
|
+
# Sync solved values back to instance attributes
|
104
|
+
self._sync_variables_to_instance_attributes()
|
105
|
+
|
106
|
+
# Verify solution
|
107
|
+
self.solution = self.variables
|
108
|
+
verification_passed = self.verify_solution()
|
109
|
+
|
110
|
+
# Mark as solved based on solver result and verification
|
111
|
+
if verification_passed:
|
112
|
+
self.is_solved = True
|
113
|
+
self.logger.info("Solution verified successfully")
|
114
|
+
return self.solution
|
115
|
+
else:
|
116
|
+
self.logger.warning("Solution verification failed")
|
117
|
+
return self.solution
|
118
|
+
else:
|
119
|
+
raise SolverError(f"Solving failed: {solve_result.message}")
|
120
|
+
|
121
|
+
except SolverError:
|
122
|
+
raise
|
123
|
+
except Exception as e:
|
124
|
+
self.logger.error(f"Solving failed: {e}")
|
125
|
+
raise SolverError(f"Unexpected error during solving: {e}") from e
|
126
|
+
|
127
|
+
def _build_dependency_graph(self):
|
128
|
+
"""Build the dependency graph for solving order determination."""
|
129
|
+
# Reset the dependency graph
|
130
|
+
from qnty.solving.order import Order
|
131
|
+
self.dependency_graph = Order()
|
132
|
+
|
133
|
+
# Get known variables
|
134
|
+
known_vars = self.get_known_symbols()
|
135
|
+
|
136
|
+
# Add dependencies from equations
|
137
|
+
for equation in self.equations:
|
138
|
+
self.dependency_graph.add_equation(equation, known_vars)
|
139
|
+
|
140
|
+
def verify_solution(self, tolerance: float = 1e-10) -> bool:
|
141
|
+
"""Verify that all equations are satisfied."""
|
142
|
+
if not self.equations:
|
143
|
+
return True
|
144
|
+
|
145
|
+
try:
|
146
|
+
for equation in self.equations:
|
147
|
+
if not equation.check_residual(self.variables, tolerance):
|
148
|
+
self.logger.debug(f"Equation verification failed: {equation}")
|
149
|
+
return False
|
150
|
+
return True
|
151
|
+
except Exception as e:
|
152
|
+
self.logger.debug(f"Solution verification error: {e}")
|
153
|
+
return False
|
154
|
+
|
155
|
+
def analyze_system(self) -> dict[str, Any]:
|
156
|
+
"""Analyze the equation system for solvability, cycles, etc."""
|
157
|
+
try:
|
158
|
+
self._build_dependency_graph()
|
159
|
+
known_vars = self.get_known_symbols()
|
160
|
+
analysis = self.dependency_graph.analyze_system(known_vars)
|
161
|
+
|
162
|
+
# Add some additional info
|
163
|
+
analysis['total_equations'] = len(self.equations)
|
164
|
+
analysis['is_determined'] = len(self.get_unknown_variables()) <= len(self.equations)
|
165
|
+
|
166
|
+
return analysis
|
167
|
+
except Exception as e:
|
168
|
+
self.logger.debug(f"Dependency analysis failed: {e}")
|
169
|
+
# Return basic analysis on failure
|
170
|
+
return {
|
171
|
+
'total_variables': len(self.variables),
|
172
|
+
'known_variables': len(self.get_known_variables()),
|
173
|
+
'unknown_variables': len(self.get_unknown_variables()),
|
174
|
+
'total_equations': len(self.equations),
|
175
|
+
'is_determined': len(self.get_unknown_variables()) <= len(self.equations),
|
176
|
+
'has_cycles': False,
|
177
|
+
'solving_order': [],
|
178
|
+
'can_solve_completely': False,
|
179
|
+
'unsolvable_variables': []
|
180
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Problem-validation integration for Problem class.
|
3
|
+
|
4
|
+
This module contains validation check management and execution
|
5
|
+
integrated with the Problem system.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from collections.abc import Callable
|
11
|
+
from typing import TYPE_CHECKING, Any
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
pass
|
15
|
+
|
16
|
+
|
17
|
+
class ValidationMixin:
|
18
|
+
"""Mixin class providing validation functionality."""
|
19
|
+
|
20
|
+
# These attributes will be provided by other mixins in the final Problem class
|
21
|
+
logger: Any
|
22
|
+
warnings: list[dict[str, Any]]
|
23
|
+
validation_checks: list[Callable]
|
24
|
+
|
25
|
+
def add_validation_check(self, check_function: Callable) -> None:
|
26
|
+
"""Add a validation check function."""
|
27
|
+
self.validation_checks.append(check_function)
|
28
|
+
|
29
|
+
def validate(self) -> list[dict[str, Any]]:
|
30
|
+
"""Run all validation checks and return any warnings."""
|
31
|
+
validation_warnings = []
|
32
|
+
|
33
|
+
for check in self.validation_checks:
|
34
|
+
try:
|
35
|
+
result = check(self)
|
36
|
+
if result:
|
37
|
+
validation_warnings.append(result)
|
38
|
+
except Exception as e:
|
39
|
+
self.logger.debug(f"Validation check failed: {e}")
|
40
|
+
|
41
|
+
return validation_warnings
|
42
|
+
|
43
|
+
def get_warnings(self) -> list[dict[str, Any]]:
|
44
|
+
"""Get all warnings from the problem."""
|
45
|
+
warnings = self.warnings.copy()
|
46
|
+
warnings.extend(self.validate())
|
47
|
+
return warnings
|
48
|
+
|
49
|
+
def _recreate_validation_checks(self):
|
50
|
+
"""Collect and integrate validation checks from class-level Check objects."""
|
51
|
+
# Clear existing checks
|
52
|
+
self.validation_checks = []
|
53
|
+
|
54
|
+
# Collect Check objects from metaclass
|
55
|
+
class_checks = getattr(self.__class__, '_class_checks', {})
|
56
|
+
|
57
|
+
for check in class_checks.values():
|
58
|
+
# Create a validation function from the Check object
|
59
|
+
def make_check_function(check_obj):
|
60
|
+
def check_function(problem_instance):
|
61
|
+
return check_obj.evaluate(problem_instance.variables)
|
62
|
+
return check_function
|
63
|
+
|
64
|
+
self.validation_checks.append(make_check_function(check))
|
@@ -0,0 +1,239 @@
|
|
1
|
+
"""
|
2
|
+
Variable lifecycle management for Problem class.
|
3
|
+
|
4
|
+
This module contains all variable-related operations including adding,
|
5
|
+
getting, managing known/unknown state, and variable caching.
|
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 Quantity as Qty
|
14
|
+
from qnty.quantities import TypeSafeVariable as Variable
|
15
|
+
|
16
|
+
from qnty.generated.units import DimensionlessUnits
|
17
|
+
from qnty.quantities import Quantity as Qty
|
18
|
+
from qnty.quantities import TypeSafeVariable as Variable
|
19
|
+
|
20
|
+
|
21
|
+
# Custom Exceptions
|
22
|
+
class VariableNotFoundError(KeyError):
|
23
|
+
"""Raised when trying to access a variable that doesn't exist."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
27
|
+
class VariablesMixin:
|
28
|
+
"""Mixin class providing variable management functionality."""
|
29
|
+
|
30
|
+
# These attributes/methods will be provided by other mixins in the final Problem class
|
31
|
+
variables: dict[str, Variable]
|
32
|
+
name: str
|
33
|
+
logger: Any
|
34
|
+
is_solved: bool
|
35
|
+
sub_problems: dict[str, Any]
|
36
|
+
dependency_graph: Any
|
37
|
+
_known_variables_cache: dict[str, Variable] | None
|
38
|
+
_unknown_variables_cache: dict[str, Variable] | None
|
39
|
+
_cache_dirty: bool
|
40
|
+
|
41
|
+
def _invalidate_caches(self) -> None:
|
42
|
+
"""Will be provided by ProblemBase."""
|
43
|
+
...
|
44
|
+
|
45
|
+
def _update_variable_caches(self) -> None:
|
46
|
+
"""Will be provided by ProblemBase."""
|
47
|
+
...
|
48
|
+
|
49
|
+
def add_variable(self, variable: Variable) -> None:
|
50
|
+
"""
|
51
|
+
Add a variable to the problem.
|
52
|
+
|
53
|
+
The variable will be available for use in equations and can be accessed
|
54
|
+
via both dictionary notation (problem['symbol']) and attribute notation
|
55
|
+
(problem.symbol).
|
56
|
+
|
57
|
+
Args:
|
58
|
+
variable: Variable object to add to the problem
|
59
|
+
|
60
|
+
Note:
|
61
|
+
If a variable with the same symbol already exists, it will be replaced
|
62
|
+
and a warning will be logged.
|
63
|
+
"""
|
64
|
+
if variable.symbol in self.variables:
|
65
|
+
self.logger.warning(f"Variable {variable.symbol} already exists. Replacing.")
|
66
|
+
|
67
|
+
if variable.symbol is not None:
|
68
|
+
self.variables[variable.symbol] = variable
|
69
|
+
# Set parent problem reference for dependency invalidation
|
70
|
+
try:
|
71
|
+
variable._parent_problem = self
|
72
|
+
except (AttributeError, TypeError):
|
73
|
+
# _parent_problem might not be settable
|
74
|
+
pass
|
75
|
+
# Also set as instance attribute for dot notation access
|
76
|
+
if variable.symbol is not None:
|
77
|
+
setattr(self, variable.symbol, variable)
|
78
|
+
self.is_solved = False
|
79
|
+
self._invalidate_caches()
|
80
|
+
|
81
|
+
def add_variables(self, *variables: Variable) -> None:
|
82
|
+
"""Add multiple variables to the problem."""
|
83
|
+
for var in variables:
|
84
|
+
self.add_variable(var)
|
85
|
+
|
86
|
+
def get_variable(self, symbol: str) -> Variable:
|
87
|
+
"""Get a variable by its symbol."""
|
88
|
+
if symbol not in self.variables:
|
89
|
+
raise VariableNotFoundError(f"Variable '{symbol}' not found in problem '{self.name}'.")
|
90
|
+
return self.variables[symbol]
|
91
|
+
|
92
|
+
def get_known_variables(self) -> dict[str, Variable]:
|
93
|
+
"""Get all known variables."""
|
94
|
+
if self._cache_dirty or self._known_variables_cache is None:
|
95
|
+
self._update_variable_caches()
|
96
|
+
return self._known_variables_cache.copy() if self._known_variables_cache else {}
|
97
|
+
|
98
|
+
def get_unknown_variables(self) -> dict[str, Variable]:
|
99
|
+
"""Get all unknown variables."""
|
100
|
+
if self._cache_dirty or self._unknown_variables_cache is None:
|
101
|
+
self._update_variable_caches()
|
102
|
+
return self._unknown_variables_cache.copy() if self._unknown_variables_cache else {}
|
103
|
+
|
104
|
+
def get_known_symbols(self) -> set[str]:
|
105
|
+
"""Get symbols of all known variables."""
|
106
|
+
return {symbol for symbol, var in self.variables.items() if var.is_known}
|
107
|
+
|
108
|
+
def get_unknown_symbols(self) -> set[str]:
|
109
|
+
"""Get symbols of all unknown variables."""
|
110
|
+
return {symbol for symbol, var in self.variables.items() if not var.is_known}
|
111
|
+
|
112
|
+
def get_known_variable_symbols(self) -> set[str]:
|
113
|
+
"""Alias for get_known_symbols for compatibility."""
|
114
|
+
return self.get_known_symbols()
|
115
|
+
|
116
|
+
def get_unknown_variable_symbols(self) -> set[str]:
|
117
|
+
"""Alias for get_unknown_symbols for compatibility."""
|
118
|
+
return self.get_unknown_symbols()
|
119
|
+
|
120
|
+
# Properties for compatibility
|
121
|
+
@property
|
122
|
+
def known_variables(self) -> dict[str, Variable]:
|
123
|
+
"""Get all variables marked as known."""
|
124
|
+
return self.get_known_variables()
|
125
|
+
|
126
|
+
@property
|
127
|
+
def unknown_variables(self) -> dict[str, Variable]:
|
128
|
+
"""Get all variables marked as unknown."""
|
129
|
+
return self.get_unknown_variables()
|
130
|
+
|
131
|
+
def mark_unknown(self, *symbols: str):
|
132
|
+
"""Mark variables as unknown (to be solved for)."""
|
133
|
+
for symbol in symbols:
|
134
|
+
if symbol in self.variables:
|
135
|
+
self.variables[symbol].mark_unknown()
|
136
|
+
else:
|
137
|
+
raise VariableNotFoundError(f"Variable '{symbol}' not found in problem '{self.name}'")
|
138
|
+
self.is_solved = False
|
139
|
+
self._invalidate_caches()
|
140
|
+
return self
|
141
|
+
|
142
|
+
def mark_known(self, **symbol_values: Qty):
|
143
|
+
"""Mark variables as known and set their values."""
|
144
|
+
for symbol, quantity in symbol_values.items():
|
145
|
+
if symbol in self.variables:
|
146
|
+
self.variables[symbol].mark_known(quantity)
|
147
|
+
else:
|
148
|
+
raise VariableNotFoundError(f"Variable '{symbol}' not found in problem '{self.name}'")
|
149
|
+
self.is_solved = False
|
150
|
+
self._invalidate_caches()
|
151
|
+
return self
|
152
|
+
|
153
|
+
def invalidate_dependents(self, changed_variable_symbol: str) -> None:
|
154
|
+
"""
|
155
|
+
Mark all variables that depend on the changed variable as unknown.
|
156
|
+
This ensures they get recalculated when the problem is re-solved.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
changed_variable_symbol: Symbol of the variable whose value changed
|
160
|
+
"""
|
161
|
+
if not hasattr(self, 'dependency_graph') or not self.dependency_graph:
|
162
|
+
# If dependency graph hasn't been built yet, we can't invalidate
|
163
|
+
return
|
164
|
+
|
165
|
+
# Get all variables that depend on the changed variable
|
166
|
+
dependent_vars = self.dependency_graph.graph.get(changed_variable_symbol, [])
|
167
|
+
|
168
|
+
# Mark each dependent variable as unknown
|
169
|
+
for dependent_symbol in dependent_vars:
|
170
|
+
if dependent_symbol in self.variables:
|
171
|
+
var = self.variables[dependent_symbol]
|
172
|
+
# Only mark as unknown if it was previously solved (known)
|
173
|
+
if var.is_known:
|
174
|
+
var.mark_unknown()
|
175
|
+
# Recursively invalidate variables that depend on this one
|
176
|
+
self.invalidate_dependents(dependent_symbol)
|
177
|
+
|
178
|
+
# Mark problem as needing re-solving
|
179
|
+
self.is_solved = False
|
180
|
+
self._invalidate_caches()
|
181
|
+
|
182
|
+
def _create_placeholder_variable(self, symbol: str) -> None:
|
183
|
+
"""Create a placeholder variable for a missing symbol."""
|
184
|
+
|
185
|
+
placeholder_var = Variable(
|
186
|
+
name=f"Auto-created: {symbol}",
|
187
|
+
expected_dimension=DimensionlessUnits.dimensionless.dimension,
|
188
|
+
is_known=False
|
189
|
+
)
|
190
|
+
placeholder_var.symbol = symbol
|
191
|
+
placeholder_var.quantity = Qty(0.0, DimensionlessUnits.dimensionless)
|
192
|
+
self.add_variable(placeholder_var)
|
193
|
+
self.logger.debug(f"Auto-created placeholder variable: {symbol}")
|
194
|
+
|
195
|
+
def _clone_variable(self, variable: Variable) -> Variable:
|
196
|
+
"""Create a copy of a variable to avoid shared state without corrupting global units."""
|
197
|
+
# Create a new variable of the same exact type to preserve .equals() method
|
198
|
+
# This ensures domain-specific variables (Length, Pressure, etc.) keep their type
|
199
|
+
variable_type = type(variable)
|
200
|
+
|
201
|
+
# Use __new__ to avoid constructor parameter issues
|
202
|
+
cloned = variable_type.__new__(variable_type)
|
203
|
+
|
204
|
+
# Initialize manually with the same attributes as the original
|
205
|
+
cloned.name = variable.name
|
206
|
+
cloned.symbol = variable.symbol
|
207
|
+
cloned.expected_dimension = variable.expected_dimension
|
208
|
+
cloned.quantity = variable.quantity # Keep reference to same quantity - units must not be copied
|
209
|
+
cloned.is_known = variable.is_known
|
210
|
+
|
211
|
+
# Ensure the cloned variable has fresh validation checks
|
212
|
+
if hasattr(variable, 'validation_checks'):
|
213
|
+
try:
|
214
|
+
cloned.validation_checks = []
|
215
|
+
except (AttributeError, TypeError):
|
216
|
+
# validation_checks might be read-only or not settable
|
217
|
+
pass
|
218
|
+
return cloned
|
219
|
+
|
220
|
+
def _sync_variables_to_instance_attributes(self):
|
221
|
+
"""
|
222
|
+
Sync variable objects to instance attributes after solving.
|
223
|
+
This ensures that self.P refers to the same Variable object that's in self.variables.
|
224
|
+
Variables maintain their original dimensional types (e.g., AreaVariable, PressureVariable).
|
225
|
+
"""
|
226
|
+
for var_symbol, var in self.variables.items():
|
227
|
+
# Update instance attribute if it exists
|
228
|
+
if hasattr(self, var_symbol):
|
229
|
+
# Variables preserve their dimensional types during solving
|
230
|
+
setattr(self, var_symbol, var)
|
231
|
+
|
232
|
+
# Also update sub-problem namespace objects
|
233
|
+
for namespace, sub_problem in self.sub_problems.items():
|
234
|
+
if hasattr(self, namespace):
|
235
|
+
namespace_obj = getattr(self, namespace)
|
236
|
+
for var_symbol in sub_problem.variables:
|
237
|
+
namespaced_symbol = f"{namespace}_{var_symbol}"
|
238
|
+
if namespaced_symbol in self.variables and hasattr(namespace_obj, var_symbol):
|
239
|
+
setattr(namespace_obj, var_symbol, self.variables[namespaced_symbol])
|