qnty 0.1.0__tar.gz → 0.1.2__tar.gz

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 (61) hide show
  1. {qnty-0.1.0 → qnty-0.1.2}/PKG-INFO +1 -1
  2. {qnty-0.1.0 → qnty-0.1.2}/pyproject.toml +1 -1
  3. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/nodes.py +6 -5
  4. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/composition.py +77 -7
  5. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/problem.py +70 -14
  6. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_qnty.py +14 -2
  7. qnty-0.1.2/src/qnty/quantities/field_setter.pyi +6620 -0
  8. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_vars.py +1395 -322
  9. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_vars.pyi +1505 -2140
  10. {qnty-0.1.0 → qnty-0.1.2}/README.md +0 -0
  11. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/__init__.py +0 -0
  12. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/__init__.py +0 -0
  13. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/numerical.py +0 -0
  14. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/solvers.py +0 -0
  15. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/tests.py +0 -0
  16. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/__init__.py +0 -0
  17. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/base.py +0 -0
  18. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/field_dims.py +0 -0
  19. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/field_dims.pyi +0 -0
  20. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/signature.py +0 -0
  21. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/equations/__init__.py +0 -0
  22. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/equations/equation.py +0 -0
  23. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/equations/system.py +0 -0
  24. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/__init__.py +0 -0
  25. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/formatter.py +0 -0
  26. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/functions.py +0 -0
  27. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/types.py +0 -0
  28. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/__init__.py +0 -0
  29. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/integration/__init__.py +0 -0
  30. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/plotting/__init__.py +0 -0
  31. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/reporting/__init__.py +0 -0
  32. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/__init__.py +0 -0
  33. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/rules.py +0 -0
  34. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/solving.py +0 -0
  35. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/validation.py +0 -0
  36. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/__init__.py +0 -0
  37. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/base_qnty.py +0 -0
  38. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_converters.py +0 -0
  39. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_setter.py +0 -0
  40. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/__init__.py +0 -0
  41. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/manager.py +0 -0
  42. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/order.py +0 -0
  43. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/__init__.py +0 -0
  44. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/base.py +0 -0
  45. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/iterative.py +0 -0
  46. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/simultaneous.py +0 -0
  47. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/__init__.py +0 -0
  48. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/field_units.py +0 -0
  49. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/field_units.pyi +0 -0
  50. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/prefixes.py +0 -0
  51. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/registry.py +0 -0
  52. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/__init__.py +0 -0
  53. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/caching/__init__.py +0 -0
  54. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/caching/manager.py +0 -0
  55. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/__init__.py +0 -0
  56. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/context.py +0 -0
  57. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/exceptions.py +0 -0
  58. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/handlers.py +0 -0
  59. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/logging.py +0 -0
  60. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/protocols.py +0 -0
  61. {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/scope_discovery.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qnty
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: High-performance unit system library for Python with dimensional safety and fast unit conversions
5
5
  License: Apache-2.0
6
6
  Keywords: units,dimensional analysis,engineering,physics,quantities,measurements
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qnty"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "High-performance unit system library for Python with dimensional safety and fast unit conversions"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -374,15 +374,16 @@ class BinaryOperation(Expression):
374
374
  if abs(right_value) < DIVISION_BY_ZERO_THRESHOLD:
375
375
  raise ValueError(f"Division by zero in expression: {self}")
376
376
 
377
- # Fast paths
378
- if right_value == 1.0:
377
+ # Fast paths - but only when they preserve correct dimensional analysis
378
+ if right_value == 1.0 and right_val._dimension_sig == 1:
379
+ # Division by dimensionless 1.0 - safe optimization
379
380
  return left_val
380
381
  elif left_value == 0.0:
381
382
  # Zero divided by anything is zero (with appropriate unit)
382
383
  return Quantity(0.0, (left_val / right_val).unit)
383
- elif right_value == -1.0:
384
- # Division by -1 is negation
385
- return Quantity(-left_value, (left_val / right_val).unit)
384
+ elif right_value == -1.0 and right_val._dimension_sig == 1:
385
+ # Division by dimensionless -1.0 is negation - safe optimization
386
+ return Quantity(-left_value, left_val.unit)
386
387
 
387
388
  # Regular division
388
389
  return left_val / right_val
@@ -133,6 +133,7 @@ class SubProblemProxy:
133
133
  is_known=original_var.is_known,
134
134
  proxy=self,
135
135
  original_symbol=original_var.symbol,
136
+ original_variable=original_var, # Pass the original variable for type preservation
136
137
  )
137
138
 
138
139
  return namespaced_var
@@ -152,16 +153,25 @@ class ConfigurableVariable:
152
153
  This acts as a proxy around the actual qnty Variable rather than inheriting from it.
153
154
  """
154
155
 
155
- def __init__(self, symbol, name, quantity, is_known=True, proxy=None, original_symbol=None):
156
+ def __init__(self, symbol, name, quantity, is_known=True, proxy=None, original_symbol=None, original_variable=None):
156
157
  # Store the actual variable (we'll delegate to it)
157
158
  # Create a variable of the appropriate type based on the original
158
- # For now, we'll create a Dimensionless variable and update it
159
- self._variable = Dimensionless(name)
159
+ if original_variable is not None:
160
+ # Preserve the original variable type
161
+ self._variable = type(original_variable)(name)
162
+ else:
163
+ # Fallback to Dimensionless if no original variable provided
164
+ self._variable = Dimensionless(name)
160
165
 
161
166
  # Set the properties
162
167
  self._variable.symbol = symbol
163
168
  self._variable.quantity = quantity
164
169
  self._variable.is_known = is_known
170
+
171
+ # Ensure the arithmetic mode is properly initialized
172
+ # The __init__ should have set it, but in case it didn't
173
+ if not hasattr(self._variable, '_arithmetic_mode'):
174
+ self._variable._arithmetic_mode = "expression"
165
175
 
166
176
  # Store proxy information
167
177
  self._proxy = proxy
@@ -169,6 +179,13 @@ class ConfigurableVariable:
169
179
 
170
180
  def __getattr__(self, name):
171
181
  """Delegate all other attributes to the wrapped variable."""
182
+ # Handle special case for _arithmetic_mode since it's accessed in __mul__ etc.
183
+ if name == '_arithmetic_mode':
184
+ # First check if we have it directly
185
+ if hasattr(self._variable, '_arithmetic_mode'):
186
+ return self._variable._arithmetic_mode
187
+ # Otherwise default to 'expression'
188
+ return 'expression'
172
189
  return getattr(self._variable, name)
173
190
 
174
191
  # Delegate arithmetic operations to the wrapped variable
@@ -230,11 +247,64 @@ class ConfigurableVariable:
230
247
  setattr(self._variable, name, value)
231
248
 
232
249
  def set(self, value):
233
- """Override set method to track configuration changes."""
234
- result = self._variable.set(value)
250
+ """Override set method to track configuration changes and return the correct setter."""
251
+ # Create a wrapper setter that tracks changes after unit application
252
+ original_setter = self._variable.set(value)
253
+
235
254
  if self._proxy and self._original_symbol:
236
- # Track this configuration change
237
- self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
255
+ # Create tracking wrapper
256
+ return TrackingSetterWrapper(original_setter, self._proxy, self._original_symbol, self._variable)
257
+ else:
258
+ return original_setter
259
+
260
+
261
+ class TrackingSetterWrapper:
262
+ """
263
+ A wrapper around setter objects that tracks configuration changes after unit application.
264
+ This ensures that when `proxy.set(value).unit` is called, the proxy tracks the final
265
+ configuration after the unit is applied.
266
+ """
267
+
268
+ def __init__(self, original_setter, proxy, original_symbol, variable):
269
+ self._original_setter = original_setter
270
+ self._proxy = proxy
271
+ self._original_symbol = original_symbol
272
+ self._variable = variable
273
+
274
+ def __getattr__(self, name):
275
+ """
276
+ Intercept property access for unit properties and track configuration after application.
277
+ """
278
+ # Get the property from the original setter
279
+ attr = getattr(self._original_setter, name)
280
+
281
+ # If it's a property (unit setter), wrap it to track configuration
282
+ if hasattr(attr, '__call__'):
283
+ # It's a method, just delegate
284
+ return attr
285
+ else:
286
+ # It's a property access, call it and then track configuration
287
+ result = attr # This will set the variable quantity and return the variable
288
+
289
+ # Track the configuration after the unit is applied
290
+ if self._proxy and self._original_symbol:
291
+ self._proxy.track_configuration(
292
+ self._original_symbol,
293
+ self._variable.quantity,
294
+ self._variable.is_known
295
+ )
296
+
297
+ return result
298
+
299
+ def with_unit(self, unit):
300
+ """Delegate with_unit method and track configuration."""
301
+ result = self._original_setter.with_unit(unit)
302
+ if self._proxy and self._original_symbol:
303
+ self._proxy.track_configuration(
304
+ self._original_symbol,
305
+ self._variable.quantity,
306
+ self._variable.is_known
307
+ )
238
308
  return result
239
309
 
240
310
  def update(self, value=None, unit=None, quantity=None, is_known=None):
@@ -177,6 +177,10 @@ class Problem(ValidationMixin):
177
177
  # Sub-problem composition support
178
178
  self.sub_problems: dict[str, Any] = {}
179
179
  self.variable_aliases: dict[str, str] = {}
180
+
181
+ # Track original variable states for re-solving
182
+ self._original_variable_states: dict[str, bool] = {}
183
+ self._original_variable_units: dict[str, Any] = {}
180
184
 
181
185
  # Initialize equation reconstructor
182
186
  self.equation_reconstructor = None
@@ -227,6 +231,11 @@ class Problem(ValidationMixin):
227
231
 
228
232
  if variable.symbol is not None:
229
233
  self.variables[variable.symbol] = variable
234
+ # Track original is_known state and unit for re-solving
235
+ self._original_variable_states[variable.symbol] = variable.is_known
236
+ # Store the unit from original quantity to preserve unit info
237
+ if variable.quantity is not None:
238
+ self._original_variable_units[variable.symbol] = variable.quantity.unit
230
239
  # Set parent problem reference for dependency invalidation
231
240
  if hasattr(variable, "_parent_problem"):
232
241
  setattr(variable, "_parent_problem", self)
@@ -353,11 +362,11 @@ class Problem(ValidationMixin):
353
362
  # This ensures domain-specific variables (Length, Pressure, etc.) keep their type
354
363
  variable_type = type(variable)
355
364
 
356
- # Use __new__ to avoid constructor parameter issues
357
- cloned = variable_type.__new__(variable_type)
365
+ # Create a new instance properly initialized
366
+ # Pass the name to the constructor which all FieldQnty subclasses accept
367
+ cloned = variable_type(variable.name)
358
368
 
359
- # Initialize manually with the same attributes as the original
360
- cloned.name = variable.name
369
+ # Copy over the attributes from the original
361
370
  cloned.symbol = variable.symbol
362
371
  cloned.expected_dimension = variable.expected_dimension
363
372
  cloned.quantity = variable.quantity # Keep reference to same quantity - units must not be copied
@@ -368,6 +377,37 @@ class Problem(ValidationMixin):
368
377
  cloned.validation_checks = []
369
378
  return cloned
370
379
 
380
+ def _update_variables_with_solution(self, solved_variables: dict[str, FieldQnty]):
381
+ """
382
+ Update variables with solution, preserving original units for display.
383
+ """
384
+ for symbol, solved_var in solved_variables.items():
385
+ if symbol in self.variables:
386
+ original_var = self.variables[symbol]
387
+
388
+ # If we have a solved quantity and an original unit to preserve
389
+ if (solved_var.quantity is not None and
390
+ symbol in self._original_variable_units and
391
+ symbol in self._original_variable_states and
392
+ not self._original_variable_states[symbol]): # Was originally unknown
393
+
394
+ # Convert solved quantity to original unit for display
395
+ original_unit = self._original_variable_units[symbol]
396
+ try:
397
+ # Convert the solved quantity to the original unit
398
+ converted_value = solved_var.quantity.to(original_unit).value
399
+ from ..quantities.base_qnty import Quantity
400
+ original_var.quantity = Quantity(converted_value, original_unit)
401
+ original_var.is_known = True
402
+ except Exception:
403
+ # If conversion fails, use the solved quantity as-is
404
+ original_var.quantity = solved_var.quantity
405
+ original_var.is_known = solved_var.is_known
406
+ else:
407
+ # For originally known variables or if no unit conversion needed
408
+ original_var.quantity = solved_var.quantity
409
+ original_var.is_known = solved_var.is_known
410
+
371
411
  def _sync_variables_to_instance_attributes(self):
372
412
  """
373
413
  Sync variable objects to instance attributes after solving.
@@ -539,10 +579,8 @@ class Problem(ValidationMixin):
539
579
  self.logger.info(f"Solving problem: {self.name}")
540
580
 
541
581
  try:
542
- # Clear previous solution
543
- self.solution = {}
544
- self.is_solved = False
545
- self.solving_history = []
582
+ # Reset solution state and restore original variable states
583
+ self.reset_solution()
546
584
 
547
585
  # Build dependency graph
548
586
  self._build_dependency_graph()
@@ -551,8 +589,8 @@ class Problem(ValidationMixin):
551
589
  solve_result = self.solver_manager.solve(self.equations, self.variables, self.dependency_graph, max_iterations, tolerance)
552
590
 
553
591
  if solve_result.success:
554
- # Update variables with the result
555
- self.variables = solve_result.variables
592
+ # Update variables with the result, preserving original units where possible
593
+ self._update_variables_with_solution(solve_result.variables)
556
594
  self.solving_history.extend(solve_result.steps)
557
595
 
558
596
  # Sync solved values back to instance attributes
@@ -641,10 +679,14 @@ class Problem(ValidationMixin):
641
679
  self.solution = {}
642
680
  self.solving_history = []
643
681
 
644
- # Reset unknown variables to unknown state
645
- for var in self.variables.values():
646
- if not var.is_known:
647
- var.is_known = False
682
+ # Reset variables to their original known/unknown states
683
+ for symbol, var in self.variables.items():
684
+ if symbol in self._original_variable_states:
685
+ original_state = self._original_variable_states[symbol]
686
+ var.is_known = original_state
687
+ # If variable was originally unknown, reset it to None so solver can update it
688
+ if not original_state:
689
+ var.quantity = None
648
690
 
649
691
  def copy(self):
650
692
  """Create a copy of this problem."""
@@ -672,6 +714,20 @@ class Problem(ValidationMixin):
672
714
 
673
715
  super().__setattr__(name, value)
674
716
 
717
+ def __getattr__(self, name: str) -> Any:
718
+ """Dynamic attribute access for composed variables and other attributes."""
719
+ # Avoid recursion by checking the dict directly instead of using hasattr
720
+ try:
721
+ variables = object.__getattribute__(self, "variables")
722
+ if name in variables:
723
+ return variables[name]
724
+ except AttributeError:
725
+ # variables attribute doesn't exist yet (during initialization)
726
+ pass
727
+
728
+ # If not found, raise AttributeError
729
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
730
+
675
731
  def __getitem__(self, key: str):
676
732
  """Allow dict-like access to variables."""
677
733
  return self.get_variable(key)
@@ -738,7 +738,7 @@ class ExpressionMixin:
738
738
  continue
739
739
 
740
740
  # Strategy 2: Look for Equation objects in scope (created with .equals())
741
- frame = ScopeDiscoveryService._find_user_frame(getattr(ScopeDiscoveryService, '_get_current_frame', lambda: None)()) # type: ignore[misc]
741
+ frame = ScopeDiscoveryService._get_cached_user_frame()
742
742
  if frame:
743
743
  for obj in list(frame.f_locals.values()) + list(frame.f_globals.values()):
744
744
  if isinstance(obj, Equation):
@@ -775,8 +775,20 @@ class SetterCompatibilityMixin:
775
775
  if not hasattr(self, "_setter_class"):
776
776
  self._setter_class: type | None = None
777
777
 
778
- def set(self, value: float) -> TypeSafeSetter:
778
+ def set(self, value: float, unit: str | None = None) -> 'Self | TypeSafeSetter':
779
779
  """Create setter object for fluent API compatibility."""
780
+ if unit is not None:
781
+ # Handle direct unit setting using the specialized setter
782
+ if hasattr(self, "_setter_class") and self._setter_class:
783
+ setter = self._setter_class(self, value)
784
+ if hasattr(setter, unit):
785
+ getattr(setter, unit)
786
+ return self
787
+ else:
788
+ raise ValueError(f"Unknown unit: {unit}")
789
+ else:
790
+ raise ValueError("Unit parameter not supported for this variable type")
791
+
780
792
  if hasattr(self, "_setter_class") and self._setter_class:
781
793
  return self._setter_class(self, value) # Correct parameter order
782
794
  else: