units.py 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. from __future__ import annotations
  2. from marshmallow import fields, validate, ValidationError
  3. from flexmeasures.data.schemas.utils import MarshmallowClickMixin, convert_to_quantity
  4. from flexmeasures.utils.unit_utils import is_valid_unit, ur
  5. class QuantityValidator(validate.Validator):
  6. """Validator which succeeds if the value passed to it is a valid quantity."""
  7. def __init__(self, *, error: str | None = None):
  8. self.error = error
  9. def __call__(self, value):
  10. if not is_valid_unit(value):
  11. raise ValidationError("Not a valid quantity")
  12. return value
  13. class QuantityField(MarshmallowClickMixin, fields.Str):
  14. """Marshmallow/Click field for validating quantities against a unit registry.
  15. The FlexMeasures unit registry is based on the pint library.
  16. For example:
  17. >>> percentage_field = QuantityField("%", validate=validate.Range(min=0, max=1))
  18. >>> percentage_field.deserialize("2.5%")
  19. <Quantity(2.5, 'percent')>
  20. >>> percentage_field.deserialize(0.025)
  21. <Quantity(2.5, 'percent')>
  22. >>> power_field = QuantityField("kW", validate=validate.Range(max=ur.Quantity("1 kW")))
  23. >>> power_field.deserialize("120 W")
  24. <Quantity(0.12, 'kilowatt')>
  25. """
  26. def __init__(
  27. self,
  28. to_unit: str,
  29. *args,
  30. default_src_unit: str | None = None,
  31. return_magnitude: bool = False,
  32. **kwargs,
  33. ):
  34. super().__init__(*args, **kwargs)
  35. # Insert validation into self.validators so that multiple errors can be stored.
  36. validator = QuantityValidator()
  37. self.validators.insert(0, validator)
  38. if to_unit.startswith("/") and len(to_unit) < 2:
  39. raise ValueError(
  40. f"Variable `to_unit='{to_unit}'` must define a denominator."
  41. )
  42. self.to_unit = to_unit
  43. self.default_src_unit = default_src_unit
  44. self.return_magnitude = return_magnitude
  45. def _deserialize(
  46. self,
  47. value,
  48. attr,
  49. obj,
  50. return_magnitude: bool | None = None,
  51. **kwargs,
  52. ) -> ur.Quantity:
  53. """Turn a quantity describing string into a Quantity."""
  54. if return_magnitude is None:
  55. return_magnitude = self.return_magnitude
  56. if isinstance(value, str):
  57. q = convert_to_quantity(value=value, to_unit=self.to_unit)
  58. elif self.default_src_unit is not None:
  59. q = self._deserialize(
  60. f"{value} {self.default_src_unit}",
  61. attr,
  62. obj,
  63. **kwargs,
  64. return_magnitude=False,
  65. )
  66. else:
  67. q = self._deserialize(
  68. f"{value}", attr, obj, **kwargs, return_magnitude=False
  69. )
  70. if return_magnitude:
  71. return q.magnitude
  72. return q
  73. def _serialize(self, value, attr, data, **kwargs):
  74. """Turn a Quantity into a string in scientific format."""
  75. return "{:~P}".format(value.to(ur.Quantity(self.to_unit)))