-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Function units: mag, dB, and dex #1894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7cab353
eb2db85
94dca16
678f069
50889bc
fede5de
f51e95d
9142351
9040ba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,7 @@ | |
| add_powers) | ||
| from . import format as unit_format | ||
|
|
||
| # TODO: Support functional units, e.g. log(x), ln(x) | ||
| # TODO: Support function units, e.g. log(x), ln(x) | ||
|
|
||
| __all__ = [ | ||
| 'UnitsError', 'UnitsWarning', 'UnitBase', 'NamedUnit', | ||
|
|
@@ -95,8 +95,8 @@ def _normalize_equivalencies(equivalencies): | |
| else: | ||
| raise ValueError( | ||
| "Invalid equivalence entry {0}: {1!r}".format(i, equiv)) | ||
| if not (isinstance(funit, UnitBase) and | ||
| (isinstance(tunit, UnitBase) or tunit is None) and | ||
| if not (funit is Unit(funit) and | ||
| (tunit is None or tunit is Unit(tunit)) and | ||
| six.callable(a) and | ||
| six.callable(b)): | ||
| raise ValueError( | ||
|
|
@@ -533,7 +533,6 @@ def names(self): | |
| raise AttributeError( | ||
| "Can not get names from unnamed units. " | ||
| "Perhaps you meant to_string()?") | ||
| return self._names | ||
|
|
||
| @property | ||
| def name(self): | ||
|
|
@@ -633,24 +632,30 @@ def __div__(self, m): | |
| return self | ||
| return CompositeUnit(1, [self, m], [1, -1], _error_check=False) | ||
|
|
||
| # Cannot handle this as Unit, re-try as Quantity | ||
| from .quantity import Quantity | ||
| return Quantity(1, self) / m | ||
| try: | ||
| # Cannot handle this as Unit, re-try as Quantity | ||
| from .quantity import Quantity | ||
| return Quantity(1, self) / m | ||
| except TypeError: | ||
| return NotImplemented | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice. |
||
|
|
||
| def __rdiv__(self, m): | ||
| if isinstance(m, (bytes, six.text_type)): | ||
| return Unit(m) / self | ||
|
|
||
| # Cannot handle this as Unit. Here, m cannot be a Quantity, | ||
| # so we make it into one, fasttracking when it does not have a unit, | ||
| # for the common case of <array> / <unit>. | ||
| from .quantity import Quantity | ||
| if hasattr(m, 'unit'): | ||
| result = Quantity(m) | ||
| result /= self | ||
| return result | ||
| else: | ||
| return Quantity(m, self**(-1)) | ||
| try: | ||
| # Cannot handle this as Unit. Here, m cannot be a Quantity, | ||
| # so we make it into one, fasttracking when it does not have a unit, | ||
| # for the common case of <array> / <unit>. | ||
| from .quantity import Quantity | ||
| if hasattr(m, 'unit'): | ||
| result = Quantity(m) | ||
| result /= self | ||
| return result | ||
| else: | ||
| return Quantity(m, self**(-1)) | ||
| except TypeError: | ||
| return NotImplemented | ||
|
|
||
| __truediv__ = __div__ | ||
|
|
||
|
|
@@ -668,8 +673,11 @@ def __mul__(self, m): | |
| return CompositeUnit(1, [self, m], [1, 1], _error_check=False) | ||
|
|
||
| # Cannot handle this as Unit, re-try as Quantity. | ||
| from .quantity import Quantity | ||
| return Quantity(1, self) * m | ||
| try: | ||
| from .quantity import Quantity | ||
| return Quantity(1, self) * m | ||
| except TypeError: | ||
| return NotImplemented | ||
|
|
||
| def __rmul__(self, m): | ||
| if isinstance(m, (bytes, six.text_type)): | ||
|
|
@@ -678,13 +686,16 @@ def __rmul__(self, m): | |
| # Cannot handle this as Unit. Here, m cannot be a Quantity, | ||
| # so we make it into one, fasttracking when it does not have a unit | ||
| # for the common case of <array> * <unit>. | ||
| from .quantity import Quantity | ||
| if hasattr(m, 'unit'): | ||
| result = Quantity(m) | ||
| result *= self | ||
| return result | ||
| else: | ||
| return Quantity(m, self) | ||
| try: | ||
| from .quantity import Quantity | ||
| if hasattr(m, 'unit'): | ||
| result = Quantity(m) | ||
| result *= self | ||
| return result | ||
| else: | ||
| return Quantity(m, self) | ||
| except TypeError: | ||
| return NotImplemented | ||
|
|
||
| def __hash__(self): | ||
| # This must match the hash used in CompositeUnit for a unit | ||
|
|
@@ -699,6 +710,12 @@ def __eq__(self, other): | |
| other = Unit(other, parse_strict='silent') | ||
| except (ValueError, UnitsError, TypeError): | ||
| return False | ||
|
|
||
| # Other is Unit-like, but the test below requires it is a UnitBase | ||
| # instance; if it is not, give up (so that other can try). | ||
| if not isinstance(other, UnitBase): | ||
| return NotImplemented | ||
|
|
||
| try: | ||
| return is_effectively_unity(self._to(other)) | ||
| except UnitsError: | ||
|
|
@@ -750,8 +767,8 @@ def is_equivalent(self, other, equivalencies=[]): | |
| if isinstance(other, tuple): | ||
| return any(self.is_equivalent(u, equivalencies=equivalencies) | ||
| for u in other) | ||
| else: | ||
| other = Unit(other, parse_strict='silent') | ||
|
|
||
| other = Unit(other, parse_strict='silent') | ||
|
|
||
| return self._is_equivalent(other, equivalencies) | ||
|
|
||
|
|
@@ -786,7 +803,7 @@ def _is_equivalent(self, other, equivalencies=[]): | |
|
|
||
| return False | ||
|
|
||
| def _apply_equivalences(self, unit, other, equivalencies): | ||
| def _apply_equivalencies(self, unit, other, equivalencies): | ||
| """ | ||
| Internal function (used from `_get_converter`) to apply | ||
| equivalence pairs. | ||
|
|
@@ -796,16 +813,14 @@ def convert(v): | |
| return func(_condition_arg(v) / scale1) * scale2 | ||
| return convert | ||
|
|
||
| orig_unit = unit | ||
| orig_other = other | ||
|
|
||
| unit = self.decompose() | ||
| other = other.decompose() | ||
| if hasattr(other, 'equivalencies'): | ||
| equivalencies += other.equivalencies | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have we grown
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normal units do not have these assigned at all, but my functional units do. It is important to allow that here so that one can do without explicitly giving the equivalency built in the functional unit.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does that happen?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't happen explicitly in the code, but needs to be supported to make the functional units helpful; see
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, on your actual question: equivalencies get assigned in |
||
|
|
||
| for funit, tunit, a, b in equivalencies: | ||
| if tunit is None: | ||
| try: | ||
| ratio_in_funit = (other/unit).decompose([funit]) | ||
| ratio_in_funit = (other.decompose() / | ||
| unit.decompose()).decompose([funit]) | ||
| return make_converter(ratio_in_funit.scale, a, 1.) | ||
| except UnitsError: | ||
| pass | ||
|
|
@@ -833,8 +848,8 @@ def get_err_str(unit): | |
| unit_str = "'{0}'".format(unit_str) | ||
| return unit_str | ||
|
|
||
| unit_str = get_err_str(orig_unit) | ||
| other_str = get_err_str(orig_other) | ||
| unit_str = get_err_str(unit) | ||
| other_str = get_err_str(other) | ||
|
|
||
| raise UnitsError( | ||
| "{0} and {1} are not convertible".format( | ||
|
|
@@ -846,7 +861,7 @@ def _get_converter(self, other, equivalencies=[]): | |
| try: | ||
| scale = self._to(other) | ||
| except UnitsError: | ||
| return self._apply_equivalences( | ||
| return self._apply_equivalencies( | ||
| self, other, self._normalize_equivalencies(equivalencies)) | ||
| return lambda val: scale * _condition_arg(val) | ||
|
|
||
|
|
@@ -897,17 +912,20 @@ def _to(self, other): | |
| if self is other: | ||
| return 1.0 | ||
|
|
||
| self_decomposed = self.decompose() | ||
| other_decomposed = other.decompose() | ||
|
|
||
| # Check quickly whether equivalent. This is faster than | ||
| # `is_equivalent`, because it doesn't generate the entire | ||
| # physical type list of both units. In other words it "fails | ||
| # fast". | ||
| if(self_decomposed.powers == other_decomposed.powers and | ||
| all(self_base is other_base for (self_base, other_base) | ||
| in zip(self_decomposed.bases, other_decomposed.bases))): | ||
| return self_decomposed.scale / other_decomposed.scale | ||
| # Don't presume decomposition is possible; e.g., | ||
| # conversion to function units is through equivalencies. | ||
| if isinstance(other, UnitBase): | ||
| self_decomposed = self.decompose() | ||
| other_decomposed = other.decompose() | ||
|
|
||
| # Check quickly whether equivalent. This is faster than | ||
| # `is_equivalent`, because it doesn't generate the entire | ||
| # physical type list of both units. In other words it "fails | ||
| # fast". | ||
| if(self_decomposed.powers == other_decomposed.powers and | ||
| all(self_base is other_base for (self_base, other_base) | ||
| in zip(self_decomposed.bases, other_decomposed.bases))): | ||
| return self_decomposed.scale / other_decomposed.scale | ||
|
|
||
| raise UnitsError( | ||
| "'{0!r}' is not a scaled version of '{1!r}'".format(self, other)) | ||
|
|
@@ -1719,7 +1737,7 @@ def __call__(self, s, represents=None, format=None, namespace=None, | |
| doc=None, parse_strict='raise'): | ||
|
|
||
| # Short-circuit if we're already a unit | ||
| if isinstance(s, UnitBase): | ||
| if hasattr(s, '_get_physical_type_id'): | ||
| return s | ||
|
|
||
| # turn possible Quantity input for s or represents into a Unit | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
|
|
||
| """ | ||
| This subpackage contains classes and functions for defining and converting | ||
| between different function units and quantities, i.e., using units which | ||
| are some function of a physical unit, such as magnitudes and decibels. | ||
| """ | ||
| from .core import * | ||
| from .logarithmic import * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially, I tried to base my functional units on
UnitBase(orCompositeUnit), but I found I needed to make lots of changes tocoreto get it to work (e.g., in all of the arithmetic; much cleaner if normal units just do not know how to deal with functional ones).Hence, functional units are is not a
UnitBasesubclass, but for the equivalencies they have to behave like a unit in some respects. So, I'm trying to ducktype it, but I thought it was best to limit that to only a few places, hence here I use thatUnitwill very quickly pass through unchanged something that it considers to be a unit already.In the end, I think it may be better to have a minimal Abstract Base Class for units; this may also be good if we ever would like to get the different python units packages to talk to each other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I think what's here probably makes sense for now.