qnty 0.1.3__py3-none-any.whl → 0.1.4__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.
@@ -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 the target variable's original unit if it had one
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
- result_qty = result_qty.to(var_obj.quantity.unit)
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
qnty/expressions/nodes.py CHANGED
@@ -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
- if right_value == 1.0:
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
- elif right_value == 0.0:
315
- return Quantity(0.0, right_val.unit)
316
- elif left_value == 0.0:
317
- return Quantity(0.0, left_val.unit)
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
- elif right_value == -1.0:
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
- elif right_value == 2.0:
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
- return Quantity(1.0 if result else 0.0, DimensionlessUnits.dimensionless)
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."""
qnty/problems/problem.py CHANGED
@@ -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
- raise ValueError(f'Unknown unit: {unit_name} for {self.variable.__class__.__name__}')
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 or use dimensionless fallback
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
- # Final fallback to dimensionless
198
- return Quantity(value, DimensionlessUnits.dimensionless)
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
- raise ValueError(f"Unknown unit: {unit_str}")
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."""