Repository: wright-group/WrightTools

Issue:
raise friendly exception if unit string not in registry
Currently if a unit doesn't subscribe to an unstated style guide, things like chop and split fail (and by extension our artists). Is this desired behavior? Or should a user be able to specify any unit they desire (e.g. a unit of `'excitons/s'` is a valid physical unit but it causes a units error). 

```
import numpy as np
import WrightTools as wt

values = np.linspace(-1,1,51)
arr = np.linspace(0,1,51)

# works as expected 
d = wt.Data(name='test1')
d.create_variable('B', values=values, units='T')
d.transform('B')
d.create_channel('arr', values=arr)
wt.artists.quick1D(d)

# works as expected 
d = wt.Data(name='test2')
d.create_variable('B', values=values, units='tesla')
d.transform('B')
d.create_channel('arr', values=arr)
wt.artists.quick1D(d)

# fails at chop
d = wt.Data(name='test3')
d.create_variable('B', values=values, units='Tesla')
d.transform('B')
d.create_channel('arr', values=arr)
wt.artists.quick1D(d)
```

```
UnitsError: (UnitsError(...), 'expected units of () (recieved Tesla)')
```


Discussion:
units _must_ be valid pint: https://pint.readthedocs.io/en/stable/

they are not just arbitrary, and yes, capitalization matters (mm is different from Mm, after all)

You can register units if you like into the unit registry for your data processing.

We (mostly) use the default registry: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt

though we added a few aliases for things like `deg_C`, adding `OD` as a dimensionless quantity, etc, for the sake of backwards compatibility.
I am fine with being strict about spelling/capitalization. In this case, I think we ought to raise an exception or warning when setting somethings units which are not `None` or in the registry(s). For my personal case, it took me far too long to realize quick1D was failing because of a poorly created data object from a different day... 

Offending places that I know about which should be checked and raised include:

https://github.com/wright-group/WrightTools/blob/937f761680c07a1cd200389b40e6309c2adacecb/WrightTools/data/_variable.py#L39

https://github.com/wright-group/WrightTools/blob/937f761680c07a1cd200389b40e6309c2adacecb/WrightTools/data/_channel.py#L66

https://github.com/wright-group/WrightTools/blob/937f761680c07a1cd200389b40e6309c2adacecb/WrightTools/data/_axis.py#L57
We should make `units` in those cases a `@property` with the validation there.
@ksunden I'm not sure how to do this. Can you handle it?

Resolving patch:
diff --git a/WrightTools/_dataset.py b/WrightTools/_dataset.py
--- a/WrightTools/_dataset.py
+++ b/WrightTools/_dataset.py
@@ -185,6 +185,8 @@ def units(self, value):
         if value is None:
             if "units" in self.attrs.keys():
                 self.attrs.pop("units")
+        elif value not in wt_units.ureg:
+            raise ValueError(f"'{value}' is not in the unit registry")
         else:
             try:
                 self.attrs["units"] = value
diff --git a/WrightTools/data/_axis.py b/WrightTools/data/_axis.py
--- a/WrightTools/data/_axis.py
+++ b/WrightTools/data/_axis.py
@@ -141,6 +141,18 @@ def size(self) -> int:
         """Size."""
         return functools.reduce(operator.mul, self.shape)
 
+    @property
+    def units(self):
+        return self._units
+
+    @units.setter
+    def units(self, value):
+        if value == "None":
+            value = None
+        if value is not None and value not in wt_units.ureg:
+            raise ValueError(f"'{value}' is not in the unit registry")
+        self._units = value
+
     @property
     def units_kind(self) -> str:
         """Units kind."""


Test patch:
diff --git a/tests/data/from_Aramis.py b/tests/data/from_Aramis.py
--- a/tests/data/from_Aramis.py
+++ b/tests/data/from_Aramis.py
@@ -11,6 +11,8 @@
 
 here = pathlib.Path(__file__).parent
 
+wt.units.ureg.define("@alias count = Cnt")
+
 # --- test ----------------------------------------------------------------------------------------
 
 
diff --git a/tests/data/from_PyCMDS.py b/tests/data/from_PyCMDS.py
--- a/tests/data/from_PyCMDS.py
+++ b/tests/data/from_PyCMDS.py
@@ -13,6 +13,7 @@
 
 here = os.path.abspath(os.path.dirname(__file__))
 
+wt.units.ureg.define("steps = []")
 
 # --- test ----------------------------------------------------------------------------------------
 
diff --git a/tests/units.py b/tests/units.py
--- a/tests/units.py
+++ b/tests/units.py
@@ -26,3 +26,27 @@ def test_axis_convert_exception():
 def test_in_mm_conversion():
     assert np.isclose(wt.units.convert(25.4, "mm", "in"), 1.0)
     assert np.isclose(wt.units.convert(1.0, "in", "mm"), 25.4)
+
+
+def unit_registry_test1():
+    values = np.linspace(-1, 1, 51)
+    d = wt.Data(name="test")
+    try:
+        d.create_variable("Bgood", values=values, units="tesla")
+        d.transform("Bgood")
+    except ValueError:
+        assert False
+    else:
+        assert True
+
+
+def unit_registry_test2():
+    values = np.linspace(-1, 1, 51)
+    d = wt.Data(name="test")
+    try:
+        d.create_variable("Bbad", values=values, units="Tesla")
+        d.transform("Bbad")
+    except ValueError:
+        assert True
+    else:
+        assert False