qnty 0.1.3__tar.gz → 0.1.5__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.3 → qnty-0.1.5}/PKG-INFO +1 -1
- {qnty-0.1.3 → qnty-0.1.5}/pyproject.toml +1 -1
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/equations/equation.py +29 -3
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/expressions/functions.py +18 -2
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/expressions/nodes.py +65 -19
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/problems/composition.py +28 -26
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/problems/problem.py +4 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/base_qnty.py +38 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/field_converters.py +5 -2
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/field_qnty.py +25 -5
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/field_vars.py +3527 -1606
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/field_vars.pyi +107 -107
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/units/field_units.py +1 -1
- qnty-0.1.5/src/qnty/utils/unit_suggestions.py +198 -0
- {qnty-0.1.3 → qnty-0.1.5}/README.md +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/constants/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/constants/numerical.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/constants/solvers.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/constants/tests.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/dimensions/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/dimensions/base.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/dimensions/field_dims.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/dimensions/field_dims.pyi +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/dimensions/signature.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/equations/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/equations/system.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/expressions/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/expressions/formatter.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/expressions/types.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/extensions/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/extensions/integration/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/extensions/plotting/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/extensions/reporting/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/problems/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/problems/rules.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/problems/solving.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/problems/validation.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/field_setter.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/quantities/field_setter.pyi +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/manager.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/order.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/solvers/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/solvers/base.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/solvers/iterative.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/solving/solvers/simultaneous.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/units/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/units/field_units.pyi +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/units/prefixes.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/units/registry.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/caching/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/caching/manager.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/error_handling/__init__.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/error_handling/context.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/error_handling/exceptions.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/error_handling/handlers.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/logging.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/src/qnty/utils/protocols.py +0 -0
- {qnty-0.1.3 → qnty-0.1.5}/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.5
|
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
|
@@ -98,12 +98,38 @@ class Equation:
|
|
98
98
|
if var_obj is None:
|
99
99
|
raise ValueError(f"Variable '{target_var}' not found in variable_values")
|
100
100
|
|
101
|
-
# Convert result to
|
101
|
+
# Convert result to preferred unit or original unit if available
|
102
|
+
target_unit_constant = None
|
103
|
+
|
104
|
+
# First priority: existing quantity unit
|
102
105
|
if var_obj.quantity is not None and var_obj.quantity.unit is not None:
|
106
|
+
target_unit_constant = var_obj.quantity.unit
|
107
|
+
# Second priority: preferred unit from constructor
|
108
|
+
elif hasattr(var_obj, 'preferred_unit') and var_obj.preferred_unit is not None:
|
109
|
+
# Look up unit constant from string name
|
110
|
+
preferred_unit_name = var_obj.preferred_unit
|
103
111
|
try:
|
104
|
-
|
112
|
+
# Get the dimension-specific units class
|
113
|
+
class_name = var_obj.__class__.__name__
|
114
|
+
units_class_name = f'{class_name}Units'
|
115
|
+
|
116
|
+
# Import field_units dynamically to avoid circular imports
|
117
|
+
from ..units import field_units
|
118
|
+
units_class = getattr(field_units, units_class_name, None)
|
119
|
+
if units_class and hasattr(units_class, preferred_unit_name):
|
120
|
+
target_unit_constant = getattr(units_class, preferred_unit_name)
|
121
|
+
_logger.debug(f"Found preferred unit constant: {preferred_unit_name}")
|
122
|
+
else:
|
123
|
+
_logger.debug(f"Could not find unit constant for {preferred_unit_name} in {units_class_name}")
|
124
|
+
except (ImportError, AttributeError) as e:
|
125
|
+
_logger.debug(f"Failed to lookup preferred unit {preferred_unit_name}: {e}")
|
126
|
+
|
127
|
+
if target_unit_constant is not None:
|
128
|
+
try:
|
129
|
+
result_qty = result_qty.to(target_unit_constant)
|
130
|
+
_logger.debug(f"Converted {target_var} result to preferred unit: {target_unit_constant.symbol}")
|
105
131
|
except (ValueError, TypeError, AttributeError) as e:
|
106
|
-
_logger.debug(f"Unit conversion failed for {target_var}: {e}. Using calculated unit.")
|
132
|
+
_logger.debug(f"Unit conversion failed for {target_var} to {target_unit_constant}: {e}. Using calculated unit.")
|
107
133
|
|
108
134
|
# Update the variable and return it
|
109
135
|
var_obj.quantity = result_qty
|
@@ -16,8 +16,24 @@ ConditionalOperand = Expression | BinaryOperation
|
|
16
16
|
def _create_unary_function(name: str, docstring: str):
|
17
17
|
"""Factory function for creating unary mathematical functions."""
|
18
18
|
|
19
|
-
def func(expr: ExpressionOperand)
|
20
|
-
|
19
|
+
def func(expr: ExpressionOperand):
|
20
|
+
from .nodes import VariableReference
|
21
|
+
|
22
|
+
wrapped_expr = wrap_operand(expr)
|
23
|
+
|
24
|
+
# For known quantities (FieldQnty with known values), evaluate immediately
|
25
|
+
if hasattr(expr, 'quantity') and expr.quantity is not None:
|
26
|
+
try:
|
27
|
+
unary_func = UnaryFunction(name, wrapped_expr)
|
28
|
+
# Use an empty variable dict since we have the quantity directly
|
29
|
+
result = unary_func.evaluate({})
|
30
|
+
return result
|
31
|
+
except (ValueError, TypeError, AttributeError):
|
32
|
+
# Fall back to expression if evaluation fails
|
33
|
+
pass
|
34
|
+
|
35
|
+
# For unknown variables or expressions, return the UnaryFunction
|
36
|
+
return UnaryFunction(name, wrapped_expr)
|
21
37
|
|
22
38
|
func.__name__ = name
|
23
39
|
func.__doc__ = docstring
|
@@ -304,32 +304,37 @@ class BinaryOperation(Expression):
|
|
304
304
|
right_value = right_val.value
|
305
305
|
|
306
306
|
# ENHANCED FAST PATHS: Check most common optimizations first
|
307
|
+
# BUT ONLY when they don't affect dimensional analysis!
|
308
|
+
|
307
309
|
# Identity optimizations (1.0 multiplication) - most frequent case
|
308
|
-
|
310
|
+
# Only safe when the 1.0 value is dimensionless
|
311
|
+
if right_value == 1.0 and right_val._dimension_sig == 1:
|
309
312
|
return left_val
|
310
|
-
elif left_value == 1.0:
|
313
|
+
elif left_value == 1.0 and left_val._dimension_sig == 1:
|
311
314
|
return right_val
|
312
315
|
|
313
316
|
# Zero optimizations - second most common
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
return
|
317
|
+
# Zero multiplication always gives zero, but we need proper units via full multiplication
|
318
|
+
elif right_value == 0.0 or left_value == 0.0:
|
319
|
+
# Use full multiplication to get correct dimensional result for zero
|
320
|
+
return left_val * right_val
|
318
321
|
|
319
322
|
# Additional fast paths for common values
|
320
|
-
|
323
|
+
# Only safe when the scalar value is dimensionless
|
324
|
+
elif right_value == -1.0 and right_val._dimension_sig == 1:
|
321
325
|
return Quantity(-left_value, left_val.unit)
|
322
|
-
elif left_value == -1.0:
|
326
|
+
elif left_value == -1.0 and left_val._dimension_sig == 1:
|
323
327
|
return Quantity(-right_value, right_val.unit)
|
324
328
|
|
325
329
|
# ADDITIONAL COMMON CASES: Powers of 2 and 0.5 (very common in engineering)
|
326
|
-
|
330
|
+
# Only safe when the scalar value is dimensionless
|
331
|
+
elif right_value == 2.0 and right_val._dimension_sig == 1:
|
327
332
|
return Quantity(left_value * 2.0, left_val.unit)
|
328
|
-
elif left_value == 2.0:
|
333
|
+
elif left_value == 2.0 and left_val._dimension_sig == 1:
|
329
334
|
return Quantity(right_value * 2.0, right_val.unit)
|
330
|
-
elif right_value == 0.5:
|
335
|
+
elif right_value == 0.5 and right_val._dimension_sig == 1:
|
331
336
|
return Quantity(left_value * 0.5, left_val.unit)
|
332
|
-
elif left_value == 0.5:
|
337
|
+
elif left_value == 0.5 and left_val._dimension_sig == 1:
|
333
338
|
return Quantity(right_value * 0.5, right_val.unit)
|
334
339
|
|
335
340
|
# OPTIMIZED REGULAR CASE: Use the enhanced multiplication from base_qnty
|
@@ -432,7 +437,10 @@ class BinaryOperation(Expression):
|
|
432
437
|
|
433
438
|
# Perform comparison using optimized dispatch
|
434
439
|
result = self._perform_comparison(left_val.value, right_val.value)
|
435
|
-
|
440
|
+
|
441
|
+
# Import BooleanQuantity locally to avoid circular imports
|
442
|
+
from ..quantities.base_qnty import BooleanQuantity
|
443
|
+
return BooleanQuantity(result)
|
436
444
|
|
437
445
|
def _normalize_comparison_units(self, left_val: "Quantity", right_val: "Quantity") -> tuple["Quantity", "Quantity"]:
|
438
446
|
"""Normalize units for comparison operations."""
|
@@ -506,14 +514,14 @@ class UnaryFunction(Expression):
|
|
506
514
|
|
507
515
|
# Function dispatch table for better performance and maintainability
|
508
516
|
functions = {
|
509
|
-
"sin": lambda x:
|
510
|
-
"cos": lambda x:
|
511
|
-
"tan": lambda x:
|
517
|
+
"sin": lambda x: math.sin(self._to_radians_if_angle(x)),
|
518
|
+
"cos": lambda x: math.cos(self._to_radians_if_angle(x)),
|
519
|
+
"tan": lambda x: math.tan(self._to_radians_if_angle(x)),
|
512
520
|
"sqrt": lambda x: Quantity(math.sqrt(x.value), x.unit),
|
513
521
|
"abs": lambda x: Quantity(abs(x.value), x.unit),
|
514
|
-
"ln": lambda x:
|
515
|
-
"log10": lambda x:
|
516
|
-
"exp": lambda x:
|
522
|
+
"ln": lambda x: math.log(x.value),
|
523
|
+
"log10": lambda x: math.log10(x.value),
|
524
|
+
"exp": lambda x: math.exp(x.value),
|
517
525
|
}
|
518
526
|
|
519
527
|
func = functions.get(self.function_name)
|
@@ -522,6 +530,35 @@ class UnaryFunction(Expression):
|
|
522
530
|
else:
|
523
531
|
raise ValueError(f"Unknown function: {self.function_name}")
|
524
532
|
|
533
|
+
def _to_radians_if_angle(self, quantity: "Quantity") -> float:
|
534
|
+
"""Convert angle quantities to radians for trigonometric functions."""
|
535
|
+
from ..dimensions import field_dims
|
536
|
+
|
537
|
+
# Check if this is an angle dimension by comparing dimension signature
|
538
|
+
# Need to handle the case where angle dimensions might not exactly match due to implementation details
|
539
|
+
try:
|
540
|
+
# Import angle plane dimension for comparison
|
541
|
+
angle_plane_dim = field_dims.ANGLE_PLANE
|
542
|
+
|
543
|
+
# If this looks like an angle (has angle dimension or unit name suggests it)
|
544
|
+
if (hasattr(quantity, '_dimension_sig') and quantity._dimension_sig == angle_plane_dim) or \
|
545
|
+
(hasattr(quantity, 'unit') and hasattr(quantity.unit, 'name') and
|
546
|
+
any(angle_word in str(quantity.unit.name).lower() for angle_word in ['degree', 'radian', 'grad', 'gon'])):
|
547
|
+
|
548
|
+
# Import the radian unit for conversion
|
549
|
+
from ..units.field_units import AnglePlaneUnits
|
550
|
+
|
551
|
+
# Convert to radians and return the numeric value
|
552
|
+
radian_quantity = quantity.to(AnglePlaneUnits.radian.definition)
|
553
|
+
return radian_quantity.value
|
554
|
+
else:
|
555
|
+
# Non-angle quantities: return raw value (backward compatibility)
|
556
|
+
return quantity.value
|
557
|
+
except (ImportError, AttributeError, ValueError):
|
558
|
+
# If anything goes wrong with angle detection/conversion, fall back to raw value
|
559
|
+
return quantity.value
|
560
|
+
|
561
|
+
|
525
562
|
def get_variables(self) -> set[str]:
|
526
563
|
return self.operand.get_variables()
|
527
564
|
|
@@ -538,6 +575,15 @@ class UnaryFunction(Expression):
|
|
538
575
|
return UnaryFunction(self.function_name, simplified_operand)
|
539
576
|
|
540
577
|
def __str__(self) -> str:
|
578
|
+
# Try auto-evaluation first if possible
|
579
|
+
can_eval, variables = self._can_auto_evaluate()
|
580
|
+
if can_eval and variables:
|
581
|
+
try:
|
582
|
+
result = self.evaluate(variables)
|
583
|
+
return str(result)
|
584
|
+
except (ValueError, TypeError, AttributeError):
|
585
|
+
# Fall back to symbolic representation
|
586
|
+
pass
|
541
587
|
return ExpressionFormatter.format_unary_function(self) # type: ignore[arg-type]
|
542
588
|
|
543
589
|
|
@@ -186,6 +186,8 @@ class ConfigurableVariable:
|
|
186
186
|
return self._variable._arithmetic_mode
|
187
187
|
# Otherwise default to 'expression'
|
188
188
|
return 'expression'
|
189
|
+
|
190
|
+
|
189
191
|
return getattr(self._variable, name)
|
190
192
|
|
191
193
|
# Delegate arithmetic operations to the wrapped variable
|
@@ -233,10 +235,10 @@ class ConfigurableVariable:
|
|
233
235
|
def __ge__(self, other):
|
234
236
|
return self._variable.__ge__(other)
|
235
237
|
|
236
|
-
def __eq__(self, other):
|
238
|
+
def __eq__(self, other): # type: ignore[override]
|
237
239
|
return self._variable.__eq__(other)
|
238
240
|
|
239
|
-
def __ne__(self, other):
|
241
|
+
def __ne__(self, other): # type: ignore[override]
|
240
242
|
return self._variable.__ne__(other)
|
241
243
|
|
242
244
|
def __setattr__(self, name, value):
|
@@ -257,6 +259,30 @@ class ConfigurableVariable:
|
|
257
259
|
else:
|
258
260
|
return original_setter
|
259
261
|
|
262
|
+
def update(self, value=None, unit=None, quantity=None, is_known=None):
|
263
|
+
"""Override update method to track configuration changes."""
|
264
|
+
result = self._variable.update(value, unit, quantity, is_known)
|
265
|
+
if self._proxy and self._original_symbol:
|
266
|
+
# Track this configuration change
|
267
|
+
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
268
|
+
return result
|
269
|
+
|
270
|
+
def mark_known(self):
|
271
|
+
"""Override mark_known to track configuration changes."""
|
272
|
+
result = self._variable.mark_known()
|
273
|
+
if self._proxy and self._original_symbol:
|
274
|
+
# Track this configuration change
|
275
|
+
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
276
|
+
return result
|
277
|
+
|
278
|
+
def mark_unknown(self):
|
279
|
+
"""Override mark_unknown to track configuration changes."""
|
280
|
+
result = self._variable.mark_unknown()
|
281
|
+
if self._proxy and self._original_symbol:
|
282
|
+
# Track this configuration change
|
283
|
+
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
284
|
+
return result
|
285
|
+
|
260
286
|
|
261
287
|
class TrackingSetterWrapper:
|
262
288
|
"""
|
@@ -307,30 +333,6 @@ class TrackingSetterWrapper:
|
|
307
333
|
)
|
308
334
|
return result
|
309
335
|
|
310
|
-
def update(self, value=None, unit=None, quantity=None, is_known=None):
|
311
|
-
"""Override update method to track configuration changes."""
|
312
|
-
result = self._variable.update(value, unit, quantity, is_known)
|
313
|
-
if self._proxy and self._original_symbol:
|
314
|
-
# Track this configuration change
|
315
|
-
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
316
|
-
return result
|
317
|
-
|
318
|
-
def mark_known(self):
|
319
|
-
"""Override mark_known to track configuration changes."""
|
320
|
-
result = self._variable.mark_known()
|
321
|
-
if self._proxy and self._original_symbol:
|
322
|
-
# Track this configuration change
|
323
|
-
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
324
|
-
return result
|
325
|
-
|
326
|
-
def mark_unknown(self):
|
327
|
-
"""Override mark_unknown to track configuration changes."""
|
328
|
-
result = self._variable.mark_unknown()
|
329
|
-
if self._proxy and self._original_symbol:
|
330
|
-
# Track this configuration change
|
331
|
-
self._proxy.track_configuration(self._original_symbol, self._variable.quantity, self._variable.is_known)
|
332
|
-
return result
|
333
|
-
|
334
336
|
|
335
337
|
class DelayedVariableReference(ArithmeticOperationsMixin):
|
336
338
|
"""
|
@@ -372,6 +372,10 @@ class Problem(ValidationMixin):
|
|
372
372
|
cloned.quantity = variable.quantity # Keep reference to same quantity - units must not be copied
|
373
373
|
cloned.is_known = variable.is_known
|
374
374
|
|
375
|
+
# Copy unit preference if it exists
|
376
|
+
if hasattr(variable, '_preferred_unit'):
|
377
|
+
cloned._preferred_unit = variable._preferred_unit
|
378
|
+
|
375
379
|
# Ensure the cloned variable has fresh validation checks
|
376
380
|
if hasattr(variable, "validation_checks") and hasattr(cloned, "validation_checks"):
|
377
381
|
cloned.validation_checks = []
|
@@ -673,5 +673,43 @@ class Quantity:
|
|
673
673
|
return UnitConversions.to(self, target_unit)
|
674
674
|
|
675
675
|
|
676
|
+
class BooleanQuantity(Quantity):
|
677
|
+
"""A quantity that represents a boolean value but maintains Quantity compatibility.
|
678
|
+
|
679
|
+
This class is used for comparison operations that need to return boolean results
|
680
|
+
while maintaining the Expression interface requirement of returning Quantity objects.
|
681
|
+
"""
|
682
|
+
|
683
|
+
__slots__ = ("_boolean_value",)
|
684
|
+
|
685
|
+
def __init__(self, boolean_value: bool):
|
686
|
+
"""Initialize with a boolean value."""
|
687
|
+
# Store the actual boolean value
|
688
|
+
self._boolean_value = boolean_value
|
689
|
+
|
690
|
+
# Initialize parent with numeric representation
|
691
|
+
super().__init__(
|
692
|
+
1.0 if boolean_value else 0.0,
|
693
|
+
DimensionlessUnits.dimensionless
|
694
|
+
)
|
695
|
+
|
696
|
+
def __str__(self) -> str:
|
697
|
+
"""Display as True/False instead of 1.0/0.0."""
|
698
|
+
return str(self._boolean_value)
|
699
|
+
|
700
|
+
def __repr__(self) -> str:
|
701
|
+
"""Display as BooleanQuantity(True/False)."""
|
702
|
+
return f"BooleanQuantity({self._boolean_value})"
|
703
|
+
|
704
|
+
def __bool__(self) -> bool:
|
705
|
+
"""Return the actual boolean value."""
|
706
|
+
return self._boolean_value
|
707
|
+
|
708
|
+
@property
|
709
|
+
def boolean_value(self) -> bool:
|
710
|
+
"""Access the boolean value directly."""
|
711
|
+
return self._boolean_value
|
712
|
+
|
713
|
+
|
676
714
|
# Initialize cache manager at module load
|
677
715
|
_cache_manager.initialize_common_operations()
|
@@ -13,8 +13,8 @@ from typing import TYPE_CHECKING, Final
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from ..quantities.field_qnty import FieldQnty
|
15
15
|
|
16
|
-
from ..units import field_units
|
17
16
|
from .base_qnty import Quantity
|
17
|
+
from ..units import field_units
|
18
18
|
|
19
19
|
# ===== BASE CONVERTER CLASSES =====
|
20
20
|
|
@@ -33,7 +33,10 @@ class UnitConverter:
|
|
33
33
|
units_class = getattr(field_units, units_class_name, None)
|
34
34
|
if units_class and hasattr(units_class, unit_name):
|
35
35
|
return getattr(units_class, unit_name)
|
36
|
-
|
36
|
+
# Raise error with suggestions
|
37
|
+
from ..utils.unit_suggestions import create_unit_validation_error
|
38
|
+
var_type = getattr(self.variable, '__class__', type(self.variable)).__name__
|
39
|
+
raise create_unit_validation_error(unit_name, var_type)
|
37
40
|
|
38
41
|
def _convert_quantity(self, unit_constant, modify_original: bool = False):
|
39
42
|
"""Convert quantity to specified unit."""
|
@@ -43,6 +43,7 @@ class QuantityManagementMixin:
|
|
43
43
|
self._is_known: bool = False
|
44
44
|
self._name: str = ""
|
45
45
|
self._symbol: str | None = None
|
46
|
+
self._preferred_unit: str | None = None
|
46
47
|
|
47
48
|
@property
|
48
49
|
def quantity(self) -> Quantity | None:
|
@@ -90,6 +91,15 @@ class QuantityManagementMixin:
|
|
90
91
|
|
91
92
|
return self
|
92
93
|
|
94
|
+
def _set_preferred_unit(self, unit: str) -> None:
|
95
|
+
"""Set the preferred unit for solver results."""
|
96
|
+
self._preferred_unit = unit
|
97
|
+
|
98
|
+
@property
|
99
|
+
def preferred_unit(self) -> str | None:
|
100
|
+
"""Get the preferred unit for this variable."""
|
101
|
+
return self._preferred_unit
|
102
|
+
|
93
103
|
|
94
104
|
class FlexibleConstructorMixin:
|
95
105
|
"""Handles flexible variable initialization patterns maintaining backward compatibility."""
|
@@ -180,10 +190,9 @@ class FlexibleConstructorMixin:
|
|
180
190
|
return result
|
181
191
|
|
182
192
|
# Fallback to direct quantity creation
|
183
|
-
from ..units import DimensionlessUnits
|
184
193
|
from .base_qnty import Quantity
|
185
194
|
|
186
|
-
# Try to find the unit in the registry
|
195
|
+
# Try to find the unit in the registry
|
187
196
|
try:
|
188
197
|
from ..units.registry import registry
|
189
198
|
|
@@ -194,8 +203,10 @@ class FlexibleConstructorMixin:
|
|
194
203
|
except Exception:
|
195
204
|
pass
|
196
205
|
|
197
|
-
#
|
198
|
-
|
206
|
+
# If unit is not found, raise error with suggestions instead of falling back to dimensionless
|
207
|
+
from ..utils.unit_suggestions import create_unit_validation_error
|
208
|
+
var_type = getattr(self, '__class__', type(self)).__name__
|
209
|
+
raise create_unit_validation_error(str(unit), var_type)
|
199
210
|
|
200
211
|
def _find_unit_property(self, setter: TypeSafeSetter, unit: str) -> str | None:
|
201
212
|
"""Find unit property with simple lookup."""
|
@@ -609,6 +620,12 @@ class ExpressionMixin:
|
|
609
620
|
|
610
621
|
def __gt__(self, other) -> Expression:
|
611
622
|
return self.gt(other)
|
623
|
+
|
624
|
+
def __eq__(self, other) -> Expression: # type: ignore[override]
|
625
|
+
return self.eq(other)
|
626
|
+
|
627
|
+
def __ne__(self, other) -> Expression: # type: ignore[override]
|
628
|
+
return self.ne(other)
|
612
629
|
|
613
630
|
def eq(self, other) -> Expression:
|
614
631
|
"""Create equality comparison expression."""
|
@@ -827,7 +844,10 @@ class UnitConverter:
|
|
827
844
|
except Exception:
|
828
845
|
pass
|
829
846
|
|
830
|
-
|
847
|
+
# Raise error with suggestions
|
848
|
+
from ..utils.unit_suggestions import create_unit_validation_error
|
849
|
+
var_type = getattr(self.variable, '__class__', type(self.variable)).__name__
|
850
|
+
raise create_unit_validation_error(unit_str, var_type)
|
831
851
|
|
832
852
|
def _convert_quantity(self, to_unit_constant, modify_original: bool = True):
|
833
853
|
"""Convert the variable's quantity to the specified unit."""
|