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.
- {qnty-0.1.0 → qnty-0.1.2}/PKG-INFO +1 -1
- {qnty-0.1.0 → qnty-0.1.2}/pyproject.toml +1 -1
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/nodes.py +6 -5
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/composition.py +77 -7
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/problem.py +70 -14
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_qnty.py +14 -2
- qnty-0.1.2/src/qnty/quantities/field_setter.pyi +6620 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_vars.py +1395 -322
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_vars.pyi +1505 -2140
- {qnty-0.1.0 → qnty-0.1.2}/README.md +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/numerical.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/solvers.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/constants/tests.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/base.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/field_dims.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/field_dims.pyi +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/dimensions/signature.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/equations/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/equations/equation.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/equations/system.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/formatter.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/functions.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/expressions/types.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/integration/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/plotting/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/extensions/reporting/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/rules.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/solving.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/problems/validation.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/base_qnty.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_converters.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/quantities/field_setter.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/manager.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/order.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/base.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/iterative.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/solving/solvers/simultaneous.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/field_units.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/field_units.pyi +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/prefixes.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/units/registry.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/caching/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/caching/manager.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/__init__.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/context.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/exceptions.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/error_handling/handlers.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/logging.py +0 -0
- {qnty-0.1.0 → qnty-0.1.2}/src/qnty/utils/protocols.py +0 -0
- {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.
|
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
|
@@ -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,
|
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
|
-
|
159
|
-
|
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
|
-
|
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
|
-
#
|
237
|
-
self._proxy
|
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
|
-
#
|
357
|
-
|
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
|
-
#
|
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
|
-
#
|
543
|
-
self.
|
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.
|
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
|
645
|
-
for var in self.variables.
|
646
|
-
if
|
647
|
-
|
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.
|
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:
|