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,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