qnty 0.0.7__py3-none-any.whl → 0.0.9__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.
Files changed (76) hide show
  1. qnty/__init__.py +140 -58
  2. qnty/_backup/problem_original.py +1251 -0
  3. qnty/_backup/quantity.py +63 -0
  4. qnty/codegen/cli.py +125 -0
  5. qnty/codegen/generators/data/unit_data.json +8807 -0
  6. qnty/codegen/generators/data_processor.py +345 -0
  7. qnty/codegen/generators/dimensions_gen.py +434 -0
  8. qnty/codegen/generators/doc_generator.py +141 -0
  9. qnty/codegen/generators/out/dimension_mapping.json +974 -0
  10. qnty/codegen/generators/out/dimension_metadata.json +123 -0
  11. qnty/codegen/generators/out/units_metadata.json +223 -0
  12. qnty/codegen/generators/quantities_gen.py +159 -0
  13. qnty/codegen/generators/setters_gen.py +178 -0
  14. qnty/codegen/generators/stubs_gen.py +167 -0
  15. qnty/codegen/generators/units_gen.py +295 -0
  16. qnty/codegen/generators/utils/__init__.py +0 -0
  17. qnty/equations/__init__.py +4 -0
  18. qnty/equations/equation.py +257 -0
  19. qnty/equations/system.py +127 -0
  20. qnty/expressions/__init__.py +61 -0
  21. qnty/expressions/cache.py +94 -0
  22. qnty/expressions/functions.py +96 -0
  23. qnty/expressions/nodes.py +546 -0
  24. qnty/generated/__init__.py +0 -0
  25. qnty/generated/dimensions.py +514 -0
  26. qnty/generated/quantities.py +6003 -0
  27. qnty/generated/quantities.pyi +4192 -0
  28. qnty/generated/setters.py +12210 -0
  29. qnty/generated/units.py +9798 -0
  30. qnty/problem/__init__.py +91 -0
  31. qnty/problem/base.py +142 -0
  32. qnty/problem/composition.py +385 -0
  33. qnty/problem/composition_mixin.py +382 -0
  34. qnty/problem/equations.py +413 -0
  35. qnty/problem/metaclass.py +302 -0
  36. qnty/problem/reconstruction.py +1016 -0
  37. qnty/problem/solving.py +180 -0
  38. qnty/problem/validation.py +64 -0
  39. qnty/problem/variables.py +239 -0
  40. qnty/quantities/__init__.py +6 -0
  41. qnty/quantities/expression_quantity.py +314 -0
  42. qnty/quantities/quantity.py +428 -0
  43. qnty/quantities/typed_quantity.py +215 -0
  44. qnty/solving/__init__.py +0 -0
  45. qnty/solving/manager.py +90 -0
  46. qnty/solving/order.py +355 -0
  47. qnty/solving/solvers/__init__.py +20 -0
  48. qnty/solving/solvers/base.py +92 -0
  49. qnty/solving/solvers/iterative.py +185 -0
  50. qnty/solving/solvers/simultaneous.py +547 -0
  51. qnty/units/__init__.py +0 -0
  52. qnty/{prefixes.py → units/prefixes.py} +54 -33
  53. qnty/{unit.py → units/registry.py} +73 -32
  54. qnty/utils/__init__.py +0 -0
  55. qnty/utils/logging.py +40 -0
  56. qnty/validation/__init__.py +0 -0
  57. qnty/validation/registry.py +0 -0
  58. qnty/validation/rules.py +167 -0
  59. qnty-0.0.9.dist-info/METADATA +199 -0
  60. qnty-0.0.9.dist-info/RECORD +63 -0
  61. qnty/dimension.py +0 -186
  62. qnty/equation.py +0 -216
  63. qnty/expression.py +0 -492
  64. qnty/unit_types/base.py +0 -47
  65. qnty/units.py +0 -8113
  66. qnty/variable.py +0 -263
  67. qnty/variable_types/base.py +0 -58
  68. qnty/variable_types/expression_variable.py +0 -68
  69. qnty/variable_types/typed_variable.py +0 -87
  70. qnty/variables.py +0 -2298
  71. qnty/variables.pyi +0 -6148
  72. qnty-0.0.7.dist-info/METADATA +0 -355
  73. qnty-0.0.7.dist-info/RECORD +0 -19
  74. /qnty/{unit_types → codegen}/__init__.py +0 -0
  75. /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
  76. {qnty-0.0.7.dist-info → qnty-0.0.9.dist-info}/WHEEL +0 -0
@@ -10,7 +10,7 @@ from dataclasses import dataclass
10
10
  from enum import Enum
11
11
 
12
12
 
13
- @dataclass(frozen=True)
13
+ @dataclass(frozen=True, slots=True)
14
14
  class SIPrefix:
15
15
  """
16
16
  Standard SI prefix definition.
@@ -25,16 +25,12 @@ class SIPrefix:
25
25
  factor: float
26
26
 
27
27
  def apply_to_name(self, base_name: str) -> str:
28
- """Apply prefix to a base unit name."""
29
- if not self.name:
30
- return base_name
31
- return f"{self.name}{base_name}"
28
+ """Apply prefix to a base unit name. Optimized for performance."""
29
+ return base_name if not self.name else self.name + base_name
32
30
 
33
31
  def apply_to_symbol(self, base_symbol: str) -> str:
34
- """Apply prefix to a base unit symbol."""
35
- if not self.symbol:
36
- return base_symbol
37
- return f"{self.symbol}{base_symbol}"
32
+ """Apply prefix to a base unit symbol. Optimized for performance."""
33
+ return base_symbol if not self.symbol else self.symbol + base_symbol
38
34
 
39
35
 
40
36
  class StandardPrefixes(Enum):
@@ -71,8 +67,8 @@ class StandardPrefixes(Enum):
71
67
  YOCTO = SIPrefix("yocto", "y", 1e-24)
72
68
 
73
69
 
74
- # Common prefix groups for different unit types
75
- COMMON_LENGTH_PREFIXES = [
70
+ # Common prefix groups for different unit types - type annotated for better IDE support
71
+ COMMON_LENGTH_PREFIXES: list[StandardPrefixes] = [
76
72
  StandardPrefixes.KILO,
77
73
  StandardPrefixes.CENTI,
78
74
  StandardPrefixes.MILLI,
@@ -80,20 +76,20 @@ COMMON_LENGTH_PREFIXES = [
80
76
  StandardPrefixes.NANO,
81
77
  ]
82
78
 
83
- COMMON_MASS_PREFIXES = [
79
+ COMMON_MASS_PREFIXES: list[StandardPrefixes] = [
84
80
  StandardPrefixes.KILO, # Note: kilogram is the SI base unit
85
81
  StandardPrefixes.MILLI,
86
82
  StandardPrefixes.MICRO,
87
83
  ]
88
84
 
89
- COMMON_TIME_PREFIXES = [
85
+ COMMON_TIME_PREFIXES: list[StandardPrefixes] = [
90
86
  StandardPrefixes.MILLI,
91
87
  StandardPrefixes.MICRO,
92
88
  StandardPrefixes.NANO,
93
89
  StandardPrefixes.PICO,
94
90
  ]
95
91
 
96
- COMMON_ELECTRIC_PREFIXES = [
92
+ COMMON_ELECTRIC_PREFIXES: list[StandardPrefixes] = [
97
93
  StandardPrefixes.KILO,
98
94
  StandardPrefixes.MILLI,
99
95
  StandardPrefixes.MICRO,
@@ -101,13 +97,13 @@ COMMON_ELECTRIC_PREFIXES = [
101
97
  StandardPrefixes.PICO,
102
98
  ]
103
99
 
104
- COMMON_ENERGY_PREFIXES = [
100
+ COMMON_ENERGY_PREFIXES: list[StandardPrefixes] = [
105
101
  StandardPrefixes.KILO,
106
102
  StandardPrefixes.MEGA,
107
103
  StandardPrefixes.GIGA,
108
104
  ]
109
105
 
110
- COMMON_POWER_PREFIXES = [
106
+ COMMON_POWER_PREFIXES: list[StandardPrefixes] = [
111
107
  StandardPrefixes.KILO,
112
108
  StandardPrefixes.MEGA,
113
109
  StandardPrefixes.GIGA,
@@ -115,39 +111,61 @@ COMMON_POWER_PREFIXES = [
115
111
  StandardPrefixes.MICRO,
116
112
  ]
117
113
 
118
- COMMON_PRESSURE_PREFIXES = [
114
+ COMMON_PRESSURE_PREFIXES: list[StandardPrefixes] = [
119
115
  StandardPrefixes.KILO,
120
116
  StandardPrefixes.MEGA,
121
117
  StandardPrefixes.GIGA,
122
118
  ]
123
119
 
124
120
 
125
- def get_prefix_by_name(name: str) -> SIPrefix | None:
126
- """Get a prefix by its name (e.g., 'kilo', 'milli')."""
121
+ # Performance optimization: Pre-computed lookup dictionaries
122
+ _NAME_TO_PREFIX: dict[str, SIPrefix] = {}
123
+ _SYMBOL_TO_PREFIX: dict[str, SIPrefix] = {}
124
+ _FACTOR_TO_PREFIX: dict[float, SIPrefix] = {}
125
+
126
+ def _initialize_lookup_caches():
127
+ """Initialize lookup caches for O(1) prefix lookups."""
127
128
  for prefix_enum in StandardPrefixes:
128
- if prefix_enum.value.name == name:
129
- return prefix_enum.value
130
- return None
129
+ prefix = prefix_enum.value
130
+ _NAME_TO_PREFIX[prefix.name] = prefix
131
+ _SYMBOL_TO_PREFIX[prefix.symbol] = prefix
132
+ _FACTOR_TO_PREFIX[prefix.factor] = prefix
133
+
134
+ def get_prefix_by_name(name: str) -> SIPrefix | None:
135
+ """Get a prefix by its name (e.g., 'kilo', 'milli'). O(1) lookup."""
136
+ return _NAME_TO_PREFIX.get(name)
131
137
 
132
138
 
133
139
  def get_prefix_by_symbol(symbol: str) -> SIPrefix | None:
134
- """Get a prefix by its symbol (e.g., 'k', 'm')."""
135
- for prefix_enum in StandardPrefixes:
136
- if prefix_enum.value.symbol == symbol:
137
- return prefix_enum.value
138
- return None
140
+ """Get a prefix by its symbol (e.g., 'k', 'm'). O(1) lookup."""
141
+ return _SYMBOL_TO_PREFIX.get(symbol)
139
142
 
140
143
 
141
144
  def get_prefix_by_factor(factor: float, tolerance: float = 1e-10) -> SIPrefix | None:
142
- """Get a prefix by its multiplication factor."""
143
- for prefix_enum in StandardPrefixes:
144
- if abs(prefix_enum.value.factor - factor) < tolerance:
145
- return prefix_enum.value
145
+ """Get a prefix by its multiplication factor. O(1) lookup for exact matches, optimized tolerance search."""
146
+ # Fast path for exact matches
147
+ if factor in _FACTOR_TO_PREFIX:
148
+ return _FACTOR_TO_PREFIX[factor]
149
+
150
+ # Optimized tolerance path - only search if tolerance is meaningful
151
+ if tolerance > 1e-15: # Avoid expensive search for tiny tolerances
152
+ # Use items() view for better performance than .items()
153
+ for cached_factor, prefix in _FACTOR_TO_PREFIX.items():
154
+ if abs(cached_factor - factor) < tolerance:
155
+ return prefix
146
156
  return None
147
157
 
148
158
 
149
- # Define which units should get automatic prefixes
150
- PREFIXABLE_UNITS = {
159
+ def extract_prefix_values(prefix_enums: list[StandardPrefixes]) -> list[SIPrefix]:
160
+ """Extract SIPrefix values from StandardPrefixes enums efficiently.
161
+
162
+ This is optimized for bulk operations that need to convert enum lists to value lists.
163
+ """
164
+ return [prefix_enum.value for prefix_enum in prefix_enums]
165
+
166
+
167
+ # Define which units should get automatic prefixes - using more descriptive type annotation
168
+ PREFIXABLE_UNITS: dict[str, list[StandardPrefixes]] = {
151
169
  # Base SI units
152
170
  'meter': COMMON_LENGTH_PREFIXES,
153
171
  'gram': COMMON_MASS_PREFIXES,
@@ -227,3 +245,6 @@ PREFIXABLE_UNITS = {
227
245
  StandardPrefixes.MICRO
228
246
  ], # Common non-SI unit
229
247
  }
248
+
249
+ # Initialize lookup caches on module load for optimal performance
250
+ _initialize_lookup_caches()
@@ -7,11 +7,11 @@ Unit definitions, constants and registry for the high-performance unit system.
7
7
 
8
8
  from dataclasses import dataclass
9
9
 
10
- from .dimension import DimensionSignature
10
+ from ..generated.dimensions import DimensionSignature
11
11
  from .prefixes import SIPrefix, StandardPrefixes
12
12
 
13
13
 
14
- @dataclass(frozen=True)
14
+ @dataclass(frozen=True, slots=True)
15
15
  class UnitDefinition:
16
16
  """Immutable unit definition optimized for performance."""
17
17
  name: str
@@ -39,58 +39,70 @@ class UnitDefinition:
39
39
  class UnitConstant:
40
40
  """Unit constant that provides type safety and performance."""
41
41
 
42
+ __slots__ = ('definition', 'name', 'symbol', 'dimension', 'si_factor', '_hash_cache')
43
+
42
44
  def __init__(self, definition: UnitDefinition):
43
45
  self.definition = definition
44
46
  self.name = definition.name
45
47
  self.symbol = definition.symbol
46
48
  self.dimension = definition.dimension
47
49
  self.si_factor = definition.si_factor
50
+ # Cache expensive hash operation
51
+ self._hash_cache = hash(self.name)
48
52
 
49
53
  def __str__(self):
50
54
  return self.symbol
51
55
 
52
- def __eq__(self, other):
53
- """Fast equality check for unit constants."""
54
- return isinstance(other, UnitConstant) and self.name == other.name
56
+ def __eq__(self, other) -> bool:
57
+ """Ultra-fast equality check for unit constants."""
58
+ # Fast path: check type first without isinstance() overhead
59
+ return type(other) is UnitConstant and self.name == other.name
55
60
 
56
- def __hash__(self):
57
- """Enable unit constants as dictionary keys."""
58
- return hash(self.name)
61
+ def __hash__(self) -> int:
62
+ """Enable unit constants as dictionary keys with cached hash."""
63
+ return self._hash_cache
59
64
 
60
65
 
61
- class HighPerformanceRegistry:
66
+ class Registry:
62
67
  """Ultra-fast registry with pre-computed conversion tables."""
63
68
 
69
+ __slots__ = ('units', 'conversion_table', 'dimensional_groups', '_finalized',
70
+ 'base_units', 'prefixable_units', '_conversion_cache', '_dimension_cache')
71
+
64
72
  def __init__(self):
65
73
  self.units: dict[str, UnitDefinition] = {}
66
74
  self.conversion_table: dict[tuple[str, str], float] = {} # (from_unit, to_unit) -> factor
67
75
  self.dimensional_groups: dict[int | float, list[UnitDefinition]] = {}
68
- self._dimension_cache: dict[int | float, UnitConstant] = {} # Cache for common dimension mappings
69
76
  self._finalized = False
70
77
  self.base_units: dict[str, UnitDefinition] = {} # Track base units for prefix generation
71
78
  self.prefixable_units: set[str] = set() # Track which units can have prefixes
79
+ # Small cache for frequently used conversions to reduce table lookups
80
+ self._conversion_cache: dict[tuple[str, str], float] = {}
81
+ # Cache for common dimension mappings (used by variable.py)
82
+ self._dimension_cache: dict[int | float, UnitConstant] = {}
72
83
 
73
84
  # Registry starts empty - units are registered via register_all_units() in __init__.py
74
85
 
75
86
 
76
- def register_unit(self, unit_def: UnitDefinition):
87
+ def register_unit(self, unit_def: UnitDefinition) -> None:
77
88
  """Register a single unit definition."""
78
89
  if self._finalized:
79
90
  raise RuntimeError("Cannot register units after registry is finalized")
80
91
 
81
92
  self.units[unit_def.name] = unit_def
82
93
 
83
- # Group by dimension
94
+ # Group by dimension - optimized to avoid repeated signature access
84
95
  dim_sig = unit_def.dimension._signature
85
- if dim_sig not in self.dimensional_groups:
86
- self.dimensional_groups[dim_sig] = []
87
- self.dimensional_groups[dim_sig].append(unit_def)
96
+ try:
97
+ self.dimensional_groups[dim_sig].append(unit_def)
98
+ except KeyError:
99
+ self.dimensional_groups[dim_sig] = [unit_def]
88
100
 
89
101
  def register_with_prefixes(
90
102
  self,
91
103
  unit_def: UnitDefinition,
92
104
  prefixes: list[StandardPrefixes] | None = None
93
- ):
105
+ ) -> None:
94
106
  """
95
107
  Register a unit and automatically generate prefixed variants.
96
108
 
@@ -114,36 +126,65 @@ class HighPerformanceRegistry:
114
126
  prefixed_unit = UnitDefinition.with_prefix(unit_def, prefix)
115
127
  self.register_unit(prefixed_unit)
116
128
 
117
- def finalize_registration(self):
129
+ def finalize_registration(self) -> None:
118
130
  """Called after all units registered to precompute conversions."""
119
131
  if not self._finalized:
120
132
  self._precompute_conversions()
121
133
  self._finalized = True
122
134
 
123
- def _precompute_conversions(self):
124
- """Pre-compute all unit conversions for maximum speed."""
135
+ def _precompute_conversions(self) -> None:
136
+ """Pre-compute all unit conversions for maximum speed with optimized algorithms."""
125
137
  self.conversion_table.clear() # Clear existing conversions
138
+ self._conversion_cache.clear() # Clear cache
139
+
126
140
  for group in self.dimensional_groups.values():
127
- for from_unit in group:
128
- for to_unit in group:
129
- if from_unit != to_unit:
130
- factor = from_unit.si_factor / to_unit.si_factor
131
- key = (from_unit.name, to_unit.name)
132
- self.conversion_table[key] = factor
141
+ group_size = len(group)
142
+ if group_size <= 1:
143
+ continue # Skip groups with single units
144
+
145
+ # Ultra-optimized: pre-compute all factors and names in single pass
146
+ unit_data = [(unit.name, unit.si_factor) for unit in group]
147
+
148
+ # Vectorized approach: compute all combinations efficiently
149
+ for i in range(group_size):
150
+ from_name, from_si = unit_data[i]
151
+ for j in range(group_size):
152
+ if i != j: # Skip same unit conversion
153
+ to_name, to_si = unit_data[j]
154
+ # Pre-compute factor - avoid repeated division
155
+ factor = from_si / to_si
156
+ self.conversion_table[(from_name, to_name)] = factor
133
157
 
134
158
  def convert(self, value: float, from_unit: UnitConstant, to_unit: UnitConstant) -> float:
135
- """Ultra-fast conversion using pre-computed table."""
136
- if from_unit == to_unit:
159
+ """Ultra-fast conversion with optimized equality check and caching."""
160
+ # Ultra-fast path: avoid expensive equality check by comparing names directly
161
+ if from_unit.name == to_unit.name:
137
162
  return value
138
163
 
139
- # O(1) lookup for pre-computed conversions
140
164
  key = (from_unit.name, to_unit.name)
141
- if key in self.conversion_table:
142
- return value * self.conversion_table[key]
165
+
166
+ # Check small cache first for frequently used conversions
167
+ try:
168
+ return value * self._conversion_cache[key]
169
+ except KeyError:
170
+ pass
171
+
172
+ # O(1) lookup for pre-computed conversions
173
+ try:
174
+ factor = self.conversion_table[key]
175
+ # Cache frequently used conversions (keep cache small)
176
+ if len(self._conversion_cache) < 50:
177
+ self._conversion_cache[key] = factor
178
+ return value * factor
179
+ except KeyError:
180
+ pass
143
181
 
144
182
  # Fallback (shouldn't happen for registered units)
145
- return value * from_unit.si_factor / to_unit.si_factor
183
+ factor = from_unit.si_factor / to_unit.si_factor
184
+ if len(self._conversion_cache) < 50:
185
+ self._conversion_cache[key] = factor
186
+ return value * factor
146
187
 
147
188
 
148
189
  # Global high-performance registry
149
- registry = HighPerformanceRegistry()
190
+ registry = Registry()
qnty/utils/__init__.py ADDED
File without changes
qnty/utils/logging.py ADDED
@@ -0,0 +1,40 @@
1
+ import logging
2
+ import os
3
+
4
+ _LOGGER: logging.Logger | None = None
5
+
6
+
7
+ def get_logger(name: str = "optinova") -> logging.Logger:
8
+ """Return a module-level configured logger.
9
+
10
+ Log level resolves in order:
11
+ 1. Explicit environment variable OPTINOVA_LOG_LEVEL
12
+ 2. Existing logger level if already configured
13
+ 3. Defaults to INFO
14
+ """
15
+ global _LOGGER
16
+ if _LOGGER is not None:
17
+ return _LOGGER
18
+
19
+ logger = logging.getLogger(name)
20
+ if not logger.handlers:
21
+ # Basic handler (stdout)
22
+ handler = logging.StreamHandler()
23
+ fmt = os.getenv("OPTINOVA_LOG_FORMAT", "%(asctime)s | %(levelname)s | %(name)s | %(message)s")
24
+ handler.setFormatter(logging.Formatter(fmt))
25
+ logger.addHandler(handler)
26
+
27
+ # Resolve level
28
+ level_str = os.getenv("OPTINOVA_LOG_LEVEL", "INFO").upper()
29
+ level = getattr(logging, level_str, logging.INFO)
30
+ logger.setLevel(level)
31
+ logger.propagate = False
32
+
33
+ _LOGGER = logger
34
+ return logger
35
+
36
+
37
+ def set_log_level(level: str):
38
+ """Dynamically adjust log level at runtime."""
39
+ logger = get_logger()
40
+ logger.setLevel(getattr(logging, level.upper(), logging.INFO))
File without changes
File without changes
@@ -0,0 +1,167 @@
1
+ """
2
+ Engineering problem checks and validation system.
3
+
4
+ This module provides a clean API for defining engineering code compliance checks,
5
+ warnings, and validation rules at the problem level rather than variable level.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Literal
12
+
13
+ from qnty.expressions import Expression as QntyExpression
14
+
15
+
16
+ @dataclass
17
+ class Rules:
18
+ """
19
+ Represents an engineering problem check (code compliance, validation, etc.).
20
+
21
+ Checks are defined at the EngineeringProblem class level and evaluated after solving.
22
+ They can represent code compliance rules, engineering judgment warnings, or
23
+ validation conditions.
24
+ """
25
+ condition: QntyExpression
26
+ message: str
27
+ warning_type: str = "VALIDATION"
28
+ severity: Literal["INFO", "WARNING", "ERROR"] = "WARNING"
29
+ name: str | None = None
30
+
31
+ def __post_init__(self):
32
+ """Generate a name if not provided."""
33
+ if self.name is None:
34
+ self.name = f"{self.warning_type}_{self.severity}"
35
+
36
+ def evaluate(self, variables: dict[str, Any]) -> dict[str, Any] | None:
37
+ """
38
+ Evaluate the check condition and return a warning dict if condition is True.
39
+
40
+ Args:
41
+ variables: Dictionary of variable name -> variable object mappings
42
+
43
+ Returns:
44
+ Warning dictionary if condition is met, None otherwise
45
+ """
46
+ try:
47
+ # Evaluate the condition expression using qnty's evaluation system
48
+ result = self._evaluate_expression(self.condition, variables)
49
+
50
+ if result:
51
+ return {
52
+ "type": self.warning_type,
53
+ "severity": self.severity,
54
+ "message": self.message,
55
+ "check_name": self.name,
56
+ "condition": str(self.condition)
57
+ }
58
+
59
+ except Exception as e:
60
+ # If evaluation fails, return an error warning
61
+ import traceback
62
+ return {
63
+ "type": "EVALUATION_ERROR",
64
+ "severity": "ERROR",
65
+ "message": f"Failed to evaluate check '{self.name}': {str(e)}",
66
+ "check_name": self.name,
67
+ "condition": str(self.condition),
68
+ "debug_info": f"Expression type: {type(self.condition)}, Variables: {list(variables.keys())}",
69
+ "traceback": traceback.format_exc()
70
+ }
71
+
72
+ return None
73
+
74
+ def _evaluate_expression(self, expr: QntyExpression, variables: dict[str, Any]) -> bool:
75
+ """
76
+ Evaluate a qnty expression with current variable values.
77
+
78
+ For qnty expressions, we can evaluate them directly since they have
79
+ references to the variable values.
80
+ """
81
+ try:
82
+ # qnty expressions have an evaluate() method that needs variable values
83
+ if hasattr(expr, 'evaluate'):
84
+ # Create a variable_values dict with the variable objects, not their quantities
85
+ var_values = {}
86
+ for var_name, var_obj in variables.items():
87
+ var_values[var_name] = var_obj
88
+
89
+ result = expr.evaluate(var_values)
90
+ # For boolean comparisons, result should be 1.0 (True) or 0.0 (False)
91
+ # Handle FastQuantity type
92
+ if hasattr(result, 'value'): # FastQuantity
93
+ return bool(result.value > 0.5)
94
+ elif hasattr(result, '__float__'):
95
+ # Use type: ignore to handle FastQuantity/Expression types that don't have __float__
96
+ return bool(float(result) > 0.5) # type: ignore[arg-type]
97
+ else:
98
+ # Last resort - try str conversion then float
99
+ result_str = str(result)
100
+ # Handle cases like "0.0 " (with units)
101
+ try:
102
+ return bool(float(result_str.split()[0]) > 0.5)
103
+ except (ValueError, IndexError):
104
+ return False
105
+ else:
106
+ # Try direct conversion as fallback
107
+ if hasattr(expr, '__float__'):
108
+ # Use type: ignore to handle Expression types that don't have __float__
109
+ return bool(float(expr) > 0.5) # type: ignore[arg-type]
110
+ else:
111
+ return bool(float(str(expr)) > 0.5)
112
+ except Exception as e:
113
+ # If evaluation fails, assume the condition is not met
114
+ # Re-raise to get better debugging info
115
+ raise e
116
+
117
+
118
+ def add_rule(
119
+ condition: QntyExpression,
120
+ message: str,
121
+ warning_type: str = "VALIDATION",
122
+ severity: Literal["INFO", "WARNING", "ERROR"] = "WARNING",
123
+ name: str | None = None
124
+ ) -> Rules:
125
+ """
126
+ Create a new engineering problem check.
127
+
128
+ This function is intended to be called at the class level when defining
129
+ EngineeringProblem subclasses. It creates Check objects that will be
130
+ automatically collected by the metaclass.
131
+
132
+ Args:
133
+ condition: A qnty Expression that evaluates to True when the check should trigger
134
+ message: Descriptive message explaining what the check means
135
+ warning_type: Category of check (e.g., "CODE_COMPLIANCE", "VALIDATION")
136
+ severity: Severity level of the check
137
+ name: Optional name for the check
138
+
139
+ Returns:
140
+ Check object that can be assigned to a class attribute
141
+
142
+ Example:
143
+ class MyProblem(EngineeringProblem):
144
+ # Variables...
145
+ P = Pressure(90, "psi")
146
+ t = Length(0.1, "inch")
147
+ D = Length(1.0, "inch")
148
+
149
+ # Checks defined at class level
150
+ thick_wall_check = add_check(
151
+ t.geq(D / 6),
152
+ "Thick wall condition detected - requires special consideration",
153
+ warning_type="CODE_COMPLIANCE",
154
+ severity="WARNING"
155
+ )
156
+ """
157
+ return Rules(
158
+ condition=condition,
159
+ message=message,
160
+ warning_type=warning_type,
161
+ severity=severity,
162
+ name=name
163
+ )
164
+
165
+ __all__ = [
166
+ "add_rule"
167
+ ]