qnty 0.0.9__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. qnty/__init__.py +2 -3
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +540 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1101 -0
  21. qnty/problems/problem.py +737 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
  54. qnty-0.1.1.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/WHEEL +0 -0
@@ -2,11 +2,19 @@ from typing import Any
2
2
 
3
3
  import numpy as np
4
4
 
5
- from qnty.equations.equation import Equation
6
- from qnty.quantities import Quantity as Qty
7
- from qnty.quantities import TypeSafeVariable as Variable
5
+ try:
6
+ from scipy.linalg import solve as scipy_solve # type: ignore[import-untyped]
7
+
8
+ HAS_SCIPY = True
9
+ except ImportError:
10
+ HAS_SCIPY = False
11
+ scipy_solve = None
12
+
8
13
  from qnty.solving.order import Order
9
14
 
15
+ from ...equations import Equation
16
+ from ...quantities import Quantity
17
+ from ...quantities.field_qnty import FieldQnty
10
18
  from .base import BaseSolver, SolveResult
11
19
 
12
20
 
@@ -44,9 +52,7 @@ class SimultaneousEquationSolver(BaseSolver):
44
52
  LARGE_SYSTEM_THRESHOLD = 100 # Switch to optimized algorithms for n > 100
45
53
  SPARSE_THRESHOLD = 0.1 # Use sparse matrices if density < 10%
46
54
 
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:
55
+ def can_handle(self, equations: list[Equation], unknowns: set[str], dependency_graph: Order | None = None, analysis: dict[str, Any] | None = None) -> bool:
50
56
  """
51
57
  Determine if this solver can handle the given system.
52
58
 
@@ -64,7 +70,7 @@ class SimultaneousEquationSolver(BaseSolver):
64
70
  """
65
71
  # dependency_graph parameter unused but required for interface compatibility
66
72
  _ = dependency_graph
67
-
73
+
68
74
  system_size = len(equations)
69
75
  num_unknowns = len(unknowns)
70
76
 
@@ -75,17 +81,10 @@ class SimultaneousEquationSolver(BaseSolver):
75
81
  # Only handle systems with cycles (mutual dependencies)
76
82
  if analysis is None:
77
83
  return False
78
- has_cycles = analysis.get('has_cycles', False)
84
+ has_cycles = analysis.get("has_cycles", False)
79
85
  return bool(has_cycles)
80
86
 
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:
87
+ def solve(self, equations: list[Equation], variables: dict[str, FieldQnty], dependency_graph: Order | None = None, max_iterations: int = 100, tolerance: float = DEFAULT_TOLERANCE) -> SolveResult:
89
88
  """
90
89
  Solve the n×n simultaneous system using matrix operations.
91
90
 
@@ -116,49 +115,24 @@ class SimultaneousEquationSolver(BaseSolver):
116
115
 
117
116
  # Step 2: Extract and solve matrix system
118
117
  try:
119
- solution_vector = self._solve_matrix_system(
120
- equations, unknown_variable_names, working_variables
121
- )
118
+ solution_vector = self._solve_matrix_system(equations, unknown_variable_names, working_variables)
122
119
  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
- )
120
+ return SolveResult(variables=working_variables, steps=self.steps, success=False, message="Failed to solve matrix system", method="SimultaneousEquationSolver")
130
121
 
131
122
  # Step 3: Update variables with solutions
132
- self._apply_solution_to_variables(
133
- unknown_variable_names, solution_vector, working_variables
134
- )
123
+ self._apply_solution_to_variables(unknown_variable_names, solution_vector, working_variables)
135
124
 
136
125
  # Step 4: Verify solution quality
137
- verification_result = self._verify_solution_quality(
138
- equations, working_variables, tolerance
139
- )
126
+ verification_result = self._verify_solution_quality(equations, working_variables, tolerance)
140
127
 
141
128
  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
129
+ variables=working_variables, steps=self.steps, success=verification_result.success, message=verification_result.message, method="SimultaneousEquationSolver", iterations=1
148
130
  )
149
131
 
150
132
  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
- )
133
+ return SolveResult(variables=working_variables, steps=self.steps, success=False, message=f"Simultaneous solving failed: {general_error}", method="SimultaneousEquationSolver")
158
134
 
159
- def _validate_system(
160
- self, equations: list[Equation], variables: dict[str, Variable]
161
- ) -> SolveResult:
135
+ def _validate_system(self, equations: list[Equation], variables: dict[str, FieldQnty]) -> SolveResult:
162
136
  """
163
137
  Validate that the system meets requirements for simultaneous solving.
164
138
 
@@ -176,24 +150,15 @@ class SimultaneousEquationSolver(BaseSolver):
176
150
  steps=self.steps,
177
151
  success=False,
178
152
  message=f"Simultaneous solver requires n×n system (got {num_equations} equations, {num_unknowns} unknowns)",
179
- method="SimultaneousEquationSolver"
153
+ method="SimultaneousEquationSolver",
180
154
  )
181
155
 
182
156
  if self.logger:
183
- self.logger.debug(
184
- f"Attempting {num_unknowns}×{num_unknowns} simultaneous solution for {unknown_variable_names}"
185
- )
157
+ self.logger.debug(f"Attempting {num_unknowns}×{num_unknowns} simultaneous solution for {unknown_variable_names}")
186
158
 
187
- return SolveResult(
188
- variables=variables,
189
- steps=self.steps,
190
- success=True,
191
- message="System validation passed",
192
- method="SimultaneousEquationSolver"
193
- )
159
+ return SolveResult(variables=variables, steps=self.steps, success=True, message="System validation passed", method="SimultaneousEquationSolver")
194
160
 
195
- def _solve_matrix_system(self, equations: list[Equation], unknown_variables: list[str],
196
- working_variables: dict[str, Variable]) -> np.ndarray | None:
161
+ def _solve_matrix_system(self, equations: list[Equation], unknown_variables: list[str], working_variables: dict[str, FieldQnty]) -> np.ndarray | None:
197
162
  """
198
163
  Extract coefficient matrix and solve the linear system.
199
164
 
@@ -230,8 +195,7 @@ class SimultaneousEquationSolver(BaseSolver):
230
195
  self.logger.error(f"Linear algebra error: {linear_algebra_error}")
231
196
  return None
232
197
 
233
- def _solve_optimized_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray,
234
- system_size: int) -> np.ndarray:
198
+ def _solve_optimized_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray, system_size: int) -> np.ndarray:
235
199
  """
236
200
  Solve the matrix system using optimized algorithms based on system characteristics.
237
201
 
@@ -267,41 +231,19 @@ class SimultaneousEquationSolver(BaseSolver):
267
231
 
268
232
  def _solve_sparse_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray) -> np.ndarray:
269
233
  """
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.
234
+ Solve sparse matrix system. Currently falls back to dense solver.
274
235
  """
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
236
  return np.linalg.solve(coefficient_matrix, constant_vector)
282
237
 
283
238
  def _solve_large_dense_system(self, coefficient_matrix: np.ndarray, constant_vector: np.ndarray) -> np.ndarray:
284
239
  """
285
240
  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
241
  """
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)
242
+ if HAS_SCIPY and scipy_solve is not None:
243
+ return scipy_solve(coefficient_matrix, constant_vector, assume_a="gen")
244
+ return np.linalg.solve(coefficient_matrix, constant_vector)
298
245
 
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
- ):
246
+ def _apply_solution_to_variables(self, unknown_variables: list[str], solution_vector: np.ndarray, working_variables: dict[str, FieldQnty]):
305
247
  """
306
248
  Apply solution values to variables and record solving steps.
307
249
  """
@@ -309,12 +251,7 @@ class SimultaneousEquationSolver(BaseSolver):
309
251
  solution_value = float(solution_vector[i])
310
252
  self._update_variable_with_solution(variable_name, solution_value, working_variables)
311
253
 
312
- def _verify_solution_quality(
313
- self,
314
- equations: list[Equation],
315
- working_variables: dict[str, Variable],
316
- tolerance: float
317
- ) -> SolveResult:
254
+ def _verify_solution_quality(self, equations: list[Equation], working_variables: dict[str, FieldQnty], tolerance: float) -> SolveResult:
318
255
  """
319
256
  Verify solution quality by checking equation residuals.
320
257
 
@@ -340,16 +277,9 @@ class SimultaneousEquationSolver(BaseSolver):
340
277
  variable_solutions = {var: f"{working_variables[var].quantity}" for var in working_variables if not working_variables[var].is_known}
341
278
  self.logger.debug(f"Solved {num_unknowns}×{num_unknowns} system: {variable_solutions}")
342
279
 
343
- return SolveResult(
344
- variables=working_variables,
345
- steps=[],
346
- success=is_successful,
347
- message=success_message,
348
- method="SimultaneousEquationSolver"
349
- )
280
+ return SolveResult(variables=working_variables, steps=[], success=is_successful, message=success_message, method="SimultaneousEquationSolver")
350
281
 
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]:
282
+ def _extract_matrix_system(self, equations: list[Equation], unknown_variables: list[str], variables: dict[str, FieldQnty]) -> tuple[np.ndarray | None, np.ndarray | None]:
353
283
  """
354
284
  Extract coefficient matrix A and constant vector b from the system of equations.
355
285
 
@@ -379,24 +309,21 @@ class SimultaneousEquationSolver(BaseSolver):
379
309
 
380
310
  # Process equations in batches for large systems to reduce memory pressure
381
311
  for equation_index, equation in enumerate(equations):
382
- coefficient_list = self._extract_linear_coefficients_vector(
383
- equation, unknown_variables, variables
384
- )
312
+ coefficient_list = self._extract_linear_coefficients_vector(equation, unknown_variables, variables)
385
313
  if coefficient_list is None:
386
314
  return None, None
387
315
 
388
316
  # coefficient_list contains [a1, a2, ..., an, constant]
389
317
  # where equation is a1*x1 + a2*x2 + ... + an*xn = constant
390
318
  coefficient_matrix[equation_index, :] = coefficient_list[:-1] # Coefficients
391
- constant_vector[equation_index] = coefficient_list[-1] # Constant term
319
+ constant_vector[equation_index] = coefficient_list[-1] # Constant term
392
320
 
393
321
  return coefficient_matrix, constant_vector
394
322
 
395
323
  except Exception:
396
324
  return None, None
397
325
 
398
- def _extract_linear_coefficients_vector(self, equation: Equation, unknown_variables: list[str],
399
- variables: dict[str, Variable]) -> list[float] | None:
326
+ def _extract_linear_coefficients_vector(self, equation: Equation, unknown_variables: list[str], variables: dict[str, FieldQnty]) -> list[float] | None:
400
327
  """
401
328
  Extract linear coefficients from equation using numerical differentiation.
402
329
 
@@ -420,47 +347,19 @@ class SimultaneousEquationSolver(BaseSolver):
420
347
  coefficients = []
421
348
 
422
349
  # 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 []
350
+ residual_test_cases: list[float] = []
424
351
 
425
352
  # Reuse test variables dictionary to reduce object creation overhead
426
353
  test_vars = variables.copy()
427
354
 
428
355
  # Test case for each unknown variable (finite difference)
429
356
  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
-
357
+ self._set_test_variables(test_vars, unknown_variables, variable_index)
445
358
  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
359
+ residual_test_cases.append(residual)
450
360
 
451
361
  # 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
362
+ self._set_test_variables(test_vars, unknown_variables, -1) # -1 means set all to 0
464
363
  baseline_residual = self._calculate_equation_residual(equation, test_vars)
465
364
 
466
365
  # Extract coefficients: for equation sum(ai*xi) - c = 0
@@ -478,7 +377,7 @@ class SimultaneousEquationSolver(BaseSolver):
478
377
  except Exception:
479
378
  return None
480
379
 
481
- def _calculate_equation_residual(self, equation: Equation, test_variables: dict[str, Variable]) -> float:
380
+ def _calculate_equation_residual(self, equation: Equation, test_variables: dict[str, FieldQnty]) -> float:
482
381
  """
483
382
  Calculate equation residual (LHS - RHS) with proper unit handling.
484
383
 
@@ -494,24 +393,15 @@ class SimultaneousEquationSolver(BaseSolver):
494
393
  left_hand_side = equation.lhs.evaluate(test_variables)
495
394
  right_hand_side = equation.rhs.evaluate(test_variables)
496
395
 
497
- # Calculate residual with unit handling
396
+ # Calculate residual and extract numerical value
498
397
  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
398
+ return self._extract_numerical_value(residual)
508
399
 
509
400
  except Exception:
510
401
  # Fallback for cases where evaluation fails
511
- return float('inf')
402
+ return float("inf")
512
403
 
513
- def _update_variable_with_solution(self, variable_symbol: str, solution_value: float,
514
- variables: dict[str, Variable]):
404
+ def _update_variable_with_solution(self, variable_symbol: str, solution_value: float, variables: dict[str, FieldQnty]):
515
405
  """
516
406
  Update a variable with its solved value and record the solving step.
517
407
 
@@ -524,15 +414,11 @@ class SimultaneousEquationSolver(BaseSolver):
524
414
  if original_variable.quantity is None:
525
415
  raise ValueError(f"Variable {variable_symbol} has no quantity")
526
416
  result_unit = original_variable.quantity.unit
527
- solution_quantity = Qty(solution_value, result_unit)
417
+ solution_quantity = Quantity(solution_value, result_unit)
528
418
 
529
419
  # Preserve the original variable name and create solved variable
530
420
  original_name = original_variable.name
531
- solved_variable = Variable(
532
- name=original_name,
533
- expected_dimension=solution_quantity.dimension,
534
- is_known=True
535
- )
421
+ solved_variable = FieldQnty(name=original_name, expected_dimension=solution_quantity.dimension, is_known=True)
536
422
  solved_variable.quantity = solution_quantity
537
423
  solved_variable.symbol = variable_symbol
538
424
  variables[variable_symbol] = solved_variable
@@ -541,7 +427,49 @@ class SimultaneousEquationSolver(BaseSolver):
541
427
  self._log_step(
542
428
  1, # iteration number
543
429
  variable_symbol,
544
- 'simultaneous_system',
430
+ "simultaneous_system",
545
431
  str(solution_quantity),
546
- 'simultaneous'
432
+ "simultaneous",
547
433
  )
434
+
435
+ def _set_test_variables(self, test_vars: dict[str, FieldQnty], unknown_variables: list[str], active_index: int):
436
+ """
437
+ Set test variables for coefficient extraction.
438
+
439
+ Args:
440
+ test_vars: Dictionary of test variables to modify
441
+ unknown_variables: List of unknown variable names
442
+ active_index: Index of variable to set to 1.0, others set to 0.0. If -1, all set to 0.0
443
+ """
444
+ for unknown_index, var_name in enumerate(unknown_variables):
445
+ test_value = 1.0 if unknown_index == active_index else 0.0
446
+ original_var = test_vars[var_name]
447
+ if original_var.quantity is None:
448
+ raise ValueError(f"Variable {var_name} has no quantity")
449
+ test_var = FieldQnty(name=f"test_{var_name}", expected_dimension=original_var.quantity.dimension, is_known=True)
450
+ test_var.quantity = Quantity(test_value, original_var.quantity.unit)
451
+ test_var.symbol = var_name
452
+ test_vars[var_name] = test_var
453
+
454
+ def _extract_numerical_value(self, value: Any) -> float:
455
+ """
456
+ Extract numerical value from various quantity types.
457
+
458
+ Args:
459
+ value: Value that may be a Quantity, float, int, or other numeric type
460
+
461
+ Returns:
462
+ Float representation of the value
463
+ """
464
+ # Check for Quantity type first (most common case)
465
+ if isinstance(value, Quantity):
466
+ return float(value.value)
467
+ # Handle primitive numeric types
468
+ elif isinstance(value, int | float):
469
+ return float(value)
470
+ # Handle objects with .value attribute as last resort
471
+ elif hasattr(value, "value"):
472
+ return float(value.value)
473
+ else:
474
+ # Last resort: try direct conversion
475
+ return float(value)
qnty/units/__init__.py CHANGED
@@ -0,0 +1 @@
1
+ from .field_units import *