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
@@ -0,0 +1,547 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from qnty.equations.equation import Equation
|
6
|
+
from qnty.quantities import Quantity as Qty
|
7
|
+
from qnty.quantities import TypeSafeVariable as Variable
|
8
|
+
from qnty.solving.order import Order
|
9
|
+
|
10
|
+
from .base import BaseSolver, SolveResult
|
11
|
+
|
12
|
+
|
13
|
+
class SimultaneousEquationSolver(BaseSolver):
|
14
|
+
"""
|
15
|
+
Solver for n×n simultaneous linear equation systems using matrix operations.
|
16
|
+
|
17
|
+
This solver handles systems where equations are mutually dependent (forming cycles
|
18
|
+
in the dependency graph), requiring simultaneous solution rather than sequential
|
19
|
+
solution of individual equations.
|
20
|
+
|
21
|
+
Algorithm:
|
22
|
+
1. Validate system requirements (square, n≥2, has cycles)
|
23
|
+
2. Extract coefficient matrix A and constant vector b from equations
|
24
|
+
3. Check numerical stability (condition number)
|
25
|
+
4. Solve matrix system Ax = b using robust linear algebra
|
26
|
+
5. Update variables with solutions and verify residuals
|
27
|
+
|
28
|
+
Supports any size square system (n equations, n unknowns) that is:
|
29
|
+
- Linearly independent (non-singular matrix)
|
30
|
+
- Well-conditioned (numerically stable)
|
31
|
+
- Composed of linear equations with units preserved throughout
|
32
|
+
|
33
|
+
Examples:
|
34
|
+
2×2 system: x + y = 3, 2x - y = 0 → x=1, y=2
|
35
|
+
3×3 system: Complex engineering systems with interdependent variables
|
36
|
+
"""
|
37
|
+
|
38
|
+
# Constants for numerical stability and validation
|
39
|
+
MIN_SYSTEM_SIZE = 2
|
40
|
+
MAX_CONDITION_NUMBER = 1e12
|
41
|
+
DEFAULT_TOLERANCE = 1e-10
|
42
|
+
|
43
|
+
# Performance optimization constants
|
44
|
+
LARGE_SYSTEM_THRESHOLD = 100 # Switch to optimized algorithms for n > 100
|
45
|
+
SPARSE_THRESHOLD = 0.1 # Use sparse matrices if density < 10%
|
46
|
+
|
47
|
+
def can_handle(self, equations: list[Equation], unknowns: set[str],
|
48
|
+
dependency_graph: Order | None = None,
|
49
|
+
analysis: dict[str, Any] | None = None) -> bool:
|
50
|
+
"""
|
51
|
+
Determine if this solver can handle the given system.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
equations: List of equations to solve
|
55
|
+
unknowns: Set of unknown variable symbols
|
56
|
+
dependency_graph: Optional dependency graph (unused)
|
57
|
+
analysis: Optional system analysis containing cycle information
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
True if this solver can handle the system:
|
61
|
+
- Square system (n equations = n unknowns)
|
62
|
+
- Minimum size (n ≥ 2)
|
63
|
+
- Contains cycles (indicating simultaneous equations needed)
|
64
|
+
"""
|
65
|
+
# dependency_graph parameter unused but required for interface compatibility
|
66
|
+
_ = dependency_graph
|
67
|
+
|
68
|
+
system_size = len(equations)
|
69
|
+
num_unknowns = len(unknowns)
|
70
|
+
|
71
|
+
# Validate square system with minimum size
|
72
|
+
if system_size != num_unknowns or system_size < self.MIN_SYSTEM_SIZE:
|
73
|
+
return False
|
74
|
+
|
75
|
+
# Only handle systems with cycles (mutual dependencies)
|
76
|
+
if analysis is None:
|
77
|
+
return False
|
78
|
+
has_cycles = analysis.get('has_cycles', False)
|
79
|
+
return bool(has_cycles)
|
80
|
+
|
81
|
+
def solve(
|
82
|
+
self,
|
83
|
+
equations: list[Equation],
|
84
|
+
variables: dict[str, Variable],
|
85
|
+
dependency_graph: Order | None = None,
|
86
|
+
max_iterations: int = 100,
|
87
|
+
tolerance: float = DEFAULT_TOLERANCE
|
88
|
+
) -> SolveResult:
|
89
|
+
"""
|
90
|
+
Solve the n×n simultaneous system using matrix operations.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
equations: List of n linear equations to solve simultaneously
|
94
|
+
variables: Dictionary of all variables (known and unknown)
|
95
|
+
dependency_graph: Optional dependency graph
|
96
|
+
(unused for simultaneous systems)
|
97
|
+
max_iterations: Maximum iterations
|
98
|
+
(unused for direct matrix solving)
|
99
|
+
tolerance: Numerical tolerance for residual checking
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
SolveResult containing the solution or error information
|
103
|
+
"""
|
104
|
+
# Mark unused parameters to satisfy linter
|
105
|
+
_ = dependency_graph, max_iterations
|
106
|
+
|
107
|
+
self.steps = []
|
108
|
+
|
109
|
+
# Step 1: Validate system requirements
|
110
|
+
validation_result = self._validate_system(equations, variables)
|
111
|
+
if not validation_result.success:
|
112
|
+
return validation_result
|
113
|
+
|
114
|
+
unknown_variable_names = list(self._get_unknown_variables(variables))
|
115
|
+
working_variables = dict(variables)
|
116
|
+
|
117
|
+
# Step 2: Extract and solve matrix system
|
118
|
+
try:
|
119
|
+
solution_vector = self._solve_matrix_system(
|
120
|
+
equations, unknown_variable_names, working_variables
|
121
|
+
)
|
122
|
+
if solution_vector is None:
|
123
|
+
return SolveResult(
|
124
|
+
variables=working_variables,
|
125
|
+
steps=self.steps,
|
126
|
+
success=False,
|
127
|
+
message="Failed to solve matrix system",
|
128
|
+
method="SimultaneousEquationSolver"
|
129
|
+
)
|
130
|
+
|
131
|
+
# Step 3: Update variables with solutions
|
132
|
+
self._apply_solution_to_variables(
|
133
|
+
unknown_variable_names, solution_vector, working_variables
|
134
|
+
)
|
135
|
+
|
136
|
+
# Step 4: Verify solution quality
|
137
|
+
verification_result = self._verify_solution_quality(
|
138
|
+
equations, working_variables, tolerance
|
139
|
+
)
|
140
|
+
|
141
|
+
return SolveResult(
|
142
|
+
variables=working_variables,
|
143
|
+
steps=self.steps,
|
144
|
+
success=verification_result.success,
|
145
|
+
message=verification_result.message,
|
146
|
+
method="SimultaneousEquationSolver",
|
147
|
+
iterations=1
|
148
|
+
)
|
149
|
+
|
150
|
+
except Exception as general_error:
|
151
|
+
return SolveResult(
|
152
|
+
variables=working_variables,
|
153
|
+
steps=self.steps,
|
154
|
+
success=False,
|
155
|
+
message=f"Simultaneous solving failed: {general_error}",
|
156
|
+
method="SimultaneousEquationSolver"
|
157
|
+
)
|
158
|
+
|
159
|
+
def _validate_system(
|
160
|
+
self, equations: list[Equation], variables: dict[str, Variable]
|
161
|
+
) -> SolveResult:
|
162
|
+
"""
|
163
|
+
Validate that the system meets requirements for simultaneous solving.
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
SolveResult with success=True if valid, or error result if invalid
|
167
|
+
"""
|
168
|
+
unknown_variable_names = list(self._get_unknown_variables(variables))
|
169
|
+
num_unknowns = len(unknown_variable_names)
|
170
|
+
num_equations = len(equations)
|
171
|
+
|
172
|
+
# Validate square system with minimum size
|
173
|
+
if num_unknowns != num_equations or num_unknowns < self.MIN_SYSTEM_SIZE:
|
174
|
+
return SolveResult(
|
175
|
+
variables=variables,
|
176
|
+
steps=self.steps,
|
177
|
+
success=False,
|
178
|
+
message=f"Simultaneous solver requires n×n system (got {num_equations} equations, {num_unknowns} unknowns)",
|
179
|
+
method="SimultaneousEquationSolver"
|
180
|
+
)
|
181
|
+
|
182
|
+
if self.logger:
|
183
|
+
self.logger.debug(
|
184
|
+
f"Attempting {num_unknowns}×{num_unknowns} simultaneous solution for {unknown_variable_names}"
|
185
|
+
)
|
186
|
+
|
187
|
+
return SolveResult(
|
188
|
+
variables=variables,
|
189
|
+
steps=self.steps,
|
190
|
+
success=True,
|
191
|
+
message="System validation passed",
|
192
|
+
method="SimultaneousEquationSolver"
|
193
|
+
)
|
194
|
+
|
195
|
+
def _solve_matrix_system(self, equations: list[Equation], unknown_variables: list[str],
|
196
|
+
working_variables: dict[str, Variable]) -> np.ndarray | None:
|
197
|
+
"""
|
198
|
+
Extract coefficient matrix and solve the linear system.
|
199
|
+
|
200
|
+
Returns:
|
201
|
+
Solution vector if successful, None if failed
|
202
|
+
"""
|
203
|
+
# Extract coefficient matrix A and constant vector b
|
204
|
+
coefficient_matrix, constant_vector = self._extract_matrix_system(equations, unknown_variables, working_variables)
|
205
|
+
|
206
|
+
if coefficient_matrix is None or constant_vector is None:
|
207
|
+
if self.logger:
|
208
|
+
self.logger.error("Could not extract linear coefficients from equations")
|
209
|
+
return None
|
210
|
+
|
211
|
+
# Check numerical stability via condition number
|
212
|
+
condition_number = np.linalg.cond(coefficient_matrix)
|
213
|
+
if condition_number > self.MAX_CONDITION_NUMBER:
|
214
|
+
if self.logger:
|
215
|
+
# Use debug level for expected fallback scenarios (systems with conditionals)
|
216
|
+
if np.isinf(condition_number):
|
217
|
+
self.logger.debug("Matrix is singular (cond=inf), falling back to iterative solver")
|
218
|
+
else:
|
219
|
+
self.logger.debug(f"System is ill-conditioned (cond={condition_number:.2e}), falling back to iterative solver")
|
220
|
+
return None
|
221
|
+
|
222
|
+
try:
|
223
|
+
# Choose optimal solving algorithm based on system size
|
224
|
+
system_size = coefficient_matrix.shape[0]
|
225
|
+
solution_vector = self._solve_optimized_system(coefficient_matrix, constant_vector, system_size)
|
226
|
+
return solution_vector
|
227
|
+
|
228
|
+
except np.linalg.LinAlgError as linear_algebra_error:
|
229
|
+
if self.logger:
|
230
|
+
self.logger.error(f"Linear algebra error: {linear_algebra_error}")
|
231
|
+
return None
|
232
|
+
|
233
|
+
def _solve_optimized_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray,
|
234
|
+
system_size: int) -> np.ndarray:
|
235
|
+
"""
|
236
|
+
Solve the matrix system using optimized algorithms based on system characteristics.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
coefficient_matrix: The coefficient matrix A
|
240
|
+
constant_vector: The constant vector b
|
241
|
+
system_size: Size of the system (n for n×n)
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Solution vector for the system Ax = b
|
245
|
+
"""
|
246
|
+
if system_size <= self.LARGE_SYSTEM_THRESHOLD:
|
247
|
+
# Use standard NumPy solver for small-medium systems
|
248
|
+
return np.linalg.solve(coefficient_matrix, constant_vector)
|
249
|
+
else:
|
250
|
+
# For large systems, use more efficient algorithms
|
251
|
+
if self.logger:
|
252
|
+
self.logger.debug(f"Using optimized algorithms for large system (n={system_size})")
|
253
|
+
|
254
|
+
# Check matrix density to decide between dense/sparse algorithms
|
255
|
+
density = np.count_nonzero(coefficient_matrix) / coefficient_matrix.size
|
256
|
+
|
257
|
+
if density < self.SPARSE_THRESHOLD:
|
258
|
+
# Use sparse matrix algorithms
|
259
|
+
if self.logger:
|
260
|
+
self.logger.debug(f"Matrix density {density:.3f} < {self.SPARSE_THRESHOLD}, using sparse algorithms")
|
261
|
+
return self._solve_sparse_system(coefficient_matrix, constant_vector)
|
262
|
+
else:
|
263
|
+
# Use optimized dense algorithms
|
264
|
+
if self.logger:
|
265
|
+
self.logger.debug(f"Matrix density {density:.3f} >= {self.SPARSE_THRESHOLD}, using optimized dense algorithms")
|
266
|
+
return self._solve_large_dense_system(coefficient_matrix, constant_vector)
|
267
|
+
|
268
|
+
def _solve_sparse_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray) -> np.ndarray:
|
269
|
+
"""
|
270
|
+
Solve sparse matrix system using scipy.sparse algorithms.
|
271
|
+
|
272
|
+
Note: This is a placeholder for potential scipy.sparse integration.
|
273
|
+
For now, falls back to standard NumPy solver.
|
274
|
+
"""
|
275
|
+
# Future optimization: Convert to scipy.sparse.csc_matrix and use sparse solvers
|
276
|
+
# from scipy.sparse.linalg import spsolve
|
277
|
+
# sparse_matrix = scipy.sparse.csc_matrix(coefficient_matrix)
|
278
|
+
# return spsolve(sparse_matrix, constant_vector)
|
279
|
+
|
280
|
+
# Fallback to standard solver for now
|
281
|
+
return np.linalg.solve(coefficient_matrix, constant_vector)
|
282
|
+
|
283
|
+
def _solve_large_dense_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray) -> np.ndarray:
|
284
|
+
"""
|
285
|
+
Solve large dense matrix system using optimized algorithms.
|
286
|
+
|
287
|
+
Uses LU decomposition with partial pivoting for better numerical stability
|
288
|
+
and potential reuse of factorization.
|
289
|
+
"""
|
290
|
+
# Use scipy.linalg.solve which is optimized for large systems
|
291
|
+
try:
|
292
|
+
# Try importing scipy for better performance on large systems
|
293
|
+
from scipy.linalg import solve # type: ignore[import-untyped]
|
294
|
+
return solve(coefficient_matrix, constant_vector, assume_a='gen')
|
295
|
+
except ImportError:
|
296
|
+
# Fallback to NumPy if scipy is not available
|
297
|
+
return np.linalg.solve(coefficient_matrix, constant_vector)
|
298
|
+
|
299
|
+
def _apply_solution_to_variables(
|
300
|
+
self,
|
301
|
+
unknown_variables: list[str],
|
302
|
+
solution_vector: np.ndarray,
|
303
|
+
working_variables: dict[str, Variable]
|
304
|
+
):
|
305
|
+
"""
|
306
|
+
Apply solution values to variables and record solving steps.
|
307
|
+
"""
|
308
|
+
for i, variable_name in enumerate(unknown_variables):
|
309
|
+
solution_value = float(solution_vector[i])
|
310
|
+
self._update_variable_with_solution(variable_name, solution_value, working_variables)
|
311
|
+
|
312
|
+
def _verify_solution_quality(
|
313
|
+
self,
|
314
|
+
equations: list[Equation],
|
315
|
+
working_variables: dict[str, Variable],
|
316
|
+
tolerance: float
|
317
|
+
) -> SolveResult:
|
318
|
+
"""
|
319
|
+
Verify solution quality by checking equation residuals.
|
320
|
+
|
321
|
+
Returns:
|
322
|
+
SolveResult indicating whether solution meets quality requirements
|
323
|
+
"""
|
324
|
+
max_residual = 0.0
|
325
|
+
for equation in equations:
|
326
|
+
if equation.check_residual(working_variables, tolerance):
|
327
|
+
if self.logger:
|
328
|
+
self.logger.debug(f"Equation {equation.name} verified")
|
329
|
+
else:
|
330
|
+
residual = self._calculate_equation_residual(equation, working_variables)
|
331
|
+
max_residual = max(max_residual, abs(residual))
|
332
|
+
if self.logger:
|
333
|
+
self.logger.warning(f"Equation {equation.name} residual: {residual}")
|
334
|
+
|
335
|
+
is_successful = max_residual < tolerance
|
336
|
+
success_message = "Simultaneous system solved successfully" if is_successful else f"Large residuals detected (max={max_residual:.2e})"
|
337
|
+
|
338
|
+
if self.logger and is_successful:
|
339
|
+
num_unknowns = len([v for v in working_variables.values() if not v.is_known])
|
340
|
+
variable_solutions = {var: f"{working_variables[var].quantity}" for var in working_variables if not working_variables[var].is_known}
|
341
|
+
self.logger.debug(f"Solved {num_unknowns}×{num_unknowns} system: {variable_solutions}")
|
342
|
+
|
343
|
+
return SolveResult(
|
344
|
+
variables=working_variables,
|
345
|
+
steps=[],
|
346
|
+
success=is_successful,
|
347
|
+
message=success_message,
|
348
|
+
method="SimultaneousEquationSolver"
|
349
|
+
)
|
350
|
+
|
351
|
+
def _extract_matrix_system(self, equations: list[Equation], unknown_variables: list[str],
|
352
|
+
variables: dict[str, Variable]) -> tuple[np.ndarray | None, np.ndarray | None]:
|
353
|
+
"""
|
354
|
+
Extract coefficient matrix A and constant vector b from the system of equations.
|
355
|
+
|
356
|
+
Args:
|
357
|
+
equations: List of linear equations to extract coefficients from
|
358
|
+
unknown_variables: List of unknown variable symbols (determines column order)
|
359
|
+
variables: Dictionary of all variables for evaluation
|
360
|
+
|
361
|
+
Returns:
|
362
|
+
Tuple of (coefficient_matrix, constant_vector) for system Ax = b
|
363
|
+
Returns (None, None) if extraction fails
|
364
|
+
|
365
|
+
Algorithm:
|
366
|
+
For each equation, extract coefficients by numerical differentiation:
|
367
|
+
1. Test each unknown variable with value 1, others with 0
|
368
|
+
2. Calculate residual to determine coefficient
|
369
|
+
3. Build coefficient matrix row by row
|
370
|
+
"""
|
371
|
+
try:
|
372
|
+
num_equations = len(equations)
|
373
|
+
|
374
|
+
# Use consistent float64 precision for numerical stability
|
375
|
+
dtype = np.float64
|
376
|
+
|
377
|
+
coefficient_matrix = np.zeros((num_equations, num_equations), dtype=dtype)
|
378
|
+
constant_vector = np.zeros(num_equations, dtype=dtype)
|
379
|
+
|
380
|
+
# Process equations in batches for large systems to reduce memory pressure
|
381
|
+
for equation_index, equation in enumerate(equations):
|
382
|
+
coefficient_list = self._extract_linear_coefficients_vector(
|
383
|
+
equation, unknown_variables, variables
|
384
|
+
)
|
385
|
+
if coefficient_list is None:
|
386
|
+
return None, None
|
387
|
+
|
388
|
+
# coefficient_list contains [a1, a2, ..., an, constant]
|
389
|
+
# where equation is a1*x1 + a2*x2 + ... + an*xn = constant
|
390
|
+
coefficient_matrix[equation_index, :] = coefficient_list[:-1] # Coefficients
|
391
|
+
constant_vector[equation_index] = coefficient_list[-1] # Constant term
|
392
|
+
|
393
|
+
return coefficient_matrix, constant_vector
|
394
|
+
|
395
|
+
except Exception:
|
396
|
+
return None, None
|
397
|
+
|
398
|
+
def _extract_linear_coefficients_vector(self, equation: Equation, unknown_variables: list[str],
|
399
|
+
variables: dict[str, Variable]) -> list[float] | None:
|
400
|
+
"""
|
401
|
+
Extract linear coefficients from equation using numerical differentiation.
|
402
|
+
|
403
|
+
Args:
|
404
|
+
equation: The equation to extract coefficients from
|
405
|
+
unknown_variables: List of unknown variable symbols
|
406
|
+
variables: Dictionary of all variables
|
407
|
+
|
408
|
+
Returns:
|
409
|
+
List [a1, a2, ..., an, c] for equation a1*x1 + a2*x2 + ... + an*xn = c
|
410
|
+
Returns None if extraction fails
|
411
|
+
|
412
|
+
Algorithm:
|
413
|
+
Uses finite difference approximation:
|
414
|
+
1. Set all unknowns to 0, calculate residual → gives constant term
|
415
|
+
2. Set one unknown to 1, others to 0 → gives coefficient for that variable
|
416
|
+
3. Repeat for all unknowns to build coefficient vector
|
417
|
+
"""
|
418
|
+
try:
|
419
|
+
num_unknowns = len(unknown_variables)
|
420
|
+
coefficients = []
|
421
|
+
|
422
|
+
# Memory optimization: Pre-allocate arrays for large systems
|
423
|
+
residual_test_cases: np.ndarray | list[float] = np.zeros(num_unknowns) if num_unknowns > 10 else []
|
424
|
+
|
425
|
+
# Reuse test variables dictionary to reduce object creation overhead
|
426
|
+
test_vars = variables.copy()
|
427
|
+
|
428
|
+
# Test case for each unknown variable (finite difference)
|
429
|
+
for variable_index in range(num_unknowns):
|
430
|
+
# Reset all unknowns to 0, then set current one to 1
|
431
|
+
for unknown_index, var_name in enumerate(unknown_variables):
|
432
|
+
test_value = 1.0 if unknown_index == variable_index else 0.0
|
433
|
+
original_var = test_vars[var_name]
|
434
|
+
if original_var.quantity is None:
|
435
|
+
raise ValueError(f"Variable {var_name} has no quantity")
|
436
|
+
test_var = Variable(
|
437
|
+
name=f"test_{var_name}",
|
438
|
+
expected_dimension=original_var.quantity.dimension,
|
439
|
+
is_known=True
|
440
|
+
)
|
441
|
+
test_var.quantity = Qty(test_value, original_var.quantity.unit)
|
442
|
+
test_var.symbol = var_name
|
443
|
+
test_vars[var_name] = test_var
|
444
|
+
|
445
|
+
residual = self._calculate_equation_residual(equation, test_vars)
|
446
|
+
if isinstance(residual_test_cases, list):
|
447
|
+
residual_test_cases.append(residual)
|
448
|
+
else:
|
449
|
+
residual_test_cases[variable_index] = residual
|
450
|
+
|
451
|
+
# Test case with all unknowns = 0 (baseline)
|
452
|
+
for var_name in unknown_variables:
|
453
|
+
original_var = test_vars[var_name]
|
454
|
+
if original_var.quantity is None:
|
455
|
+
raise ValueError(f"Variable {var_name} has no quantity")
|
456
|
+
test_var = Variable(
|
457
|
+
name=f"test_{var_name}",
|
458
|
+
expected_dimension=original_var.quantity.dimension,
|
459
|
+
is_known=True
|
460
|
+
)
|
461
|
+
test_var.quantity = Qty(0.0, original_var.quantity.unit)
|
462
|
+
test_var.symbol = var_name
|
463
|
+
test_vars[var_name] = test_var
|
464
|
+
baseline_residual = self._calculate_equation_residual(equation, test_vars)
|
465
|
+
|
466
|
+
# Extract coefficients: for equation sum(ai*xi) - c = 0
|
467
|
+
# When xi=1, xj=0 (j≠i): ai - c = residual_i → ai = residual_i + c
|
468
|
+
# When all xi=0: -c = baseline_residual → c = -baseline_residual
|
469
|
+
|
470
|
+
constant_term = -baseline_residual
|
471
|
+
for residual in residual_test_cases:
|
472
|
+
coefficient = residual + constant_term
|
473
|
+
coefficients.append(coefficient)
|
474
|
+
|
475
|
+
coefficients.append(constant_term) # Add constant term
|
476
|
+
return coefficients
|
477
|
+
|
478
|
+
except Exception:
|
479
|
+
return None
|
480
|
+
|
481
|
+
def _calculate_equation_residual(self, equation: Equation, test_variables: dict[str, Variable]) -> float:
|
482
|
+
"""
|
483
|
+
Calculate equation residual (LHS - RHS) with proper unit handling.
|
484
|
+
|
485
|
+
Args:
|
486
|
+
equation: The equation to evaluate
|
487
|
+
test_variables: Dictionary of variables with test values
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
Numerical residual value (dimensionless)
|
491
|
+
Returns infinity if evaluation fails
|
492
|
+
"""
|
493
|
+
try:
|
494
|
+
left_hand_side = equation.lhs.evaluate(test_variables)
|
495
|
+
right_hand_side = equation.rhs.evaluate(test_variables)
|
496
|
+
|
497
|
+
# Calculate residual with unit handling
|
498
|
+
residual = left_hand_side - right_hand_side
|
499
|
+
|
500
|
+
# Return the magnitude of the residual
|
501
|
+
if hasattr(residual, 'value'):
|
502
|
+
return residual.value
|
503
|
+
elif isinstance(residual, int | float):
|
504
|
+
return float(residual)
|
505
|
+
else:
|
506
|
+
# If it's a Qty object without .value, try to convert
|
507
|
+
return float(residual.value) if hasattr(residual, 'value') else 0.0
|
508
|
+
|
509
|
+
except Exception:
|
510
|
+
# Fallback for cases where evaluation fails
|
511
|
+
return float('inf')
|
512
|
+
|
513
|
+
def _update_variable_with_solution(self, variable_symbol: str, solution_value: float,
|
514
|
+
variables: dict[str, Variable]):
|
515
|
+
"""
|
516
|
+
Update a variable with its solved value and record the solving step.
|
517
|
+
|
518
|
+
Args:
|
519
|
+
variable_symbol: Symbol of the variable to update
|
520
|
+
solution_value: Numerical solution value
|
521
|
+
variables: Dictionary of variables to update
|
522
|
+
"""
|
523
|
+
original_variable = variables[variable_symbol]
|
524
|
+
if original_variable.quantity is None:
|
525
|
+
raise ValueError(f"Variable {variable_symbol} has no quantity")
|
526
|
+
result_unit = original_variable.quantity.unit
|
527
|
+
solution_quantity = Qty(solution_value, result_unit)
|
528
|
+
|
529
|
+
# Preserve the original variable name and create solved variable
|
530
|
+
original_name = original_variable.name
|
531
|
+
solved_variable = Variable(
|
532
|
+
name=original_name,
|
533
|
+
expected_dimension=solution_quantity.dimension,
|
534
|
+
is_known=True
|
535
|
+
)
|
536
|
+
solved_variable.quantity = solution_quantity
|
537
|
+
solved_variable.symbol = variable_symbol
|
538
|
+
variables[variable_symbol] = solved_variable
|
539
|
+
|
540
|
+
# Record solving step for tracking
|
541
|
+
self._log_step(
|
542
|
+
1, # iteration number
|
543
|
+
variable_symbol,
|
544
|
+
'simultaneous_system',
|
545
|
+
str(solution_quantity),
|
546
|
+
'simultaneous'
|
547
|
+
)
|
qnty/units/__init__.py
ADDED
File without changes
|