mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[Feature] Engineering Units (#6539)
* Conversion: Support conversion from "engineering notation" * Add unit tests for scientific notation * Update docs for unit conversion
This commit is contained in:
parent
8bf614607c
commit
6e713b15ae
@ -1,6 +1,7 @@
|
||||
"""Helper functions for converting between units."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -9,7 +10,6 @@ import pint
|
||||
|
||||
_unit_registry = None
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
@ -70,6 +70,64 @@ def reload_unit_registry():
|
||||
return reg
|
||||
|
||||
|
||||
def from_engineering_notation(value):
|
||||
"""Convert a provided value to 'natural' representation from 'engineering' notation.
|
||||
|
||||
Ref: https://en.wikipedia.org/wiki/Engineering_notation
|
||||
|
||||
In "engineering notation", the unit (or SI prefix) is often combined with the value,
|
||||
and replaces the decimal point.
|
||||
|
||||
Examples:
|
||||
- 1K2 -> 1.2K
|
||||
- 3n05 -> 3.05n
|
||||
- 8R6 -> 8.6R
|
||||
|
||||
And, we should also take into account any provided trailing strings:
|
||||
|
||||
- 1K2 ohm -> 1.2K ohm
|
||||
- 10n005F -> 10.005nF
|
||||
"""
|
||||
value = str(value).strip()
|
||||
|
||||
pattern = f'(\d+)([a-zA-Z]+)(\d+)(.*)'
|
||||
|
||||
if match := re.match(pattern, value):
|
||||
left, prefix, right, suffix = match.groups()
|
||||
return f'{left}.{right}{prefix}{suffix}'
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def convert_value(value, unit):
|
||||
"""Attempt to convert a value to a specified unit.
|
||||
|
||||
Arguments:
|
||||
value: The value to convert
|
||||
unit: The target unit to convert to
|
||||
|
||||
Returns:
|
||||
The converted value (ideally a pint.Quantity value)
|
||||
|
||||
Raises:
|
||||
Exception if the value cannot be converted to the specified unit
|
||||
"""
|
||||
ureg = get_unit_registry()
|
||||
|
||||
# Convert the provided value to a pint.Quantity object
|
||||
value = ureg.Quantity(value)
|
||||
|
||||
# Convert to the specified unit
|
||||
if unit:
|
||||
if is_dimensionless(value):
|
||||
magnitude = value.to_base_units().magnitude
|
||||
value = ureg.Quantity(magnitude, unit)
|
||||
else:
|
||||
value = value.to(unit)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
"""Validate that the provided value is a valid physical quantity.
|
||||
|
||||
@ -94,34 +152,29 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
if not value:
|
||||
raise ValidationError(_('No value provided'))
|
||||
|
||||
# Create a "backup" value which be tried if the first value fails
|
||||
# e.g. value = "10k" and unit = "ohm" -> "10kohm"
|
||||
# e.g. value = "10m" and unit = "F" -> "10mF"
|
||||
# Construct a list of values to "attempt" to convert
|
||||
attempts = [value]
|
||||
|
||||
# Attempt to convert from engineering notation
|
||||
eng = from_engineering_notation(value)
|
||||
attempts.append(eng)
|
||||
|
||||
# Append the unit, if provided
|
||||
# These are the "final" attempts to convert the value, and *must* appear after previous attempts
|
||||
if unit:
|
||||
backup_value = value + unit
|
||||
else:
|
||||
backup_value = None
|
||||
attempts.append(f'{value}{unit}')
|
||||
attempts.append(f'{eng}{unit}')
|
||||
|
||||
ureg = get_unit_registry()
|
||||
value = None
|
||||
|
||||
try:
|
||||
value = ureg.Quantity(value)
|
||||
|
||||
if unit:
|
||||
if is_dimensionless(value):
|
||||
magnitude = value.to_base_units().magnitude
|
||||
value = ureg.Quantity(magnitude, unit)
|
||||
else:
|
||||
value = value.to(unit)
|
||||
|
||||
except Exception:
|
||||
if backup_value:
|
||||
try:
|
||||
value = ureg.Quantity(backup_value)
|
||||
except Exception:
|
||||
value = None
|
||||
else:
|
||||
# Run through the available "attempts", take the first successful result
|
||||
for attempt in attempts:
|
||||
try:
|
||||
value = convert_value(attempt, unit)
|
||||
break
|
||||
except Exception as exc:
|
||||
value = None
|
||||
pass
|
||||
|
||||
if value is None:
|
||||
if unit:
|
||||
@ -129,6 +182,8 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
else:
|
||||
raise ValidationError(_('Invalid quantity supplied'))
|
||||
|
||||
ureg = get_unit_registry()
|
||||
|
||||
# Calculate the "magnitude" of the value, as a float
|
||||
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
|
||||
# So, we ensure that it is converted to a floating point value
|
||||
|
@ -58,6 +58,43 @@ class ConversionTest(TestCase):
|
||||
q = InvenTree.conversion.convert_physical_value(val, 'm')
|
||||
self.assertAlmostEqual(q, expected, 3)
|
||||
|
||||
def test_engineering_units(self):
|
||||
"""Test that conversion works with engineering notation."""
|
||||
# Run some basic checks over the helper function
|
||||
tests = [
|
||||
('3', '3'),
|
||||
('3k3', '3.3k'),
|
||||
('123R45', '123.45R'),
|
||||
('10n5F', '10.5nF'),
|
||||
]
|
||||
|
||||
for val, expected in tests:
|
||||
self.assertEqual(
|
||||
InvenTree.conversion.from_engineering_notation(val), expected
|
||||
)
|
||||
|
||||
# Now test the conversion function
|
||||
tests = [('33k3ohm', 33300), ('123kohm45', 123450), ('10n005', 0.000000010005)]
|
||||
|
||||
for val, expected in tests:
|
||||
output = InvenTree.conversion.convert_physical_value(
|
||||
val, 'ohm', strip_units=True
|
||||
)
|
||||
self.assertAlmostEqual(output, expected, 12)
|
||||
|
||||
def test_scientific_notation(self):
|
||||
"""Test that scientific notation is handled correctly."""
|
||||
tests = [
|
||||
('3E2', 300),
|
||||
('-12.3E-3', -0.0123),
|
||||
('1.23E-3', 0.00123),
|
||||
('99E9', 99000000000),
|
||||
]
|
||||
|
||||
for val, expected in tests:
|
||||
output = InvenTree.conversion.convert_physical_value(val, strip_units=True)
|
||||
self.assertAlmostEqual(output, expected, 6)
|
||||
|
||||
def test_base_units(self):
|
||||
"""Test conversion to specified base units."""
|
||||
tests = {
|
||||
|
@ -11,6 +11,33 @@ Support for real-world "physical" units of measure is implemented using the [pin
|
||||
- Enforce use of compatible units when creating part parameters
|
||||
- Enable custom units as required
|
||||
|
||||
### Unit Conversion
|
||||
|
||||
InvenTree uses the pint library to convert between compatible units of measure. For example, it is possible to convert between units of mass (e.g. grams, kilograms, pounds, etc) or units of length (e.g. millimeters, inches, etc). This is a powerful feature that ensures that units are used consistently throughout the application.
|
||||
|
||||
### Engineering Notation
|
||||
|
||||
We support the use of engineering notation for units, which allows for easy conversion between units of different orders of magnitude. For example, the following values would all be considered *valid*:
|
||||
|
||||
- `10k3` : `10,300`
|
||||
- `10M3` : `10,000,000`
|
||||
- `3n02` : `0.00000000302`
|
||||
|
||||
### Scientific Notation
|
||||
|
||||
Scientific notation is also supported, and can be used to represent very large or very small numbers. For example, the following values would all be considered *valid*:
|
||||
|
||||
- `1E-3` : `0.001`
|
||||
- `1E3` : `1000`
|
||||
- `-123.45E-3` : `-0.12345`
|
||||
|
||||
!!! tip "Case Sensitive"
|
||||
Support for scientific notation is case sensitive. For example, `1E3` is a valid value, but `1e3` is not.
|
||||
|
||||
### Case Sensitivity
|
||||
|
||||
The pint library is case sensitive, and units must be specified in the correct case. For example, `kg` is a valid unit, but `KG` is not. In particular, you need to pay close attention when using SI prefixes (e.g. `k` for kilo, `M` for mega, `n` for nano, etc).
|
||||
|
||||
## Unit Support
|
||||
|
||||
Physical units are supported by the following InvenTree subsystems:
|
||||
|
Loading…
Reference in New Issue
Block a user