diff --git a/changelog/47.docs.md b/changelog/47.docs.md new file mode 100644 index 0000000..a64b853 --- /dev/null +++ b/changelog/47.docs.md @@ -0,0 +1 @@ +Move docs on design decisions out of `src/openscm_units/_unit_registry.py` into a dedicated notebook diff --git a/changelog/47.trivial.md b/changelog/47.trivial.md new file mode 100644 index 0000000..b2365f2 --- /dev/null +++ b/changelog/47.trivial.md @@ -0,0 +1,3 @@ +Move `unit_registry` from `src/openscm_units/_unit_registry.py` into `openscm_units/__init__.py`. +This seems to be necessary to make the docs appear correctly without hacks (see [this issue](https://github.com/sphinx-doc/sphinx/issues/8547)). +This does not affect users of the public API hence is a trivial change. diff --git a/docs/source/api/openscm_units._unit_registry.rst b/docs/source/api/openscm_units._unit_registry.rst deleted file mode 100644 index 18ae6a2..0000000 --- a/docs/source/api/openscm_units._unit_registry.rst +++ /dev/null @@ -1,20 +0,0 @@ -openscm\_units.\_unit\_registry -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: openscm_units._unit_registry - -.. currentmodule:: openscm_units._unit_registry - - - -unit\_registry -============== - -.. autodata:: unit_registry - - -ScmUnitRegistry -=============== - -.. autoclass:: ScmUnitRegistry - :members: diff --git a/docs/source/api/openscm_units.rst b/docs/source/api/openscm_units.rst index de948c7..498e1bf 100644 --- a/docs/source/api/openscm_units.rst +++ b/docs/source/api/openscm_units.rst @@ -9,5 +9,16 @@ API Reference .. autosummary:: :toctree: ./ - openscm_units._unit_registry openscm_units.data + +unit\_registry +============== + +.. autodata:: unit_registry + + +ScmUnitRegistry +=============== + +.. autoclass:: ScmUnitRegistry + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 7b07057..3d4ccef 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,9 @@ project = "OpenSCM-Units" # put the authors in their own variable, so they can be reused later -authors = ", ".join(["Zeb Nicholls", "Sven Willner", "Jared Lewis", "Robert Gieseke"]) +authors = ", ".join( + ["Zeb Nicholls", "Jared Lewis", "Mika Pflueger", "Robert Gieseke", "Sven Willner"] +) # add a copyright year variable, we can extend this over time in future as # needed copyright_year = "2020 - 2023" @@ -72,6 +74,7 @@ autodoc_default_options = { # Show the inheritance of classes "show-inheritance": True, + "imported-members": True, } # autosummary with autodocgen @@ -87,8 +90,6 @@ "module_title_decider": lambda modulename: "API Reference" if modulename == "openscm_units" else modulename, - # Include private docs too - "skip_module_regex": "", } ] diff --git a/docs/source/notebooks.md b/docs/source/notebooks.md index a5effb9..af3df83 100644 --- a/docs/source/notebooks.md +++ b/docs/source/notebooks.md @@ -1,8 +1,8 @@ (notebooks-reference)= # Notebooks -Here we provide various examples of how to use OpenSCM-Units. -They are derived from +Here we provide further documentation related to OpenSCM-Units. +The docs are derived from [jupyter notebooks](https://docs.jupyter.org/en/latest/start/index.html), but are saved using [jupytext](https://jupytext.readthedocs.io/en/latest/) to keep our repository slim and make it easier to track changes. @@ -16,6 +16,14 @@ notebooks/basic-demo.py notebooks/custom-conversions.py ``` +## For developers + +```{toctree} +:caption: Contents +:maxdepth: 1 +notebooks/design-principles.py +``` + ## Notebook execution info ```{nb-exec-table} diff --git a/docs/source/notebooks/custom-conversions.py b/docs/source/notebooks/custom-conversions.py index a8e7465..bae4790 100644 --- a/docs/source/notebooks/custom-conversions.py +++ b/docs/source/notebooks/custom-conversions.py @@ -57,10 +57,12 @@ unit_registry.add_standards() # start with e.g. N2O -nitrous_oxide = unit_registry("N2O") -print(f"N2O: {nitrous_oxide}") +nitrous_oxide = unit_registry("tN2O / yr") # our unit registry allows us to make conversions using the # conversion factors we previously defined -with unit_registry.context("Custom1"): - print(f"N2O in CO2-equivalent: {nitrous_oxide.to('CO2')}") +for context in ["Custom1", "Custom2"]: + with unit_registry.context(context): + print( + f"{nitrous_oxide} in CO2-equivalent in context {context} is {nitrous_oxide.to('tCO2 / yr')}" + ) diff --git a/docs/source/notebooks/design-principles.py b/docs/source/notebooks/design-principles.py new file mode 100644 index 0000000..ba66ed0 --- /dev/null +++ b/docs/source/notebooks/design-principles.py @@ -0,0 +1,178 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Design principles +# +# Here we provide an overview of the key design principles and choices in OpenSCM-Units. + +# %% [markdown] +# Unit handling makes use of the [Pint](https://github.com/hgrecco/pint) library. This +# allows us to easily define units as well as contexts. Contexts allow us to perform +# conversions which would not normally be allowed e.g. in the 'AR4GWP100' +# context we can convert from CO2 to CH4 using the AR4GWP100 equivalence metric. +# +# An illustration of how the ``unit_registry`` can be used is shown below + +# %% +import traceback + +from pint.errors import DimensionalityError + +from openscm_units import unit_registry + +# %% +unit_registry("CO2") + +# %% +emissions_aus = 0.34 * unit_registry("Gt C / yr") +emissions_aus + +# %% +emissions_aus.to("Mt CO2 / yr") + +# %% +with unit_registry.context("AR4GWP100"): + print((100 * unit_registry("Mt CH4 / yr")).to("Mt CO2 / yr")) + +# %% [markdown] +# ## More details on emissions unit +# +# Emissions are a flux composed of three parts: mass, the species being emitted and the +# time period e.g. "t CO2 / yr". As mass and time are part of SI units, all we need to +# define in OpenSCM-Units are emissions units i.e. the stuff. Here we include as many of the canonical +# emissions units, and their conversions, as possible. +# +# For emissions units, there are a few cases to be considered: +# +# - fairly obvious ones e.g. carbon dioxide emissions can be provided in 'C' or 'CO2' and +# converting between the two is possible +# - less obvious ones e.g. NOx emissions can be provided in 'N' or 'NOx', we provide +# conversions between these two which can be enabled if needed (see below). +# - case-sensitivity. In order to provide a simplified interface, using all uppercase +# versions of any unit is also valid e.g. `unit_registry("HFC4310mee")` is the same as +# `unit_registry("HFC4310MEE")` +# - hyphens and underscores in units. In order to be Pint compatible and to simplify +# things, we strip all hyphens and underscores from units. +# +# As a convenience, we allow users to combine the mass and the type of emissions to make a +# 'joint unit' e.g. "tCO2". It should be recognised that this joint unit is a derived +# unit and not a base unit. +# +# By defining these three separate components, it is much easier to track what conversions +# are valid and which are not. For example, as the emissions units are all defined as +# emissions units, and not as atomic masses, we are able to prevent invalid conversions. +# If emissions units were simply atomic masses, it would be possible to convert between +# e.g. C and N2O which would be a problem. Conventions such as allowing carbon dioxide +# emissions to be reported in C or CO2, despite the fact that they are fundamentally +# different chemical species, is a convention which is particular to emissions (as far as +# we can tell). +# +# Pint's contexts are particularly useful for emissions as they facilitate +# metric conversions. With a context, a conversion which wouldn't normally be allowed +# (e.g. tCO2 --> tN2O) is allowed and will use whatever metric conversion is appropriate +# for that context (e.g. AR4GWP100). + +# %% [markdown] +# ## Namespace collisions +# +# Finally, we discuss namespace collisions. + +# %% [markdown] +# ### CH$_4$ +# +# Methane emissions are defined as 'CH4'. In order to prevent inadvertent conversions of +# 'CH4' to e.g. 'CO2' via 'C', the conversion 'CH4' <--> 'C' is by default forbidden. +# However, it can be performed within the context 'CH4_conversions' as shown below: + +# %% +try: + unit_registry("CH4").to("C") +except DimensionalityError: + traceback.print_exc(limit=0, chain=False) + +# %% [markdown] +# With a context, the conversion becomes legal again + +# %% +with unit_registry.context("CH4_conversions"): + print(unit_registry("CH4").to("C")) + +# %% [markdown] +# As an unavoidable side effect, this also becomes possible + +# %% +with unit_registry.context("CH4_conversions"): + print(unit_registry("CH4").to("CO2")) + +# %% [markdown] +# ### N$_2$O +# +# Nitrous oxide emissions are typically reported with units of 'N2O'. However, +# they are also reported with units of 'N2ON' (a short-hand which indicates that +# only the mass of the nitrogen is being counted). Reporting nitrous oxide +# emissions with units of simply 'N' is ambiguous (do you mean the mass of +# nitrogen, so 1 N = 28 / 44 N2O or just the mass of a single N atom, so +# 1 N = 14 / 44 N2O). By default, converting 'N2O' <--> 'N' is forbidden to +# prevent this ambiguity. However, the conversion can be performed within the +# context 'N2O_conversions', in which case it is assumed that 'N' just means a +# single N atom i.e. 1 N = 14 / 44 N2O, as shown below: + +# %% +try: + unit_registry("N2O").to("N") +except DimensionalityError: + traceback.print_exc(limit=0, chain=False) + +# %% [markdown] +# With a context, the conversion becomes legal again: + +# %% +with unit_registry.context("N2O_conversions"): + print(unit_registry("N2O").to("N")) + +# %% [markdown] +# ### NO$_x$ + +# %% [markdown] +# Like for methane, NOx emissions also suffer from a namespace collision. In order to +# prevent inadvertent conversions from 'NOx' to e.g. 'N2O', the conversion 'NOx' <--> +# 'N' is by default forbidden. It can be performed within the 'NOx_conversions' context: + +# %% +try: + unit_registry("NOx").to("N") +except DimensionalityError: + traceback.print_exc(limit=0, chain=False) + +# %% +with unit_registry.context("NOx_conversions"): + print(unit_registry("NOx").to("N")) + +# %% [markdown] +# ### NH$_3$ +# +# In order to prevent inadvertent conversions from 'NH3' to 'CO2', the conversion +# 'NH3' <--> 'N' is by default forbidden. It can be performed within the 'NH3_conversions' +# context analogous to the 'NOx_conversions' context: + +# %% +try: + unit_registry("NH3").to("N") +except DimensionalityError: + traceback.print_exc(limit=0, chain=False) + +# %% +with unit_registry.context("NH3_conversions"): + unit_registry("NH3").to("N") diff --git a/pyproject.toml b/pyproject.toml index f131856..affc875 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Handling of units related to simple climate modelling." authors = [ "Zebedee Nicholls ", "Jared Lewis ", + "Mika Pflueger ", "Robert Gieseke ", "Sven Willner ", ] diff --git a/src/openscm_units/__init__.py b/src/openscm_units/__init__.py index 6165b29..d7b2f5c 100644 --- a/src/openscm_units/__init__.py +++ b/src/openscm_units/__init__.py @@ -3,7 +3,7 @@ """ import importlib.metadata -from ._unit_registry import ScmUnitRegistry, unit_registry +from ._unit_registry import ScmUnitRegistry __version__ = importlib.metadata.version("openscm_units") @@ -11,3 +11,15 @@ "ScmUnitRegistry", "unit_registry", ] + + +unit_registry = ScmUnitRegistry() +""" +Standard unit registry + +The unit registry contains all of the recognised units. Be careful, if you +edit this registry in one place then it will also be edited in any other +places that use :mod:`openscm_units`. If you want multiple, separate registries, +create multiple instances of :class:`ScmUnitRegistry`. +""" +unit_registry.add_standards() diff --git a/src/openscm_units/_unit_registry.py b/src/openscm_units/_unit_registry.py index 5245261..1a6b820 100644 --- a/src/openscm_units/_unit_registry.py +++ b/src/openscm_units/_unit_registry.py @@ -1,162 +1,7 @@ """ Definition of our unit registry -Unit handling makes use of the `Pint `_ library. This -allows us to easily define units as well as contexts. Contexts allow us to perform -conversions which would not normally be allowed e.g. in the 'AR4GWP100' -context we can convert from CO2 to CH4 using the AR4GWP100 equivalence metric. - -An illustration of how the ``unit_registry`` can be used is shown below: - -.. code:: python - - >>> from openscm_units import unit_registry - >>> unit_registry("CO2") - - - >>> emissions_aus = 0.34 * unit_registry("Gt C / yr") - >>> emissions_aus - - - >>> emissions_aus.to("Mt CO2 / yr") - - - >>> with unit_registry.context("AR4GWP100"): - ... (100 * unit_registry("Mt CH4 / yr")).to("Mt CO2 / yr") - ... - - -**More details on emissions units** - -Emissions are a flux composed of three parts: mass, the species being emitted and the -time period e.g. "t CO2 / yr". As mass and time are part of SI units, all we need to -define here are emissions units i.e. the stuff. Here we include as many of the canonical -emissions units, and their conversions, as possible. - -For emissions units, there are a few cases to be considered: - -- fairly obvious ones e.g. carbon dioxide emissions can be provided in 'C' or 'CO2' and - converting between the two is possible -- less obvious ones e.g. NOx emissions can be provided in 'N' or 'NOx', we provide - conversions between these two which can be enabled if needed (see below). -- case-sensitivity. In order to provide a simplified interface, using all uppercase - versions of any unit is also valid e.g. ``unit_registry("HFC4310mee")`` is the same as - ``unit_registry("HFC4310MEE")`` -- hyphens and underscores in units. In order to be Pint compatible and to simplify - things, we strip all hyphens and underscores from units. - -As a convenience, we allow users to combine the mass and the type of emissions to make a -'joint unit' e.g. "tCO2". It should be recognised that this joint unit is a derived -unit and not a base unit. - -By defining these three separate components, it is much easier to track what conversions -are valid and which are not. For example, as the emissions units are all defined as -emissions units, and not as atomic masses, we are able to prevent invalid conversions. -If emissions units were simply atomic masses, it would be possible to convert between -e.g. C and N2O which would be a problem. Conventions such as allowing carbon dioxide -emissions to be reported in C or CO2, despite the fact that they are fundamentally -different chemical species, is a convention which is particular to emissions (as far as -we can tell). - -Pint's contexts are particularly useful for emissions as they facilitate -metric conversions. With a context, a conversion which wouldn't normally be allowed -(e.g. tCO2 --> tN2O) is allowed and will use whatever metric conversion is appropriate -for that context (e.g. AR4GWP100). - -Finally, we discuss namespace collisions. - -*CH4* - -Methane emissions are defined as 'CH4'. In order to prevent inadvertent conversions of -'CH4' to e.g. 'CO2' via 'C', the conversion 'CH4' <--> 'C' is by default forbidden. -However, it can be performed within the context 'CH4_conversions' as shown below: - -.. code:: python - - >>> from openscm_units import unit_registry - >>> unit_registry("CH4").to("C") - Traceback (most recent call last): - ... - pint.errors.DimensionalityError: Cannot convert from 'CH4' ([methane]) to 'C' ([carbon]) - - # with a context, the conversion becomes legal again - >>> with unit_registry.context("CH4_conversions"): - ... unit_registry("CH4").to("C") - ... - - - # as an unavoidable side effect, this also becomes possible - >>> with unit_registry.context("CH4_conversions"): - ... unit_registry("CH4").to("CO2") - ... - - -*N2O* - -Nitrous oxide emissions are typically reported with units of 'N2O'. However, -they are also reported with units of 'N2ON' (a short-hand which indicates that -only the mass of the nitrogen is being counted). Reporting nitrous oxide -emissions with units of simply 'N' is ambiguous (do you mean the mass of -nitrogen, so 1 N = 28 / 44 N2O or just the mass of a single N atom, so -1 N = 14 / 44 N2O). By default, converting 'N2O' <--> 'N' is forbidden to -prevent this ambiguity. However, the conversion can be performed within the -context 'N2O_conversions', in which case it is assumed that 'N' just means a -single N atom i.e. 1 N = 14 / 44 N2O, as shown below: - -.. code:: python - - >>> from openscm_units import unit_registry - >>> unit_registry("N2O").to("N") - Traceback (most recent call last): - ... - pint.errors.DimensionalityError: Cannot convert from 'N2O' ([nitrous_oxide]) to 'N' ([nitrogen]) - - # with a context, the conversion becomes legal again - >>> with unit_registry.context("N2O_conversions"): - ... unit_registry("N2O").to("N") - ... - - -*NOx* - -Like for methane, NOx emissions also suffer from a namespace collision. In order to -prevent inadvertent conversions from 'NOx' to e.g. 'N2O', the conversion 'NOx' <--> -'N' is by default forbidden. It can be performed within the 'NOx_conversions' context: - -.. code:: python - - >>> from openscm_units import unit_registry - >>> unit_registry("NOx").to("N") - Traceback (most recent call last): - ... - pint.errors.DimensionalityError: Cannot convert from 'NOx' ([NOx]) to 'N' ([nitrogen]) - - # with a context, the conversion becomes legal again - >>> with unit_registry.context("NOx_conversions"): - ... unit_registry("NOx").to("N") - ... - - -*NH3* - -In order to prevent inadvertent conversions from 'NH3' to 'CO2', the conversion -'NH3' <--> 'N' is by default forbidden. It can be performed within the 'NH3_conversions' -context analogous to the 'NOx_conversions' context: - -.. code:: python - - >>> from openscm_units import unit_registry - >>> unit_registry("NH3").to("N") - Traceback (most recent call last): - ... - pint.errors.DimensionalityError: Cannot convert from 'NH3' ([NH3]) to 'N' ([nitrogen]) - - # with a context, the conversion becomes legal again - >>> with unit_registry.context("NH3_conversions"): - ... unit_registry("NH3").to("N") - ... - - +See also `docs/source/notebooks/design-principles.py` """ from __future__ import annotations @@ -669,15 +514,3 @@ def split_gas_mixture( ret.append(quantity / mixture_unit * fraction_pct / 100 * constituent_unit) return ret - - -unit_registry = ScmUnitRegistry() -""" -Standard unit registry - -The unit registry contains all of the recognised units. Be careful, if you -edit this registry in one place then it will also be edited in any other -places that use ``openscm_units``. If you want multiple, separate registries, -create multiple instances of ``ScmUnitRegistry``. -""" -unit_registry.add_standards()