2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +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:
Oliver
2024-02-22 10:22:23 +11:00
committed by GitHub
parent 8bf614607c
commit 6e713b15ae
3 changed files with 144 additions and 25 deletions

View File

@ -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

View File

@ -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 = {