Repository: yt-project/unyt
Pull request #82 (2019-04-02 02:59:44)

Performance improvements
This includes fixes for several performance issues I found integrating unyt with yt.

Big changes are refactoring of parsing into a new `unyt._parsing` private module, which allows me to refactor the `UnitSystem` class to parse unit names and store unit expressions internally, a big performance win since we can avoid repeatedly and expensively parsing unit strings whenever we go through the unit system machinery.

This also includes a small behavior change in the cgs/mks conversion rules to make them slightly more relaxed. This leads to a big performance wins for all unit conversions so I definitely think it's worth it, see the test changes for the extent of the changes.

Last there are a few other more minor or isolated changes that were all clear-cut performance wins and didn't lead to any test changes.

So long as @jzuhone approves of the test changes I'm going to cut another release based on this PR so I can update the yt tests and hopefully substantially improve the performance over there.


Patch:
diff --git a/.travis.yml b/.travis.yml
index e7b81b7..865e8f7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -36,6 +36,6 @@ before_install:
   - pip install tox tox-pyenv codecov twine
 
 # Command to run tests, e.g. python setup.py test
-script: tox -p auto -vvv
+script: tox -vvv
 
 after_success: codecov
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 1aa93f1..b09880b 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -98,7 +98,7 @@ Ready to contribute? Here's how to set up ``unyt`` for local development.
     $ flake8 unyt
     $ black ./
     $ pytest --doctest-modules --doctest-rst --doctest-plus
-    $ tox -p auto
+    $ tox
 
    To get ``flake8``, ``black``, ``pytest``, ``pytest-doctestplus``, and
    ``tox``, just pip install them into your virtualenv.
@@ -127,9 +127,9 @@ run this command. Some tests depend on ``h5py``, ``Pint``, ``astropy``,
 If you would like to run the tests on multiple python versions, first ensure that you have multiple python versions visible on your ``$PATH``, then simply execute ``tox`` in the root of the ``unyt`` repository::
 
    $ cd unyt
-   $ tox -p auto
+   $ tox
 
-The ``tox`` package itself can be installed using the ``pip`` associated with one of the python installations. See the ``tox.ini`` file in the root of the repository for more details about our ``tox`` setup. Note that you do not need to install anything besides ``tox`` and ``python`` for this to work, ``tox`` will handle setting up the test environment, including installing any necessary dependencies via ``pip``. Running ``tox`` with ``-p auto`` will run the tests in parallel and is not required but may make the tests run more quickly.
+The ``tox`` package itself can be installed using the ``pip`` associated with one of the python installations. See the ``tox.ini`` file in the root of the repository for more details about our ``tox`` setup. Note that you do not need to install anything besides ``tox`` and ``python`` for this to work, ``tox`` will handle setting up the test environment, including installing any necessary dependencies via ``pip``.
 
 Pull Request Guidelines
 -----------------------
diff --git a/tox.ini b/tox.ini
index 11ba5f4..6350941 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,6 +10,7 @@ python =
 [testenv]
 setenv =
     PYTHONPATH = {toxinidir}
+depends = begin
 deps =
     pytest
     sympy
@@ -51,6 +52,7 @@ deps =
     coverage
     pytest-cov
     pytest-doctestplus
+depends = begin
 commands =
     # don't do doctests in rst files due to lack of way to specify optional
     # test dependencies there
@@ -60,6 +62,7 @@ commands =
 [testenv:py36-docs]
 whitelist_externals = make
 changedir = docs
+depends =
 deps =
     sphinx
     numpy
@@ -71,6 +74,7 @@ commands =
 [testenv:begin]
 commands =
     coverage erase
+depends =
 skip_install = true
 deps =
     coverage
@@ -80,5 +84,6 @@ commands =
     coverage report --omit='.tox/*'
     coverage html --omit='.tox/*'
 skip_install = true
+depends = py{35,36,37}
 deps =
     coverage
diff --git a/unyt/_parsing.py b/unyt/_parsing.py
new file mode 100644
index 0000000..911b028
--- /dev/null
+++ b/unyt/_parsing.py
@@ -0,0 +1,97 @@
+"""
+parsing utilities
+
+
+
+"""
+
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019, yt Development Team.
+#
+# Distributed under the terms of the Modified BSD License.
+#
+# The full license is in the LICENSE file, distributed with this software.
+# -----------------------------------------------------------------------------
+
+
+from sympy.parsing.sympy_parser import parse_expr, auto_number, rationalize
+from sympy import Basic, Float, Integer, Rational, sqrt, Symbol
+import token
+
+from unyt.exceptions import UnitParseError
+from unyt._unit_lookup_table import inv_name_alternatives
+
+
+def _auto_positive_symbol(tokens, local_dict, global_dict):
+    """
+    Inserts calls to ``Symbol`` for undefined variables.
+    Passes in positive=True as a keyword argument.
+    Adapted from sympy.sympy.parsing.sympy_parser.auto_symbol
+    """
+    result = []
+
+    tokens.append((None, None))  # so zip traverses all tokens
+    for tok, nextTok in zip(tokens, tokens[1:]):
+        tokNum, tokVal = tok
+        nextTokNum, nextTokVal = nextTok
+        if tokNum == token.NAME:
+            name = tokVal
+            if name in global_dict:
+                obj = global_dict[name]
+                if isinstance(obj, (Basic, type)) or callable(obj):
+                    result.append((token.NAME, name))
+                    continue
+
+            # try to resolve known alternative unit name
+            try:
+                used_name = inv_name_alternatives[str(name)]
+            except KeyError:
+                # if we don't know this name it's a user-defined unit name
+                # so we should create a new symbol for it
+                used_name = str(name)
+
+            result.extend(
+                [
+                    (token.NAME, "Symbol"),
+                    (token.OP, "("),
+                    (token.NAME, repr(used_name)),
+                    (token.OP, ","),
+                    (token.NAME, "positive"),
+                    (token.OP, "="),
+                    (token.NAME, "True"),
+                    (token.OP, ")"),
+                ]
+            )
+        else:
+            result.append((tokNum, tokVal))
+
+    return result
+
+
+global_dict = {
+    "Symbol": Symbol,
+    "Integer": Integer,
+    "Float": Float,
+    "Rational": Rational,
+    "sqrt": sqrt,
+}
+
+unit_text_transform = (_auto_positive_symbol, rationalize, auto_number)
+
+
+def parse_unyt_expr(unit_expr):
+    if not unit_expr:
+        # Bug catch...
+        # if unit_expr is an empty string, parse_expr fails hard...
+        unit_expr = "1"
+    try:
+        unit_expr = parse_expr(
+            unit_expr, global_dict=global_dict, transformations=unit_text_transform
+        )
+    except Exception as e:
+        msg = "Unit expression '%s' raised an error during parsing:\n%s" % (
+            unit_expr,
+            repr(e),
+        )
+        raise UnitParseError(msg)
+    return unit_expr
diff --git a/unyt/array.py b/unyt/array.py
index 8aefad3..4459e55 100644
--- a/unyt/array.py
+++ b/unyt/array.py
@@ -14,7 +14,6 @@ unyt_array class.
 # -----------------------------------------------------------------------------
 
 import copy
-from functools import wraps
 
 from functools import lru_cache
 from numbers import Number as numeric_type
@@ -116,7 +115,6 @@ from unyt.exceptions import (
     InvalidUnitEquivalence,
     InvalidUnitOperation,
     MKSCGSConversionError,
-    UnitConversionError,
     UnitOperationError,
     UnitsNotReducible,
 )
@@ -143,19 +141,6 @@ def _iterable(obj):
     return True
 
 
-def _return_arr(func):
-    @wraps(func)
-    def wrapped(*args, **kwargs):
-        ret, units = func(*args, **kwargs)
-        if ret.shape == ():
-            return unyt_quantity(ret, units)
-        else:
-            # This could be a subclass, so don't call unyt_array directly.
-            return type(args[0])(ret, units)
-
-    return wrapped
-
-
 @lru_cache(maxsize=128, typed=False)
 def _sqrt_unit(unit):
     return 1, unit ** 0.5
@@ -598,14 +583,9 @@ class unyt_array(np.ndarray):
         """
         units = _sanitize_units_convert(units, self.units.registry)
         if equivalence is None:
-            try:
-                conv_data = _check_em_conversion(
-                    self.units, units, registry=self.units.registry
-                )
-            except MKSCGSConversionError:
-                raise UnitConversionError(
-                    self.units, self.units.dimensions, units, units.dimensions
-                )
+            conv_data = _check_em_conversion(
+                self.units, units, registry=self.units.registry
+            )
             if any(conv_data):
                 new_units, (conv_factor, offset) = _em_conversion(
                     self.units, conv_data, units
@@ -793,14 +773,9 @@ class unyt_array(np.ndarray):
         """
         units = _sanitize_units_convert(units, self.units.registry)
         if equivalence is None:
-            try:
-                conv_data = _check_em_conversion(
-                    self.units, units, registry=self.units.registry
-                )
-            except MKSCGSConversionError:
-                raise UnitConversionError(
-                    self.units, self.units.dimensions, units, units.dimensions
-                )
+            conv_data = _check_em_conversion(
+                self.units, units, registry=self.units.registry
+            )
             if any(conv_data):
                 new_units, (conversion_factor, offset) = _em_conversion(
                     self.units, conv_data, units
@@ -944,7 +919,7 @@ class unyt_array(np.ndarray):
                 self.units, unit_system=us, registry=self.units.registry
             )
         except MKSCGSConversionError:
-            raise UnitsNotReducible(self.units, unit_system)
+            raise UnitsNotReducible(self.units, us)
         if any(conv_data):
             to_units, (conv, offset) = _em_conversion(
                 self.units, conv_data, unit_system=us
diff --git a/unyt/exceptions.py b/unyt/exceptions.py
index bcf50a1..13f3e3f 100644
--- a/unyt/exceptions.py
+++ b/unyt/exceptions.py
@@ -121,12 +121,7 @@ class MKSCGSConversionError(Exception):
     UnitConversionError.
     """
 
-    def __init__(self, unit):
-        self.unit = unit
-
-    def __str__(self):
-        err = "The '%s' unit cannot be safely converted." % self.unit
-        return err
+    pass
 
 
 class UnitsNotReducible(Exception):
diff --git a/unyt/unit_object.py b/unyt/unit_object.py
index 400d53f..e3a55e7 100644
--- a/unyt/unit_object.py
+++ b/unyt/unit_object.py
@@ -16,12 +16,9 @@ A class that represents a unit symbol.
 import copy
 import itertools
 import math
-
-from functools import lru_cache
-from keyword import iskeyword as _iskeyword
 import numpy as np
+from functools import lru_cache
 from numbers import Number as numeric_type
-import token
 
 from sympy import (
     Expr,
@@ -30,17 +27,14 @@ from sympy import (
     Number,
     Pow,
     Symbol,
-    Integer,
     Float,
     Basic,
     Rational,
-    sqrt,
     Mod,
     floor,
 )
 from sympy.core.numbers import One
 from sympy import sympify, latex
-from sympy.parsing.sympy_parser import parse_expr, auto_number, rationalize
 
 from unyt.dimensions import (
     angle,
@@ -58,93 +52,15 @@ from unyt.exceptions import (
     UnitConversionError,
     UnitDtypeError,
     UnitsNotReducible,
-)
-from unyt._physical_ratios import speed_of_light_cm_per_s
-from unyt._unit_lookup_table import inv_name_alternatives
-from unyt.unit_registry import (
-    default_unit_registry,
-    _lookup_unit_symbol,
-    UnitRegistry,
     UnitParseError,
 )
+from unyt._parsing import parse_unyt_expr
+from unyt._physical_ratios import speed_of_light_cm_per_s
+from unyt.unit_registry import default_unit_registry, _lookup_unit_symbol, UnitRegistry
 from unyt.unit_systems import _split_prefix
 
 sympy_one = sympify(1)
 
-global_dict = {
-    "Symbol": Symbol,
-    "Integer": Integer,
-    "Float": Float,
-    "Rational": Rational,
-    "sqrt": sqrt,
-}
-
-
-def _auto_positive_symbol(tokens, local_dict, global_dict):
-    """
-    Inserts calls to ``Symbol`` for undefined variables.
-    Passes in positive=True as a keyword argument.
-    Adapted from sympy.sympy.parsing.sympy_parser.auto_symbol
-    """
-    result = []
-    prevTok = (None, None)
-
-    tokens.append((None, None))  # so zip traverses all tokens
-    for tok, nextTok in zip(tokens, tokens[1:]):
-        tokNum, tokVal = tok
-        nextTokNum, nextTokVal = nextTok
-        if tokNum == token.NAME:
-            name = tokVal
-            if (
-                name in ["True", "False", "None"]
-                # special case 'as' for attosecond
-                or (_iskeyword(name) and name != "as")
-                or name in local_dict
-                # Don't convert attribute access
-                or (prevTok[0] == token.OP and prevTok[1] == ".")
-                # Don't convert keyword arguments
-                or (
-                    prevTok[0] == token.OP
-                    and prevTok[1] in ("(", ",")
-                    and nextTokNum == token.OP
-                    and nextTokVal == "="
-                )
-            ):
-                result.append((token.NAME, name))
-                continue
-            elif name in global_dict:
-                obj = global_dict[name]
-                if isinstance(obj, (Basic, type)) or callable(obj):
-                    result.append((token.NAME, name))
-                    continue
-
-            # try to resolve known alternative unit name
-            try:
-                used_name = inv_name_alternatives[str(name)]
-            except KeyError:
-                # if we don't know this name it's a user-defined unit name
-                # so we should create a new symbol for it
-                used_name = str(name)
-
-            result.extend(
-                [
-                    (token.NAME, "Symbol"),
-                    (token.OP, "("),
-                    (token.NAME, repr(used_name)),
-                    (token.OP, ","),
-                    (token.NAME, "positive"),
-                    (token.OP, "="),
-                    (token.NAME, "True"),
-                    (token.OP, ")"),
-                ]
-            )
-        else:
-            result.append((tokNum, tokVal))
-
-        prevTok = (tokNum, tokVal)
-
-    return result
-
 
 def _get_latex_representation(expr, registry):
     symbol_table = {}
@@ -194,9 +110,6 @@ def _get_latex_representation(expr, registry):
         return latex_repr
 
 
-unit_text_transform = (_auto_positive_symbol, rationalize, auto_number)
-
-
 class Unit(object):
     """
     A symbolic unit, using sympy functionality. We only add "dimensions" so
@@ -257,22 +170,7 @@ class Unit(object):
             if registry and unit_expr in registry._unit_object_cache:
                 return registry._unit_object_cache[unit_expr]
             unit_cache_key = unit_expr
-            if not unit_expr:
-                # Bug catch...
-                # if unit_expr is an empty string, parse_expr fails hard...
-                unit_expr = "1"
-            try:
-                unit_expr = parse_expr(
-                    unit_expr,
-                    global_dict=global_dict,
-                    transformations=unit_text_transform,
-                )
-            except SyntaxError as e:
-                msg = "Unit expression '%s' raised an error during parsing:\n%s" % (
-                    unit_expr,
-                    repr(e),
-                )
-                raise UnitParseError(msg)
+            unit_expr = parse_unyt_expr(unit_expr)
         # Simplest case. If user passes a Unit object, just use the expr.
         elif isinstance(unit_expr, Unit):
             # grab the unit object's sympy expression.
@@ -296,10 +194,7 @@ class Unit(object):
                 "sympy Expr. '%s' has type '%s'." % (unit_expr, type(unit_expr))
             )
 
-        # this is slightly faster if unit_expr is the same object as
-        # sympy_one than just checking for == equality
-        is_one = unit_expr is sympy_one or unit_expr == sympy_one
-        if dimensions is None and is_one:
+        if dimensions is None and unit_expr is sympy_one:
             dimensions = dimensionless
 
         if registry is None:
@@ -687,7 +582,10 @@ class Unit(object):
         if any(conv_data):
             new_units, _ = _em_conversion(self, conv_data, unit_system=unit_system)
         else:
-            new_units = unit_system[self.dimensions]
+            try:
+                new_units = unit_system[self.dimensions]
+            except MissingMKSCurrent:
+                raise UnitsNotReducible(self.units, unit_system)
         return Unit(new_units, registry=self.registry)
 
     def get_cgs_equivalent(self):
@@ -882,6 +780,8 @@ em_conversions = {
     ),
 }
 
+em_conversion_dims = [k[1] for k in em_conversions.keys()]
+
 
 def _em_conversion(orig_units, conv_data, to_units=None, unit_system=None):
     """Convert between E&M & MKS base units.
@@ -895,14 +795,11 @@ def _em_conversion(orig_units, conv_data, to_units=None, unit_system=None):
     conv_unit, canonical_unit, scale = conv_data
     if conv_unit is None:
         conv_unit = canonical_unit
-    new_expr = orig_units.copy().expr.replace(
-        orig_units.expr, scale * canonical_unit.expr
-    )
+    new_expr = scale * canonical_unit.expr
     if unit_system is not None:
         # we don't know the to_units, so we get it directly from the
         # conv_data
-        inter_expr = orig_units.copy().expr.replace(orig_units.expr, conv_unit.expr)
-        to_units = Unit(inter_expr, registry=orig_units.registry)
+        to_units = Unit(conv_unit.expr, registry=orig_units.registry)
     new_units = Unit(new_expr, registry=orig_units.registry)
     conv = new_units.get_conversion_factor(to_units)
     return to_units, conv
@@ -923,7 +820,7 @@ def _check_em_conversion(unit, to_unit=None, unit_system=None, registry=None):
     trying to convert between CGS & MKS E&M units, it raises an error.
     """
     em_map = ()
-    if unit == to_unit:
+    if unit == to_unit or unit.dimensions not in em_conversion_dims:
         return em_map
     if unit.is_atomic:
         prefix, unit_wo_prefix = _split_prefix(str(unit), unit.registry.lut)
@@ -945,12 +842,9 @@ def _check_em_conversion(unit, to_unit=None, unit_system=None, registry=None):
     if em_map:
         return em_map
     if unit_system is None:
-        if to_unit.dimensions != unit.dimensions:
-            raise MKSCGSConversionError(unit)
-        else:
-            from unyt.unit_systems import unit_system_registry
+        from unyt.unit_systems import unit_system_registry
 
-            unit_system = unit_system_registry["mks"]
+        unit_system = unit_system_registry["mks"]
     for unit_atom in unit.expr.atoms():
         if unit_atom.is_Number:
             continue
@@ -961,15 +855,6 @@ def _check_em_conversion(unit, to_unit=None, unit_system=None, registry=None):
                 continue
         except MissingMKSCurrent:
             raise MKSCGSConversionError(unit)
-        if (bu, budims) in em_conversions:
-            conv_unit = em_conversions[bu, budims][1]
-            if to_unit is not None:
-                for to_unit_atom in to_unit.expr.atoms():
-                    bou = str(to_unit_atom)
-                    if bou == conv_unit:
-                        raise MKSCGSConversionError(unit)
-            else:
-                raise MKSCGSConversionError(unit)
     return em_map
 
 
@@ -1033,7 +918,7 @@ def _get_unit_data_from_expr(unit_expr, unit_symbol_lut):
         return (float(unit_expr), sympy_one)
 
     if isinstance(unit_expr, Symbol):
-        return _lookup_unit_symbol(str(unit_expr), unit_symbol_lut)
+        return _lookup_unit_symbol(unit_expr.name, unit_symbol_lut)
 
     if isinstance(unit_expr, Pow):
         unit_data = _get_unit_data_from_expr(unit_expr.args[0], unit_symbol_lut)
diff --git a/unyt/unit_symbols.py b/unyt/unit_symbols.py
index 9db4073..0355d73 100644
--- a/unyt/unit_symbols.py
+++ b/unyt/unit_symbols.py
@@ -22,7 +22,12 @@ For example::
 # The full license is in the LICENSE file, distributed with this software.
 # -----------------------------------------------------------------------------
 
-from unyt.unit_registry import default_unit_registry as _default_unit_registry
-from unyt.unit_systems import add_symbols as _add_symbols
+from unyt.unit_registry import default_unit_registry as _registry
+from unyt.unit_object import Unit as _Unit
+from unyt._unit_lookup_table import name_alternatives as _name_alternatives
 
-_add_symbols(globals(), registry=_default_unit_registry)
+_namespace = globals()
+
+for _canonical_name, _alt_names in _name_alternatives.items():
+    for _alt_name in _alt_names:
+        _namespace[_alt_name] = _Unit(_canonical_name, registry=_registry)
diff --git a/unyt/unit_systems.py b/unyt/unit_systems.py
index e235600..bac8a08 100644
--- a/unyt/unit_systems.py
+++ b/unyt/unit_systems.py
@@ -14,10 +14,10 @@ Unit system class.
 from collections import OrderedDict
 from unyt import dimensions
 from unyt.exceptions import MissingMKSCurrent, IllDefinedUnitSystem, UnitsNotReducible
+from unyt._parsing import parse_unyt_expr
 from unyt._unit_lookup_table import (
     default_unit_symbol_lut as default_lut,
     inv_name_alternatives,
-    name_alternatives,
     physical_constants,
     unit_prefixes,
 )
@@ -50,14 +50,15 @@ def add_symbols(namespace, registry):
     >>> foo.joule
     J
     """
+    import unyt.unit_symbols as us
     from unyt.unit_object import Unit
 
-    for canonical_name, alt_names in name_alternatives.items():
-        for alt_name in alt_names:
-            namespace[alt_name] = Unit(canonical_name, registry=registry)
-    for name in registry.keys():
-        if name not in namespace:
-            namespace[name] = Unit(name, registry=registry)
+    for name, unit in vars(us).items():
+        if name.startswith("_"):
+            continue
+        namespace[name] = Unit(unit.expr, registry=registry)
+    for name in [k for k in registry.keys() if k not in namespace]:
+        namespace[name] = Unit(name, registry=registry)
 
 
 def add_constants(namespace, registry):
@@ -125,9 +126,9 @@ def _split_prefix(symbol_str, unit_symbol_lut):
             symbol_wo_pref = symbol_str[2:]
             possible_prefix = "da"
 
-        prefixable_units = [u for u in unit_symbol_lut if unit_symbol_lut[u][4]]
+        entry = unit_symbol_lut.get(symbol_wo_pref, None)
 
-        if symbol_wo_pref in unit_symbol_lut and symbol_wo_pref in prefixable_units:
+        if entry and entry[4]:
             return possible_prefix, symbol_wo_pref
     return "", symbol_str
 
@@ -211,18 +212,21 @@ class UnitSystem(object):
                 (dimensions.luminous_intensity, luminous_intensity_unit),
             ]
         )
+        for k, v in self.units_map.items():
+            if v is not None:
+                self.units_map[k] = parse_unyt_expr(str(v))
         for dimension, unit in self.units_map.items():
             # CGS sets the current_mks unit to none, so catch it here
             if unit is None and dimension is dimensions.current_mks:
                 continue
-            if self.registry is not None and self.registry[unit][1] is not dimension:
+            if (
+                self.registry is not None
+                and self.registry[str(unit)][1] is not dimension
+            ):
                 raise IllDefinedUnitSystem(self.units_map)
             elif self.registry is None:
-                if isinstance(unit, str):
-                    bu = _split_prefix(unit, default_lut)[1]
-                    inferred_dimension = default_lut[inv_name_alternatives[bu]][1]
-                else:
-                    inferred_dimension = getattr(unit, "dimensions", None)
+                bu = _split_prefix(str(unit), default_lut)[1]
+                inferred_dimension = default_lut[inv_name_alternatives[bu]][1]
                 if inferred_dimension is not dimension:
                     raise IllDefinedUnitSystem(self.units_map)
         self._dims = [
@@ -249,7 +253,7 @@ class UnitSystem(object):
             if cmks in key.free_symbols and self.units_map[cmks] is None:
                 raise MissingMKSCurrent(self.name)
             units = _get_system_unit_string(key, self.units_map)
-            self.units_map[key] = units
+            self.units_map[key] = parse_unyt_expr(units)
             return Unit(units, registry=self.registry)
         return Unit(self.units_map[key], registry=self.registry)
 
@@ -260,7 +264,7 @@ class UnitSystem(object):
             key = getattr(dimensions, key)
         if self.units_map[cmks] is None and cmks in key.free_symbols:
             raise MissingMKSCurrent(self.name)
-        self.units_map[key] = value
+        self.units_map[key] = parse_unyt_expr(str(value))
 
     def __str__(self):
         return self.name


Test patch:
diff --git a/unyt/tests/test_units.py b/unyt/tests/test_units.py
index db072bb..70bd184 100644
--- a/unyt/tests/test_units.py
+++ b/unyt/tests/test_units.py
@@ -44,7 +44,11 @@ from unyt.dimensions import (
 from unyt.exceptions import InvalidUnitOperation, UnitsNotReducible, UnitConversionError
 from unyt.unit_object import default_unit_registry, Unit, UnitParseError
 from unyt.unit_systems import cgs_unit_system, UnitSystem
-from unyt._unit_lookup_table import default_unit_symbol_lut, unit_prefixes
+from unyt._unit_lookup_table import (
+    default_unit_symbol_lut,
+    name_alternatives,
+    unit_prefixes,
+)
 import unyt.unit_symbols as unit_symbols
 from unyt._physical_ratios import (
     m_per_pc,
@@ -149,6 +153,14 @@ def test_create_from_string():
         Unit("m-g")
     with pytest.raises(UnitParseError):
         Unit("hello!")
+    with pytest.raises(UnitParseError):
+        Unit("True")
+    with pytest.raises(UnitParseError):
+        Unit("else")
+    with pytest.raises(UnitParseError):
+        Unit("hello(37)")
+    with pytest.raises(UnitParseError):
+        Unit("hello(foo=37)")
 
     cm = Unit("cm")
     data = 1 * cm
@@ -403,6 +415,7 @@ def test_equality():
     u2 = Unit("m * ms**-1")
 
     assert u1 == u2
+    assert u1.copy() == u2
 
 
 def test_invalid_operations():
@@ -748,3 +761,13 @@ def test_micro():
 
     assert str(Unit("um")) == "µm"
     assert str(Unit("us")) == "µs"
+
+
+def test_show_all_units_doc_table_ops():
+    for name in set(name_alternatives.keys()):
+        u = Unit(name)
+        (1 * u).in_mks()
+        try:
+            (1 * u).in_cgs()
+        except UnitsNotReducible:
+            pass
diff --git a/unyt/tests/test_unyt_array.py b/unyt/tests/test_unyt_array.py
index 8fc775f..52ed60c 100644
--- a/unyt/tests/test_unyt_array.py
+++ b/unyt/tests/test_unyt_array.py
@@ -1859,13 +1859,10 @@ def test_electromagnetic():
     u_mks = B * B / (2 * u.mu_0)
     assert_equal(u_mks.units.dimensions, dimensions.pressure)
     u_cgs = B_cgs * B_cgs / (8 * np.pi)
-    with pytest.raises(UnitConversionError):
-        u_cgs.to(u_mks.units)
+    assert_equal(u_mks, u_cgs.to(u_mks.units))
     assert_equal(u_mks.to(u_cgs.units), u_cgs)
-    with pytest.raises(UnitsNotReducible):
-        u_mks.in_cgs()
-    with pytest.raises(UnitsNotReducible):
-        u_cgs.in_mks()
+    assert_equal(u_mks.in_cgs(), u_cgs)
+    assert_equal(u_cgs.in_mks(), u_mks)
 
     current = 1.0 * u.A
     I_cgs = current.in_units("statA")
@@ -1891,10 +1888,10 @@ def test_electromagnetic():
     P_cgs = I_cgs * I_cgs * R_cgs
     assert_equal(P_mks.units.dimensions, dimensions.power)
     assert_equal(P_cgs.units.dimensions, dimensions.power)
-    with pytest.raises(UnitsNotReducible):
-        P_cgs.in_cgs()
-    with pytest.raises(UnitsNotReducible):
-        P_mks.in_cgs()
+    assert_almost_equal(P_cgs.in_cgs(), P_cgs)
+    assert_almost_equal(P_mks.in_cgs(), P_cgs)
+    assert_almost_equal(P_cgs.in_mks(), P_mks)
+    assert_almost_equal(P_mks.in_mks(), P_mks)
 
     V = unyt_quantity(1.0, "statV")
     V_mks = V.in_units("V")
@@ -1913,8 +1910,7 @@ def test_electromagnetic():
         data.to("C*T*V")
     with pytest.raises(UnitConversionError):
         data.convert_to_units("C*T*V")
-    with pytest.raises(UnitsNotReducible):
-        data.in_mks()
+    assert_equal(data.in_mks(), 6.67384e-18 * u.m ** 5 / u.s ** 4)
 
     mu_0 = 4.0e-7 * math.pi * u.N / u.A ** 2
     eps_0 = 8.85418781782e-12 * u.m ** -3 / u.kg * u.s ** 4 * u.A ** 2