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.
- qnty/__init__.py +140 -58
- qnty/_backup/problem_original.py +1251 -0
- qnty/_backup/quantity.py +63 -0
- qnty/codegen/cli.py +125 -0
- qnty/codegen/generators/data/unit_data.json +8807 -0
- qnty/codegen/generators/data_processor.py +345 -0
- qnty/codegen/generators/dimensions_gen.py +434 -0
- qnty/codegen/generators/doc_generator.py +141 -0
- qnty/codegen/generators/out/dimension_mapping.json +974 -0
- qnty/codegen/generators/out/dimension_metadata.json +123 -0
- qnty/codegen/generators/out/units_metadata.json +223 -0
- qnty/codegen/generators/quantities_gen.py +159 -0
- qnty/codegen/generators/setters_gen.py +178 -0
- qnty/codegen/generators/stubs_gen.py +167 -0
- qnty/codegen/generators/units_gen.py +295 -0
- qnty/codegen/generators/utils/__init__.py +0 -0
- qnty/equations/__init__.py +4 -0
- qnty/equations/equation.py +257 -0
- qnty/equations/system.py +127 -0
- qnty/expressions/__init__.py +61 -0
- qnty/expressions/cache.py +94 -0
- qnty/expressions/functions.py +96 -0
- qnty/expressions/nodes.py +546 -0
- qnty/generated/__init__.py +0 -0
- qnty/generated/dimensions.py +514 -0
- qnty/generated/quantities.py +6003 -0
- qnty/generated/quantities.pyi +4192 -0
- qnty/generated/setters.py +12210 -0
- qnty/generated/units.py +9798 -0
- qnty/problem/__init__.py +91 -0
- qnty/problem/base.py +142 -0
- qnty/problem/composition.py +385 -0
- qnty/problem/composition_mixin.py +382 -0
- qnty/problem/equations.py +413 -0
- qnty/problem/metaclass.py +302 -0
- qnty/problem/reconstruction.py +1016 -0
- qnty/problem/solving.py +180 -0
- qnty/problem/validation.py +64 -0
- qnty/problem/variables.py +239 -0
- qnty/quantities/__init__.py +6 -0
- qnty/quantities/expression_quantity.py +314 -0
- qnty/quantities/quantity.py +428 -0
- qnty/quantities/typed_quantity.py +215 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +90 -0
- qnty/solving/order.py +355 -0
- qnty/solving/solvers/__init__.py +20 -0
- qnty/solving/solvers/base.py +92 -0
- qnty/solving/solvers/iterative.py +185 -0
- qnty/solving/solvers/simultaneous.py +547 -0
- qnty/units/__init__.py +0 -0
- qnty/{prefixes.py → units/prefixes.py} +54 -33
- qnty/{unit.py → units/registry.py} +73 -32
- qnty/utils/__init__.py +0 -0
- qnty/utils/logging.py +40 -0
- qnty/validation/__init__.py +0 -0
- qnty/validation/registry.py +0 -0
- qnty/validation/rules.py +167 -0
- qnty-0.0.9.dist-info/METADATA +199 -0
- qnty-0.0.9.dist-info/RECORD +63 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -216
- qnty/expression.py +0 -492
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -263
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -68
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.7.dist-info/METADATA +0 -355
- qnty-0.0.7.dist-info/RECORD +0 -19
- /qnty/{unit_types → codegen}/__init__.py +0 -0
- /qnty/{variable_types → codegen/generators}/__init__.py +0 -0
- {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
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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
|
144
|
-
|
145
|
-
|
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
|
-
|
150
|
-
|
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 .
|
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
|
-
"""
|
54
|
-
|
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
|
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
|
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
|
-
|
86
|
-
self.dimensional_groups[dim_sig]
|
87
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
136
|
-
|
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
|
-
|
142
|
-
|
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
|
-
|
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 =
|
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
|
qnty/validation/rules.py
ADDED
@@ -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
|
+
]
|