qnty 0.0.8__py3-none-any.whl → 0.1.0__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 -59
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +4 -0
- qnty/equations/equation.py +220 -0
- qnty/equations/system.py +130 -0
- qnty/expressions/__init__.py +40 -0
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +74 -0
- qnty/expressions/nodes.py +701 -0
- qnty/expressions/types.py +70 -0
- qnty/extensions/plotting/__init__.py +0 -0
- qnty/extensions/reporting/__init__.py +0 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1031 -0
- qnty/problems/problem.py +695 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +29 -0
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/quantities/field_setter.py +12320 -0
- qnty/quantities/field_vars.py +6325 -0
- qnty/quantities/field_vars.pyi +4191 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +96 -0
- qnty/solving/order.py +403 -0
- qnty/solving/solvers/__init__.py +13 -0
- qnty/solving/solvers/base.py +82 -0
- qnty/solving/solvers/iterative.py +165 -0
- qnty/solving/solvers/simultaneous.py +475 -0
- qnty/units/__init__.py +1 -0
- qnty/units/field_units.py +10507 -0
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +203 -0
- qnty/{unit.py → units/registry.py} +89 -61
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +40 -0
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- qnty-0.1.0.dist-info/METADATA +199 -0
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -297
- qnty/expression.py +0 -553
- qnty/prefixes.py +0 -229
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -300
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -106
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.8.dist-info/METADATA +0 -355
- qnty-0.0.8.dist-info/RECORD +0 -19
- /qnty/{unit_types → extensions}/__init__.py +0 -0
- /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
- {qnty-0.0.8.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,420 @@
|
|
1
|
+
"""
|
2
|
+
Scope Discovery Service
|
3
|
+
=======================
|
4
|
+
|
5
|
+
Centralized service for automatically discovering variables from the calling scope.
|
6
|
+
Consolidates all scope inspection logic used across expressions, equations, and variable solving.
|
7
|
+
|
8
|
+
This module uses protocol-based design to avoid circular imports and duck typing performance issues.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import inspect
|
12
|
+
import logging
|
13
|
+
from typing import Any
|
14
|
+
|
15
|
+
from .protocols import TypeRegistry
|
16
|
+
|
17
|
+
# Setup logging for better debugging
|
18
|
+
_logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class ScopeDiscoveryService:
|
22
|
+
"""
|
23
|
+
Centralized service for scope discovery operations.
|
24
|
+
|
25
|
+
Provides optimized variable discovery from calling scopes with caching,
|
26
|
+
depth limits, and consistent error handling.
|
27
|
+
"""
|
28
|
+
|
29
|
+
# Class-level optimization settings
|
30
|
+
_scope_cache = {}
|
31
|
+
_frame_cache = {}
|
32
|
+
_variable_name_cache = {}
|
33
|
+
_max_scope_cache_size = 200 # Increased cache size
|
34
|
+
_max_frame_cache_size = 50
|
35
|
+
_max_search_depth = 8
|
36
|
+
_cache_hit_count = 0
|
37
|
+
_cache_miss_count = 0
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
def discover_variables(cls, required_vars: set[str], enable_caching: bool = True) -> dict[str, Any]:
|
41
|
+
"""
|
42
|
+
Discover variables from the calling scope with enhanced caching.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
required_vars: Set of variable names to find
|
46
|
+
enable_caching: Whether to use caching for performance
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Dictionary mapping variable names to variable instances
|
50
|
+
"""
|
51
|
+
if not required_vars:
|
52
|
+
return {}
|
53
|
+
|
54
|
+
# Check cache first if enabled
|
55
|
+
if enable_caching:
|
56
|
+
cache_key = frozenset(required_vars)
|
57
|
+
if cache_key in cls._scope_cache:
|
58
|
+
cls._cache_hit_count += 1
|
59
|
+
_logger.debug(f"Cache hit for variables: {required_vars}")
|
60
|
+
return cls._scope_cache[cache_key]
|
61
|
+
|
62
|
+
cls._cache_miss_count += 1
|
63
|
+
|
64
|
+
# Clean cache if it gets too large (LRU-style)
|
65
|
+
if len(cls._scope_cache) >= cls._max_scope_cache_size:
|
66
|
+
# Remove oldest 25% of entries
|
67
|
+
items_to_remove = len(cls._scope_cache) // 4
|
68
|
+
for _ in range(items_to_remove):
|
69
|
+
cls._scope_cache.pop(next(iter(cls._scope_cache)))
|
70
|
+
_logger.debug(f"Cleaned {items_to_remove} entries from scope cache")
|
71
|
+
|
72
|
+
# Get the calling frame with caching
|
73
|
+
frame = cls._get_cached_user_frame()
|
74
|
+
if frame is None:
|
75
|
+
_logger.debug("No user frame found")
|
76
|
+
return {}
|
77
|
+
|
78
|
+
try:
|
79
|
+
discovered = cls._search_frame_for_variables(frame, required_vars)
|
80
|
+
|
81
|
+
# Cache the result if caching is enabled and successful
|
82
|
+
if enable_caching and required_vars:
|
83
|
+
cache_key = frozenset(required_vars)
|
84
|
+
cls._scope_cache[cache_key] = discovered
|
85
|
+
_logger.debug(f"Cached discovery result for variables: {required_vars}")
|
86
|
+
|
87
|
+
return discovered
|
88
|
+
|
89
|
+
except Exception as e:
|
90
|
+
_logger.warning(f"Error during variable discovery: {e}")
|
91
|
+
return {}
|
92
|
+
|
93
|
+
@classmethod
|
94
|
+
def can_auto_evaluate(cls, expression: Any) -> tuple[bool, dict[str, Any]]:
|
95
|
+
"""
|
96
|
+
Check if expression can be auto-evaluated from scope.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
expression: Expression to check for auto-evaluation
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Tuple of (can_evaluate, discovered_variables)
|
103
|
+
"""
|
104
|
+
try:
|
105
|
+
# Use protocol-based checking instead of duck typing
|
106
|
+
if not TypeRegistry.is_expression(expression):
|
107
|
+
return False, {}
|
108
|
+
|
109
|
+
required_vars = expression.get_variables()
|
110
|
+
if not required_vars:
|
111
|
+
return True, {} # No variables needed, can evaluate
|
112
|
+
|
113
|
+
discovered = cls.discover_variables(required_vars, enable_caching=True)
|
114
|
+
|
115
|
+
# Check if all required variables are available and have values
|
116
|
+
for var_name in required_vars:
|
117
|
+
if var_name not in discovered:
|
118
|
+
_logger.debug(f"Variable '{var_name}' not found in scope")
|
119
|
+
return False, {}
|
120
|
+
|
121
|
+
var = discovered[var_name]
|
122
|
+
if not hasattr(var, "quantity") or var.quantity is None:
|
123
|
+
_logger.debug(f"Variable '{var_name}' has no quantity")
|
124
|
+
return False, {}
|
125
|
+
|
126
|
+
_logger.debug(f"Expression can be auto-evaluated with variables: {list(discovered.keys())}")
|
127
|
+
return True, discovered
|
128
|
+
|
129
|
+
except Exception as e:
|
130
|
+
_logger.warning(f"Error during auto-evaluation check: {e}")
|
131
|
+
return False, {}
|
132
|
+
|
133
|
+
@classmethod
|
134
|
+
def find_variables_in_scope(cls, filter_func=None) -> dict[str, Any]:
|
135
|
+
"""
|
136
|
+
Find all UnifiedVariable instances in the calling scope.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
filter_func: Optional function to filter variables (var) -> bool
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
Dictionary mapping variable names/symbols to variable instances
|
143
|
+
"""
|
144
|
+
frame = inspect.currentframe()
|
145
|
+
if frame is None:
|
146
|
+
_logger.warning("Unable to access current frame")
|
147
|
+
return {}
|
148
|
+
|
149
|
+
try:
|
150
|
+
frame = cls._find_user_frame(frame)
|
151
|
+
if frame is None:
|
152
|
+
return {}
|
153
|
+
|
154
|
+
discovered = {}
|
155
|
+
|
156
|
+
# Search locals first
|
157
|
+
for obj in frame.f_locals.values():
|
158
|
+
if TypeRegistry.is_variable(obj):
|
159
|
+
if filter_func is None or filter_func(obj):
|
160
|
+
var_name = cls._get_variable_name(obj)
|
161
|
+
if var_name:
|
162
|
+
discovered[var_name] = obj
|
163
|
+
|
164
|
+
# Search globals for remaining variables
|
165
|
+
for obj in frame.f_globals.values():
|
166
|
+
if TypeRegistry.is_variable(obj):
|
167
|
+
if filter_func is None or filter_func(obj):
|
168
|
+
var_name = cls._get_variable_name(obj)
|
169
|
+
if var_name and var_name not in discovered:
|
170
|
+
discovered[var_name] = obj
|
171
|
+
|
172
|
+
return discovered
|
173
|
+
|
174
|
+
finally:
|
175
|
+
del frame
|
176
|
+
|
177
|
+
@classmethod
|
178
|
+
def _get_cached_user_frame(cls) -> Any | None:
|
179
|
+
"""
|
180
|
+
Get user frame with caching to reduce repeated frame traversal.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
User frame or None if not found
|
184
|
+
"""
|
185
|
+
# Get current frame for cache key generation
|
186
|
+
current_frame = inspect.currentframe()
|
187
|
+
if current_frame is None:
|
188
|
+
return None
|
189
|
+
|
190
|
+
try:
|
191
|
+
# Create cache key based on frame signature
|
192
|
+
frame_id = id(current_frame)
|
193
|
+
|
194
|
+
# Check frame cache first
|
195
|
+
if frame_id in cls._frame_cache:
|
196
|
+
cached_frame = cls._frame_cache[frame_id]
|
197
|
+
if cached_frame is not None:
|
198
|
+
return cached_frame
|
199
|
+
|
200
|
+
# Clean frame cache if too large
|
201
|
+
if len(cls._frame_cache) >= cls._max_frame_cache_size:
|
202
|
+
cls._frame_cache.clear()
|
203
|
+
|
204
|
+
# Find user frame using optimized search
|
205
|
+
user_frame = cls._find_user_frame_optimized(current_frame)
|
206
|
+
|
207
|
+
# Cache the result
|
208
|
+
cls._frame_cache[frame_id] = user_frame
|
209
|
+
return user_frame
|
210
|
+
|
211
|
+
finally:
|
212
|
+
del current_frame
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def _find_user_frame_optimized(cls, current_frame: Any) -> Any | None:
|
216
|
+
"""
|
217
|
+
Optimized frame search with precompiled patterns.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
current_frame: Starting frame
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
User frame or None if not found within depth limit
|
224
|
+
"""
|
225
|
+
frame = current_frame
|
226
|
+
depth = 0
|
227
|
+
|
228
|
+
# Pre-compiled sets for faster lookups
|
229
|
+
internal_file_endings = frozenset(["expression.py", "equation.py", "nodes.py", "scope_discovery.py", "expression_quantity.py", "unified_variable.py", "field_qnty.py"])
|
230
|
+
internal_function_names = frozenset(["__str__", "__repr__", "_can_auto_evaluate", "_discover_variables_from_scope", "solve_from", "evaluate"])
|
231
|
+
|
232
|
+
while frame and depth < cls._max_search_depth:
|
233
|
+
code = frame.f_code
|
234
|
+
filename = code.co_filename
|
235
|
+
function_name = code.co_name
|
236
|
+
|
237
|
+
# Fast check: is this frame internal?
|
238
|
+
is_internal = any(filename.endswith(ending) for ending in internal_file_endings) or function_name in internal_function_names
|
239
|
+
|
240
|
+
if not is_internal:
|
241
|
+
_logger.debug(f"Found user frame at depth {depth}: {filename}:{function_name}")
|
242
|
+
return frame
|
243
|
+
|
244
|
+
frame = frame.f_back
|
245
|
+
depth += 1
|
246
|
+
|
247
|
+
_logger.debug(f"No user frame found within {cls._max_search_depth} levels")
|
248
|
+
return None
|
249
|
+
|
250
|
+
@classmethod
|
251
|
+
def _find_user_frame(cls, current_frame: Any) -> Any | None:
|
252
|
+
"""
|
253
|
+
Legacy method for backward compatibility.
|
254
|
+
"""
|
255
|
+
return cls._find_user_frame_optimized(current_frame)
|
256
|
+
|
257
|
+
@classmethod
|
258
|
+
def _search_frame_for_variables(cls, frame: Any, required_vars: set[str]) -> dict[str, Any]:
|
259
|
+
"""
|
260
|
+
Search a specific frame for required variables.
|
261
|
+
|
262
|
+
Args:
|
263
|
+
frame: Frame to search
|
264
|
+
required_vars: Set of variable names to find
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
Dictionary of found variables
|
268
|
+
"""
|
269
|
+
discovered = {}
|
270
|
+
|
271
|
+
# Search locals first (most common case)
|
272
|
+
local_vars = frame.f_locals
|
273
|
+
for var_name in required_vars:
|
274
|
+
# Direct lookup first (fastest)
|
275
|
+
if var_name in local_vars:
|
276
|
+
obj = local_vars[var_name]
|
277
|
+
if TypeRegistry.is_variable(obj):
|
278
|
+
discovered[var_name] = obj
|
279
|
+
continue
|
280
|
+
|
281
|
+
# Get globals once for reuse
|
282
|
+
global_vars = frame.f_globals
|
283
|
+
|
284
|
+
# Search globals only for remaining variables
|
285
|
+
if len(discovered) < len(required_vars):
|
286
|
+
remaining_vars = required_vars - discovered.keys()
|
287
|
+
for var_name in remaining_vars:
|
288
|
+
if var_name in global_vars:
|
289
|
+
obj = global_vars[var_name]
|
290
|
+
if TypeRegistry.is_variable(obj):
|
291
|
+
discovered[var_name] = obj
|
292
|
+
|
293
|
+
# Search by symbol/name for remaining variables (optimized)
|
294
|
+
if len(discovered) < len(required_vars):
|
295
|
+
remaining_vars = required_vars - discovered.keys()
|
296
|
+
cls._search_by_symbol_name_optimized(local_vars, global_vars, remaining_vars, discovered)
|
297
|
+
|
298
|
+
_logger.debug(f"Found {len(discovered)} of {len(required_vars)} required variables")
|
299
|
+
return discovered
|
300
|
+
|
301
|
+
@classmethod
|
302
|
+
def _search_by_symbol_name_optimized(cls, local_vars: dict, global_vars: dict, remaining_vars: set[str], discovered: dict[str, Any]) -> None:
|
303
|
+
"""Optimized search for variables by their symbol/name attribute."""
|
304
|
+
# Convert to list once to avoid repeated set operations
|
305
|
+
remaining_list = list(remaining_vars)
|
306
|
+
|
307
|
+
# Search locals by symbol/name with early termination
|
308
|
+
for obj in local_vars.values():
|
309
|
+
if not remaining_list: # Check list instead of set
|
310
|
+
break
|
311
|
+
if TypeRegistry.is_variable(obj):
|
312
|
+
obj_name = cls._get_variable_name(obj)
|
313
|
+
if obj_name in remaining_vars: # Still check set for O(1) lookup
|
314
|
+
discovered[obj_name] = obj
|
315
|
+
remaining_list.remove(obj_name)
|
316
|
+
remaining_vars.remove(obj_name)
|
317
|
+
|
318
|
+
# Search globals by symbol/name if still needed
|
319
|
+
if remaining_list:
|
320
|
+
for obj in global_vars.values():
|
321
|
+
if not remaining_list:
|
322
|
+
break
|
323
|
+
if TypeRegistry.is_variable(obj):
|
324
|
+
obj_name = cls._get_variable_name(obj)
|
325
|
+
if obj_name in remaining_vars:
|
326
|
+
discovered[obj_name] = obj
|
327
|
+
remaining_list.remove(obj_name)
|
328
|
+
remaining_vars.remove(obj_name)
|
329
|
+
|
330
|
+
@classmethod
|
331
|
+
def _search_by_symbol_name(cls, local_vars: dict, global_vars: dict, remaining_vars: set[str], discovered: dict[str, Any]) -> None:
|
332
|
+
"""Legacy method for backward compatibility."""
|
333
|
+
cls._search_by_symbol_name_optimized(local_vars, global_vars, remaining_vars, discovered)
|
334
|
+
|
335
|
+
@classmethod
|
336
|
+
def _get_variable_name(cls, var) -> str | None:
|
337
|
+
"""
|
338
|
+
Get the name/symbol to use for a variable with caching.
|
339
|
+
|
340
|
+
Args:
|
341
|
+
var: Variable instance
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
Variable name/symbol or None if not available
|
345
|
+
"""
|
346
|
+
var_id = id(var)
|
347
|
+
|
348
|
+
# Check cache first
|
349
|
+
if var_id in cls._variable_name_cache:
|
350
|
+
return cls._variable_name_cache[var_id]
|
351
|
+
|
352
|
+
try:
|
353
|
+
# Prefer symbol over name for equation solving
|
354
|
+
name = var.symbol if var.symbol else var.name
|
355
|
+
|
356
|
+
# Cache the result
|
357
|
+
cls._variable_name_cache[var_id] = name
|
358
|
+
return name
|
359
|
+
except (AttributeError, TypeError):
|
360
|
+
cls._variable_name_cache[var_id] = None
|
361
|
+
return None
|
362
|
+
|
363
|
+
@classmethod
|
364
|
+
def clear_cache(cls) -> None:
|
365
|
+
"""Clear all caches for testing or memory management."""
|
366
|
+
cls._scope_cache.clear()
|
367
|
+
cls._frame_cache.clear()
|
368
|
+
cls._variable_name_cache.clear()
|
369
|
+
cls._cache_hit_count = 0
|
370
|
+
cls._cache_miss_count = 0
|
371
|
+
TypeRegistry.clear_cache()
|
372
|
+
_logger.debug("Cleared all scope discovery caches")
|
373
|
+
|
374
|
+
@classmethod
|
375
|
+
def get_cache_stats(cls) -> dict[str, Any]:
|
376
|
+
"""Get cache performance statistics."""
|
377
|
+
total_requests = cls._cache_hit_count + cls._cache_miss_count
|
378
|
+
hit_rate = (cls._cache_hit_count / total_requests * 100) if total_requests > 0 else 0
|
379
|
+
|
380
|
+
return {
|
381
|
+
"scope_cache_size": len(cls._scope_cache),
|
382
|
+
"frame_cache_size": len(cls._frame_cache),
|
383
|
+
"variable_name_cache_size": len(cls._variable_name_cache),
|
384
|
+
"cache_hits": cls._cache_hit_count,
|
385
|
+
"cache_misses": cls._cache_miss_count,
|
386
|
+
"hit_rate_percent": round(hit_rate, 2),
|
387
|
+
}
|
388
|
+
|
389
|
+
@classmethod
|
390
|
+
def set_max_depth(cls, depth: int) -> None:
|
391
|
+
"""Set maximum search depth for scope discovery."""
|
392
|
+
if depth > 0:
|
393
|
+
cls._max_search_depth = depth
|
394
|
+
_logger.debug(f"Set max search depth to {depth}")
|
395
|
+
else:
|
396
|
+
raise ValueError("Depth must be positive")
|
397
|
+
|
398
|
+
@classmethod
|
399
|
+
def enable_debug_logging(cls) -> None:
|
400
|
+
"""Enable debug logging for scope discovery operations."""
|
401
|
+
logging.getLogger(__name__).setLevel(logging.DEBUG)
|
402
|
+
|
403
|
+
@classmethod
|
404
|
+
def disable_debug_logging(cls) -> None:
|
405
|
+
"""Disable debug logging for scope discovery operations."""
|
406
|
+
logging.getLogger(__name__).setLevel(logging.WARNING)
|
407
|
+
|
408
|
+
|
409
|
+
# Convenience function for backward compatibility
|
410
|
+
def discover_variables_from_scope(required_vars: set[str]) -> dict[str, Any]:
|
411
|
+
"""
|
412
|
+
Convenience function to discover variables from scope.
|
413
|
+
|
414
|
+
Args:
|
415
|
+
required_vars: Set of variable names to find
|
416
|
+
|
417
|
+
Returns:
|
418
|
+
Dictionary mapping variable names to variable instances
|
419
|
+
"""
|
420
|
+
return ScopeDiscoveryService.discover_variables(required_vars)
|
@@ -0,0 +1,199 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: qnty
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: High-performance unit system library for Python with dimensional safety and fast unit conversions
|
5
|
+
License: Apache-2.0
|
6
|
+
Keywords: units,dimensional analysis,engineering,physics,quantities,measurements
|
7
|
+
Author: tn3wman
|
8
|
+
Requires-Python: >=3.11, <3.14
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
20
|
+
Provides-Extra: benchmark
|
21
|
+
Provides-Extra: dev
|
22
|
+
Requires-Dist: Pint (>=0.24.4) ; extra == "benchmark"
|
23
|
+
Requires-Dist: numpy (>=2.3.2)
|
24
|
+
Requires-Dist: pytest (>=8.4.1) ; extra == "dev"
|
25
|
+
Requires-Dist: ruff (>=0.1.0) ; extra == "dev"
|
26
|
+
Project-URL: Bug Tracker, https://github.com/tn3wman/qnty/issues
|
27
|
+
Project-URL: Documentation, https://github.com/tn3wman/qnty#readme
|
28
|
+
Project-URL: Homepage, https://github.com/tn3wman/qnty
|
29
|
+
Project-URL: Repository, https://github.com/tn3wman/qnty
|
30
|
+
Description-Content-Type: text/markdown
|
31
|
+
|
32
|
+
# Qnty
|
33
|
+
|
34
|
+
**High-performance unit system library for Python with dimensional safety and fast unit conversions for engineering calculations.**
|
35
|
+
|
36
|
+
[](https://www.python.org/downloads/)
|
37
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
38
|
+
[](https://pypi.org/project/qnty/)
|
39
|
+
|
40
|
+
## ⚠️ Important Disclaimer
|
41
|
+
|
42
|
+
**🚧 Work in Progress**: Qnty is currently in active development and has not been thoroughly vetted for production engineering calculations. While we strive for accuracy, this library should not be used for critical engineering applications without independent verification.
|
43
|
+
|
44
|
+
**📐 Accuracy Notice**: The authors are not responsible or liable for incorrect results, calculation errors, or any consequences arising from the use of this library. Always validate calculations independently using established engineering tools and practices.
|
45
|
+
|
46
|
+
*Use Qnty to help prevent unit errors, but always verify critical calculations through multiple methods.*
|
47
|
+
|
48
|
+
---
|
49
|
+
|
50
|
+
## ✨ Key Features
|
51
|
+
|
52
|
+
- **🚀 Ultra-Fast Performance**: Prime number encoding and pre-computed conversion tables
|
53
|
+
- **🛡️ Type Safety**: Compile-time dimensional analysis prevents unit errors
|
54
|
+
- **⚡ Zero-Cost Abstractions**: Optimized operations with `__slots__` and caching
|
55
|
+
- **🔗 Fluent API**: Intuitive method chaining for readable code
|
56
|
+
- **🧮 Engineering-Focused**: Built for real-world engineering calculations
|
57
|
+
- **🧬 Mathematical System**: Built-in equation solving and expression trees
|
58
|
+
- **📊 Comprehensive Testing**: 187 tests with performance benchmarks
|
59
|
+
|
60
|
+
## 🚀 Quick Start
|
61
|
+
|
62
|
+
### Installation
|
63
|
+
|
64
|
+
```bash
|
65
|
+
pip install qnty
|
66
|
+
```
|
67
|
+
|
68
|
+
### Basic Usage
|
69
|
+
|
70
|
+
```python
|
71
|
+
from qnty import Length, Pressure, Area
|
72
|
+
|
73
|
+
# Create quantities with dimensional safety
|
74
|
+
width = Length(3, "meter", "Width")
|
75
|
+
height = Length(2, "meter", "Height")
|
76
|
+
|
77
|
+
# Solve mathematical expressions
|
78
|
+
area = Area("area", is_known=False)
|
79
|
+
area.solve_from(width * height)
|
80
|
+
print(f"Area: {area}") # Area: 6.0 m²
|
81
|
+
```
|
82
|
+
|
83
|
+
### Engineering Example
|
84
|
+
|
85
|
+
```python
|
86
|
+
from qnty import Problem, Length, Pressure
|
87
|
+
|
88
|
+
class PipeThickness(Problem):
|
89
|
+
"""Calculate pipe wall thickness"""
|
90
|
+
|
91
|
+
# Known parameters
|
92
|
+
pressure = Pressure(150, "pound_force_per_square_inch", "Internal Pressure")
|
93
|
+
diameter = Length(6, "inch", "Pipe Diameter")
|
94
|
+
allowable_stress = Pressure(20000, "pound_force_per_square_inch", "Allowable Stress")
|
95
|
+
|
96
|
+
# Unknown to solve for
|
97
|
+
thickness = Length("thickness", is_known=False)
|
98
|
+
|
99
|
+
# Engineering equation: t = (P × D) / (2 × S)
|
100
|
+
equation = thickness.equals((pressure * diameter) / (2 * allowable_stress))
|
101
|
+
|
102
|
+
# Solve the problem
|
103
|
+
problem = PipeThickness()
|
104
|
+
problem.solve()
|
105
|
+
print(f"Required thickness: {problem.thickness}")
|
106
|
+
```
|
107
|
+
|
108
|
+
### Mathematical Operations
|
109
|
+
|
110
|
+
```python
|
111
|
+
from qnty import Length, sqrt, Area
|
112
|
+
|
113
|
+
# Dimensional analysis with mathematical functions
|
114
|
+
area = Area(25, "square_meter", "Square Area")
|
115
|
+
side = Length("side", is_known=False)
|
116
|
+
side.solve_from(sqrt(area)) # Returns Length, not Area!
|
117
|
+
print(f"Side length: {side}") # Side length: 5.0 m
|
118
|
+
```
|
119
|
+
|
120
|
+
## 📚 Documentation
|
121
|
+
|
122
|
+
- **[📖 Tutorial](docs/TUTORIAL.md)** - Step-by-step learning guide
|
123
|
+
- **[📋 API Reference](docs/API_REFERENCE.md)** - Complete API documentation
|
124
|
+
- **[🏗️ Examples](examples/)** - Real-world engineering examples
|
125
|
+
- **[📁 Full Documentation](docs/)** - Complete documentation index
|
126
|
+
|
127
|
+
## 🚀 Performance
|
128
|
+
|
129
|
+
Qnty significantly outperforms other unit libraries with **18.9x average speedup** over Pint:
|
130
|
+
|
131
|
+
| Operation | Qnty | Pint | **Speedup** |
|
132
|
+
|-----------|------|------|-------------|
|
133
|
+
| Mixed Unit Addition | 0.76 μs | 17.52 μs | **23.1x** |
|
134
|
+
| Complex ASME Equation | 4.07 μs | 106.17 μs | **26.1x** 🚀 |
|
135
|
+
| Type-Safe Variables | 0.98 μs | 9.65 μs | **9.8x** |
|
136
|
+
| **AVERAGE** | **1.89 μs** | **35.83 μs** | **18.9x** 🏆 |
|
137
|
+
|
138
|
+
*Run `pytest tests/test_benchmark.py -v -s` to verify on your system.*
|
139
|
+
|
140
|
+
## 🧮 100+ Engineering Quantities
|
141
|
+
|
142
|
+
Qnty provides comprehensive coverage of engineering domains:
|
143
|
+
|
144
|
+
```python
|
145
|
+
from qnty import (
|
146
|
+
# Mechanical
|
147
|
+
Length, Area, Volume, Mass, Force, Pressure, Temperature,
|
148
|
+
# Electrical
|
149
|
+
ElectricPotential, ElectricCurrentIntensity, ElectricResistance,
|
150
|
+
# Thermal
|
151
|
+
ThermalConductivity, HeatTransferCoefficient,
|
152
|
+
# Fluid Dynamics
|
153
|
+
ViscosityDynamic, MassFlowRate, VolumetricFlowRate,
|
154
|
+
# And 80+ more...
|
155
|
+
)
|
156
|
+
```
|
157
|
+
|
158
|
+
## 🔧 Development
|
159
|
+
|
160
|
+
```bash
|
161
|
+
# Install dependencies
|
162
|
+
pip install -r requirements.txt
|
163
|
+
|
164
|
+
# Run tests
|
165
|
+
pytest
|
166
|
+
|
167
|
+
# Run specific test
|
168
|
+
pytest tests/test_dimension.py -v
|
169
|
+
|
170
|
+
# Run benchmarks
|
171
|
+
python tests/test_benchmark.py
|
172
|
+
|
173
|
+
# Lint code
|
174
|
+
ruff check src/ tests/
|
175
|
+
ruff format src/ tests/
|
176
|
+
```
|
177
|
+
|
178
|
+
## 📄 License
|
179
|
+
|
180
|
+
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
181
|
+
|
182
|
+
## 🤝 Contributing
|
183
|
+
|
184
|
+
We welcome contributions! Please see [CLAUDE.md](CLAUDE.md) for development guidelines and:
|
185
|
+
|
186
|
+
1. Fork the repository
|
187
|
+
2. Create a feature branch
|
188
|
+
3. Add tests for new functionality
|
189
|
+
4. Ensure all tests pass: `pytest`
|
190
|
+
5. Submit a pull request
|
191
|
+
|
192
|
+
---
|
193
|
+
|
194
|
+
**Ready to supercharge your engineering calculations?** 🚀
|
195
|
+
|
196
|
+
- Start with the **[Tutorial](docs/TUTORIAL.md)**
|
197
|
+
- Browse the **[API Reference](docs/API_REFERENCE.md)**
|
198
|
+
- Try the **[Examples](examples/)**
|
199
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
qnty/__init__.py,sha256=JlMYdpcYFD8agf9nJrxnBw-MXKzGxcqCO4wFxTICQaA,5564
|
2
|
+
qnty/constants/__init__.py,sha256=-ADevTOA1tgi5OebYDFzP5ov8jq7Bwy66BCZqiXmGJk,267
|
3
|
+
qnty/constants/numerical.py,sha256=zs1OaSV5NDnxHLELzHz_G3NcetuF7JEO0Lih4BGUz60,679
|
4
|
+
qnty/constants/solvers.py,sha256=f9bMxxSu99Bg0laWBnLkWbbD5-N02rHiLqlTmO-VSik,212
|
5
|
+
qnty/constants/tests.py,sha256=viGxxGBDIu0uA2nJtK20liJ1_ZjqkC20XghxwDaXRv8,218
|
6
|
+
qnty/dimensions/__init__.py,sha256=MtHz9Auvpv2xJ4jxRTT7mL1EFk5fuBLJlhxhSRRfSRc,672
|
7
|
+
qnty/dimensions/base.py,sha256=YkEn0fvXKq2Utruh2lGAdIT1mVrhu24QXfDl0CMUEG4,2604
|
8
|
+
qnty/dimensions/field_dims.py,sha256=bcPu1xxPhjoNjc7TxyP_B4xKDLHKGdtNne-sCB9hz-8,5300
|
9
|
+
qnty/dimensions/field_dims.pyi,sha256=lk3YrH3Ovs3CJCZe5MfX334kdpmsfEql4D3fLKjuYDs,4575
|
10
|
+
qnty/dimensions/signature.py,sha256=yk7QGejAV-TEPTqWE1Q5yV2sZA-RWGiK_rHiMT0Q2yU,4173
|
11
|
+
qnty/equations/__init__.py,sha256=Ou5H6tTFXgVw16JYan_a4653NxroBxcnTY6YWt380Qo,108
|
12
|
+
qnty/equations/equation.py,sha256=6Ot3-XhSFyxdv3hUwJvdTvB2BGZlAoXX46uok_6q-14,9041
|
13
|
+
qnty/equations/system.py,sha256=vMoD1iTUrAHnVFVvCUKeyNfSBfMiqpwQbfDx46kN9N8,5155
|
14
|
+
qnty/expressions/__init__.py,sha256=DA2s7DBhVCmdUgsYSTJWObsp2DbbpFn492yr1nUTg2g,930
|
15
|
+
qnty/expressions/formatter.py,sha256=yLGLwLYjhBvVi2Q6rfkg8pbyH0-a1Ko0AYLsqJTJf50,7806
|
16
|
+
qnty/expressions/functions.py,sha256=ek43udfUDpThKo38rVPBYPvKfZNc9Bbs8RuL-CvQc_A,2729
|
17
|
+
qnty/expressions/nodes.py,sha256=7JxHzhqZNrNUqShIBsIyuLmHQyeC14m4RxPCwxKvmrE,28431
|
18
|
+
qnty/expressions/types.py,sha256=eoM-IqY-k-IypRHAlRwjEtMmB6DiwX7YGot8t_vGw3o,1729
|
19
|
+
qnty/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
qnty/extensions/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
+
qnty/extensions/plotting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
+
qnty/extensions/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
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
|
26
|
+
qnty/problems/rules.py,sha256=NwIStAa8bocVtvzAsnPmRdC_0ENTJWyXLOoYBnkvpPA,5176
|
27
|
+
qnty/problems/solving.py,sha256=LTI8F9ujDiSqXE9Aiz8sOgaGJNX9p7oaR5CQIZHpCY8,44315
|
28
|
+
qnty/problems/validation.py,sha256=SmFEsgHx5XwRNlR2suOhxO-WNsOwPZhCP8wyVKYo1EE,4826
|
29
|
+
qnty/quantities/__init__.py,sha256=K_h5v6X6-OyITSXOhbIZTDAJe6-y_7iMMDEIQ4O3luc,809
|
30
|
+
qnty/quantities/base_qnty.py,sha256=QasOR4-a7gwPBvc6cLJ3ooQHmOcWbYexgtNQ9I6bXI8,32516
|
31
|
+
qnty/quantities/field_converters.py,sha256=rDWttIE0lwF1doGlLG5RJTcTikYEYruMrRBMlr8fvBI,1008701
|
32
|
+
qnty/quantities/field_qnty.py,sha256=9A-KP8DyO5oOfmxw41HKJq48dUF0LP9M_YYqvdbVvRM,42562
|
33
|
+
qnty/quantities/field_setter.py,sha256=JCvRom4qvCYvgRNgFLZEuwb1PCmz0qrKzxDv0-h4RMo,449348
|
34
|
+
qnty/quantities/field_vars.py,sha256=mo-kh3WFx6h_dfROUIXAyhdDAoEDEgGN_TlaLwu_o1U,264561
|
35
|
+
qnty/quantities/field_vars.pyi,sha256=DJtLmxXJ9MrphAqSSOkMYNlLrq7-mAzvclp6bufT4RY,154868
|
36
|
+
qnty/solving/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
|
+
qnty/solving/manager.py,sha256=LQBMhWD3ajRYMBXkwRpkVzdo7qVEDviBAoHpjAzS-0U,3945
|
38
|
+
qnty/solving/order.py,sha256=q1G3fMWamhmBK6vN0L2BuTqWl0aa94ZJPCWusrntcXo,15488
|
39
|
+
qnty/solving/solvers/__init__.py,sha256=7hM2Fz4jCS8xf5Kd3qDqkEmvtT1PO72jXguoQA_Fvqo,475
|
40
|
+
qnty/solving/solvers/base.py,sha256=UgXRhnx9m331Hn2P3U-0qTfSQIkfOhEw_MfqcLhxxBM,2839
|
41
|
+
qnty/solving/solvers/iterative.py,sha256=dZw66VRz0ScnkPsJl2miKgH7VVcGJQw1zq-5wzU0CZU,7557
|
42
|
+
qnty/solving/solvers/simultaneous.py,sha256=hLZ8878BqiI5NQd8eeRG5Sy9ztS43BBX4mk3BDMVs_4,21609
|
43
|
+
qnty/units/__init__.py,sha256=WgH6t1obC5i2ioEykO8H3z_csoRL6fP6xjzXn2_EB80,27
|
44
|
+
qnty/units/field_units.py,sha256=juW8qzt7UaL4lIMl5p_GVcfH6h6gQz7bFuOUOKRX7rY,283034
|
45
|
+
qnty/units/field_units.pyi,sha256=1iW_yWF2PWQ8wZ1vSp2SbHk4A6eiG-Hfw-9VdSBf7s0,77949
|
46
|
+
qnty/units/prefixes.py,sha256=tSD8CIPjFtCuwl2Og1a1pmqLoTvfeTljxMqPuHBC_dE,6667
|
47
|
+
qnty/units/registry.py,sha256=bfnD4kWkaEnX0Vkb7Dxa1K-YGhtjMNQjevLpcMc7KCQ,6728
|
48
|
+
qnty/utils/__init__.py,sha256=r9sYK7anOF5KoDqw6rK0O1dCVXV4MKAtt_4gab7PZIk,533
|
49
|
+
qnty/utils/caching/__init__.py,sha256=vgR18cUhDnOkB-3Qv-Kcj0l9lSZ_cD48MSfjaAHKjpA,456
|
50
|
+
qnty/utils/caching/manager.py,sha256=B9-p9eD57SjBTZwfa8LqfAwK7arhy0zH7faoE_lQt8M,16893
|
51
|
+
qnty/utils/error_handling/__init__.py,sha256=6J8a1NdaeQGtGa6r6Rc-x8FcIc5adJZ6ngm21bZXbFI,1571
|
52
|
+
qnty/utils/error_handling/context.py,sha256=hTwQst5sFWKo2UrpH6768ERwqcFfbwVQz8aNuO0fveo,1353
|
53
|
+
qnty/utils/error_handling/exceptions.py,sha256=8O0gzoTJqz1Gw9h8sdN16DKIf_ZBn2Lzsu9gyLHknXU,3608
|
54
|
+
qnty/utils/error_handling/handlers.py,sha256=_q12co-jr4YSktRoCPpGBbh6WXEDw9MbmWxUge0YWU8,8007
|
55
|
+
qnty/utils/logging.py,sha256=2H6_gSOQjxdK5024XTY3E1jGIQPE8WdalVhVBFw51OA,1143
|
56
|
+
qnty/utils/protocols.py,sha256=c_Ya_epCm7qenAADRMZiwiQ0PdD-Z4T85b1z1YQNXAk,5247
|
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,,
|