qnty 0.1.0__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.
qnty/expressions/nodes.py CHANGED
@@ -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):
qnty/problems/problem.py CHANGED
@@ -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."""
@@ -765,16 +765,34 @@ class Dimensionless(FieldQnty):
765
765
 
766
766
  Args:
767
767
  value: The numeric value to set
768
- unit: Optional unit string (for compatibility with base class)
768
+ unit: Optional unit string (for direct setting)
769
769
 
770
770
  Returns:
771
- DimensionlessSetter: A setter with unit properties like .meters, .inches, etc.
771
+ DimensionlessSetter: A setter with unit properties like .dimensionless
772
772
 
773
773
  Example:
774
- >>> length = Length("beam_length")
775
- >>> length.set(100).millimeters # Sets to 100 mm
776
- """
777
- return ts.DimensionlessSetter(self, value)
774
+ >>> factor = Dimensionless("factor")
775
+ >>> factor.set(2.5).dimensionless # Sets to 2.5
776
+ >>> factor.set(2.5, "dimensionless") # Direct setting
777
+ """
778
+ if unit is not None:
779
+ # Direct setting with unit
780
+ setter = ts.DimensionlessSetter(self, value)
781
+ # Get the unit property and call it to set the value
782
+ if hasattr(setter, unit):
783
+ getattr(setter, unit)
784
+ else:
785
+ # Try common aliases
786
+ unit_aliases = {
787
+ "dimensionless": "dimensionless", "": "dimensionless"
788
+ }
789
+ if unit in unit_aliases and hasattr(setter, unit_aliases[unit]):
790
+ getattr(setter, unit_aliases[unit])
791
+ else:
792
+ raise ValueError(f"Unknown unit: {unit}")
793
+ return self # Return the variable itself for consistency
794
+ else:
795
+ return ts.DimensionlessSetter(self, value)
778
796
 
779
797
 
780
798
  class DynamicFluidity(FieldQnty):
@@ -2358,7 +2376,7 @@ class Length(FieldQnty):
2358
2376
 
2359
2377
  Args:
2360
2378
  value: The numeric value to set
2361
- unit: Optional unit string (for compatibility with base class)
2379
+ unit: Optional unit string (for direct setting)
2362
2380
 
2363
2381
  Returns:
2364
2382
  LengthSetter: A setter with unit properties like .meters, .inches, etc.
@@ -2366,8 +2384,30 @@ class Length(FieldQnty):
2366
2384
  Example:
2367
2385
  >>> length = Length("beam_length")
2368
2386
  >>> length.set(100).millimeters # Sets to 100 mm
2369
- """
2370
- return ts.LengthSetter(self, value)
2387
+ >>> length.set(100, "inch") # Direct setting
2388
+ """
2389
+ if unit is not None:
2390
+ # Direct setting with unit
2391
+ setter = ts.LengthSetter(self, value)
2392
+ # Get the unit property and call it to set the value
2393
+ if hasattr(setter, unit):
2394
+ getattr(setter, unit)
2395
+ else:
2396
+ # Try common aliases
2397
+ unit_aliases = {
2398
+ "inch": "inch", "inches": "inch", "in": "inch",
2399
+ "foot": "foot", "feet": "foot", "ft": "foot",
2400
+ "meter": "meter", "meters": "meter", "m": "meter",
2401
+ "millimeter": "millimeter", "millimeters": "millimeter", "mm": "millimeter",
2402
+ "centimeter": "centimeter", "centimeters": "centimeter", "cm": "centimeter"
2403
+ }
2404
+ if unit in unit_aliases and hasattr(setter, unit_aliases[unit]):
2405
+ getattr(setter, unit_aliases[unit])
2406
+ else:
2407
+ raise ValueError(f"Unknown unit: {unit}")
2408
+ return self # Return the variable itself for consistency
2409
+ else:
2410
+ return ts.LengthSetter(self, value)
2371
2411
 
2372
2412
 
2373
2413
  class LinearMassDensity(FieldQnty):
@@ -4482,16 +4522,39 @@ class Pressure(FieldQnty):
4482
4522
 
4483
4523
  Args:
4484
4524
  value: The numeric value to set
4485
- unit: Optional unit string (for compatibility with base class)
4525
+ unit: Optional unit string (for direct setting)
4486
4526
 
4487
4527
  Returns:
4488
- PressureSetter: A setter with unit properties like .meters, .inches, etc.
4528
+ PressureSetter: A setter with unit properties like .psi, .bar, etc.
4489
4529
 
4490
4530
  Example:
4491
- >>> length = Length("beam_length")
4492
- >>> length.set(100).millimeters # Sets to 100 mm
4493
- """
4494
- return ts.PressureSetter(self, value)
4531
+ >>> pressure = Pressure("system_pressure")
4532
+ >>> pressure.set(100).psi # Sets to 100 psi
4533
+ >>> pressure.set(100, "psi") # Direct setting
4534
+ """
4535
+ if unit is not None:
4536
+ # Direct setting with unit
4537
+ setter = ts.PressureSetter(self, value)
4538
+ # Get the unit property and call it to set the value
4539
+ if hasattr(setter, unit):
4540
+ getattr(setter, unit)
4541
+ else:
4542
+ # Try common aliases
4543
+ unit_aliases = {
4544
+ "psi": "psi", "pound_per_square_inch": "psi",
4545
+ "bar": "bar", "bars": "bar",
4546
+ "pascal": "pascal", "pascals": "pascal", "pa": "pascal",
4547
+ "kpa": "kilopascal", "kilopascal": "kilopascal",
4548
+ "mpa": "megapascal", "megapascal": "megapascal",
4549
+ "atm": "atmosphere", "atmosphere": "atmosphere"
4550
+ }
4551
+ if unit in unit_aliases and hasattr(setter, unit_aliases[unit]):
4552
+ getattr(setter, unit_aliases[unit])
4553
+ else:
4554
+ raise ValueError(f"Unknown unit: {unit}")
4555
+ return self # Return the variable itself for consistency
4556
+ else:
4557
+ return ts.PressureSetter(self, value)
4495
4558
 
4496
4559
 
4497
4560
  class RadiationDoseEquivalent(FieldQnty):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qnty
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -14,15 +14,15 @@ qnty/equations/system.py,sha256=vMoD1iTUrAHnVFVvCUKeyNfSBfMiqpwQbfDx46kN9N8,5155
14
14
  qnty/expressions/__init__.py,sha256=DA2s7DBhVCmdUgsYSTJWObsp2DbbpFn492yr1nUTg2g,930
15
15
  qnty/expressions/formatter.py,sha256=yLGLwLYjhBvVi2Q6rfkg8pbyH0-a1Ko0AYLsqJTJf50,7806
16
16
  qnty/expressions/functions.py,sha256=ek43udfUDpThKo38rVPBYPvKfZNc9Bbs8RuL-CvQc_A,2729
17
- qnty/expressions/nodes.py,sha256=7JxHzhqZNrNUqShIBsIyuLmHQyeC14m4RxPCwxKvmrE,28431
17
+ qnty/expressions/nodes.py,sha256=oqMFfMeeWhccv4KBHNI5qYBz2pSe6jD-OwE4P2HyG1A,28644
18
18
  qnty/expressions/types.py,sha256=eoM-IqY-k-IypRHAlRwjEtMmB6DiwX7YGot8t_vGw3o,1729
19
19
  qnty/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  qnty/extensions/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  qnty/extensions/plotting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  qnty/extensions/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  qnty/problems/__init__.py,sha256=g7zuml2IecoSwgzX6r1zZ5SlmBKFc8qqTR_cao037pc,4808
24
- qnty/problems/composition.py,sha256=JUOu3IksLRJEJ2OvPyarxkuzvcrnYI9fNeov2Lj7ynk,41937
25
- qnty/problems/problem.py,sha256=Yh7wLo7RyZcR9fie6niJqcnP81rZhDKtNTgd5KQr1XE,28279
24
+ qnty/problems/composition.py,sha256=HI5ffsF14IXRuqjM0z_O0kkpme9fPnJVdz6m4rDCJsM,44916
25
+ qnty/problems/problem.py,sha256=zCUFsD44Clp0lezKLeG0hPAzR-Qee9JHeonjoERBmNM,30933
26
26
  qnty/problems/rules.py,sha256=NwIStAa8bocVtvzAsnPmRdC_0ENTJWyXLOoYBnkvpPA,5176
27
27
  qnty/problems/solving.py,sha256=LTI8F9ujDiSqXE9Aiz8sOgaGJNX9p7oaR5CQIZHpCY8,44315
28
28
  qnty/problems/validation.py,sha256=SmFEsgHx5XwRNlR2suOhxO-WNsOwPZhCP8wyVKYo1EE,4826
@@ -31,7 +31,7 @@ qnty/quantities/base_qnty.py,sha256=QasOR4-a7gwPBvc6cLJ3ooQHmOcWbYexgtNQ9I6bXI8,
31
31
  qnty/quantities/field_converters.py,sha256=rDWttIE0lwF1doGlLG5RJTcTikYEYruMrRBMlr8fvBI,1008701
32
32
  qnty/quantities/field_qnty.py,sha256=9A-KP8DyO5oOfmxw41HKJq48dUF0LP9M_YYqvdbVvRM,42562
33
33
  qnty/quantities/field_setter.py,sha256=JCvRom4qvCYvgRNgFLZEuwb1PCmz0qrKzxDv0-h4RMo,449348
34
- qnty/quantities/field_vars.py,sha256=mo-kh3WFx6h_dfROUIXAyhdDAoEDEgGN_TlaLwu_o1U,264561
34
+ qnty/quantities/field_vars.py,sha256=l2xWxuPrS8W1q1KtrBJQBST667-hxZXQERZXusombaA,267592
35
35
  qnty/quantities/field_vars.pyi,sha256=DJtLmxXJ9MrphAqSSOkMYNlLrq7-mAzvclp6bufT4RY,154868
36
36
  qnty/solving/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  qnty/solving/manager.py,sha256=LQBMhWD3ajRYMBXkwRpkVzdo7qVEDviBAoHpjAzS-0U,3945
@@ -55,6 +55,6 @@ qnty/utils/error_handling/handlers.py,sha256=_q12co-jr4YSktRoCPpGBbh6WXEDw9MbmWx
55
55
  qnty/utils/logging.py,sha256=2H6_gSOQjxdK5024XTY3E1jGIQPE8WdalVhVBFw51OA,1143
56
56
  qnty/utils/protocols.py,sha256=c_Ya_epCm7qenAADRMZiwiQ0PdD-Z4T85b1z1YQNXAk,5247
57
57
  qnty/utils/scope_discovery.py,sha256=mQc-FHJ5-VNBzqQwiFofV-hqeF3GpLRaLlTjYDRnOqs,15184
58
- qnty-0.1.0.dist-info/METADATA,sha256=IEWbkj_Ll1dkIkr50avhKUlChVE6rrAnSy5Bv9y4Ma0,6761
59
- qnty-0.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
60
- qnty-0.1.0.dist-info/RECORD,,
58
+ qnty-0.1.1.dist-info/METADATA,sha256=9Khz22UTCVgWts-ktT19CxzUPKWPgA0RGmCTYVjFZRE,6761
59
+ qnty-0.1.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
60
+ qnty-0.1.1.dist-info/RECORD,,
File without changes