From 56ff15ed0a834ed5fdb2e904b9d75e6b7c91971e Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 27 May 2021 11:39:16 +1000 Subject: [PATCH 1/5] Add fixes --- src/openscm_units/_unit_registry.py | 47 +++++++++++++++++++++-------- tests/unit/test_units.py | 31 +++++++++++-------- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/openscm_units/_unit_registry.py b/src/openscm_units/_unit_registry.py index 3b26119..99fb59b 100644 --- a/src/openscm_units/_unit_registry.py +++ b/src/openscm_units/_unit_registry.py @@ -34,9 +34,8 @@ - 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. nitrous oxide emissions can be provided in 'N', 'N2O' or - 'N2ON' (a short-hand which indicates that only the mass of the nitrogen is being counted), - we provide conversions between these three +- less obvious ones e.g. NOx emissions can be provided in 'N' or 'NOx' (a short-hand + which is assumed to be NO2), we provide conversions between these two - 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")`` @@ -85,6 +84,22 @@ ... unit_registry("CH4").to("CO2") +*N2O* + +Nitrous oxide emissions are typically defined as 'N2O'. However, they are also reported as 'N2ON' (a short-hand which indicates that only the mass of the nitrogen is being counted). Reporting nitrous oxide emissions as 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") + pint.errors.DimensionalityError: Cannot convert from 'CH4' ([methane]) to 'C' ([carbon]) + + # with a context, the conversion becomes legal again + >>> with unit_registry.context("N2O_conversions"): + ... unit_registry("CH4").to("C") + + *NOx* Like for methane, NOx emissions also suffer from a namespace collision. In order to @@ -102,11 +117,6 @@ ... unit_registry("NOx").to("N") - # as an unavoidable side effect, this also becomes possible - >>> with unit_registry.context("NOx_conversions"): - ... unit_registry("NOx").to("N2O") - - *NH3* In order to prevent inadvertent conversions from 'NH3' to 'CO2', the conversion @@ -144,9 +154,11 @@ "CO2": ["12/44 * C", "carbon_dioxide"], "CH4": "methane", "HC50": ["CH4"], + "N2O": "nitrous_oxide", + "N2ON": ["44/28 * N2O", "nitrous_oxide_farming_style"], "N": "nitrogen", - "N2O": ["14/44 * N", "nitrous_oxide"], - "N2ON": ["14/28 * N", "nitrous_oxide_farming_style"], + # "N2O": ["14/44 * N", "nitrous_oxide"], + # "N2ON": ["14/28 * N", "nitrous_oxide_farming_style"], "NO2": ["14/46 * N", "nitrogen_dioxide"], # aerosol precursors "NOx": "NOx", @@ -379,16 +391,27 @@ def _load_contexts(self): ) self.add_context(_ch4_context) - _n2o_context = pint.Context("NOx_conversions") + _n2o_context = pint.Context("N2O_conversions") _n2o_context = self._add_transformations_to_context( _n2o_context, + "[nitrous_oxide]", + self.nitrous_oxide, + "[nitrogen]", + self.nitrogen, + 14 / 44, + ) + self.add_context(_n2o_context) + + _nox_context = pint.Context("NOx_conversions") + _nox_context = self._add_transformations_to_context( + _nox_context, "[nitrogen]", self.nitrogen, "[NOx]", self.NOx, (14 + 2 * 16) / 14, ) - self.add_context(_n2o_context) + self.add_context(_nox_context) _nh3_context = pint.Context("NH3_conversions") _nh3_context = self._add_transformations_to_context( diff --git a/tests/unit/test_units.py b/tests/unit/test_units.py index b85ec53..328af55 100644 --- a/tests/unit/test_units.py +++ b/tests/unit/test_units.py @@ -24,9 +24,16 @@ def test_base_unit(): def test_nitrogen(): N = unit_registry("N") - np.testing.assert_allclose(N.to("N2ON").magnitude, 28 / 14) np.testing.assert_allclose(N.to("NO2").magnitude, 46 / 14) + # can only convert to N with right context + with pytest.raises(DimensionalityError): + N.to("N2ON") + + with unit_registry.context("N2O_conversions"): + np.testing.assert_allclose(N.to("N2ON").magnitude, 28 / 14) + np.testing.assert_allclose(N.to("N2O").magnitude, 44 / 14) + def test_nox(): NOx = unit_registry("NOx") @@ -42,8 +49,6 @@ def test_nox(): np.testing.assert_allclose(N.to("NOx").magnitude, 46 / 14) np.testing.assert_allclose(NO2.to("NOx").magnitude, 1) np.testing.assert_allclose(NOx.to("NO2").magnitude, 1) - # this also becomes allowed, unfortunately... - np.testing.assert_allclose(NOx.to("N2O").magnitude, 44 / 46) def test_ammonia(): @@ -149,32 +154,32 @@ def test_a(): def test_context(): CO2 = unit_registry("CO2") - N = unit_registry("N") + N2ON = unit_registry("N2ON") with unit_registry.context("AR4GWP100"): - np.testing.assert_allclose(CO2.to("N").magnitude, 14 / (44 * 298)) - np.testing.assert_allclose(N.to("CO2").magnitude, 44 * 298 / 14) + np.testing.assert_allclose(CO2.to("N2ON").magnitude, 28 / (44 * 298)) + np.testing.assert_allclose(N2ON.to("CO2").magnitude, 44 * 298 / 28) def test_context_with_magnitude(): CO2 = 1 * unit_registry("CO2") - N = 1 * unit_registry("N") + N2ON = 1 * unit_registry("N2ON") with unit_registry.context("AR4GWP100"): - np.testing.assert_allclose(CO2.to("N").magnitude, 14 / (44 * 298)) - np.testing.assert_allclose(N.to("CO2").magnitude, 44 * 298 / 14) + np.testing.assert_allclose(CO2.to("N2ON").magnitude, 28 / (44 * 298)) + np.testing.assert_allclose(N2ON.to("CO2").magnitude, 44 * 298 / 28) def test_context_compound_unit(): CO2 = 1 * unit_registry("kg CO2 / yr") - N = 1 * unit_registry("kg N / yr") + N2ON = 1 * unit_registry("kg N2ON / yr") with unit_registry.context("AR4GWP100"): - np.testing.assert_allclose(CO2.to("kg N / yr").magnitude, 14 / (44 * 298)) - np.testing.assert_allclose(N.to("kg CO2 / yr").magnitude, 44 * 298 / 14) + np.testing.assert_allclose(CO2.to("kg N2ON / yr").magnitude, 28 / (44 * 298)) + np.testing.assert_allclose(N2ON.to("kg CO2 / yr").magnitude, 44 * 298 / 28) def test_context_dimensionality_error(): CO2 = unit_registry("CO2") with pytest.raises(DimensionalityError): - CO2.to("N") + CO2.to("N2O") @pytest.mark.parametrize( From f6de397a81f09c0c2d9d153b2340f444332bc1d6 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 27 May 2021 12:23:40 +1000 Subject: [PATCH 2/5] CHANGELOG --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76c190e..96c2274 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,8 @@ Changelog master ------ + +- (`#25 `_) Add "N2O_conversions" context to remove ambiguity in N2O conversions - (`#23 `_) Add AR5 GWPs with climate-carbon cycle feedbacks (closes `#22 `_) - (`#20 `_) Make ``openscm_units.data`` a module by adding an ``__init__.py`` file to it and add docs for ``openscm_units.data`` (closes `#19 `_) - (`#18 `_) Made NH3 a separate dimension to avoid accidental conversion to CO2 in GWP contexts. Also added an ``nh3_conversions`` context to convert to nitrogen (closes `#12 `_) From dbb34b6dc787540893d3a07cdeb04c9c2b36a123 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 27 May 2021 12:29:37 +1000 Subject: [PATCH 3/5] Fix docs --- src/openscm_units/_unit_registry.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/openscm_units/_unit_registry.py b/src/openscm_units/_unit_registry.py index 99fb59b..79ef213 100644 --- a/src/openscm_units/_unit_registry.py +++ b/src/openscm_units/_unit_registry.py @@ -86,19 +86,26 @@ *N2O* -Nitrous oxide emissions are typically defined as 'N2O'. However, they are also reported as 'N2ON' (a short-hand which indicates that only the mass of the nitrogen is being counted). Reporting nitrous oxide emissions as 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: +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") - pint.errors.DimensionalityError: Cannot convert from 'CH4' ([methane]) to 'C' ([carbon]) + 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("CH4").to("C") - + ... unit_registry("N2O").to("N") + *NOx* @@ -157,8 +164,6 @@ "N2O": "nitrous_oxide", "N2ON": ["44/28 * N2O", "nitrous_oxide_farming_style"], "N": "nitrogen", - # "N2O": ["14/44 * N", "nitrous_oxide"], - # "N2ON": ["14/28 * N", "nitrous_oxide_farming_style"], "NO2": ["14/46 * N", "nitrogen_dioxide"], # aerosol precursors "NOx": "NOx", From 146e2b2508001fdb2f6c171baad719e1df875953 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 27 May 2021 15:26:04 +1000 Subject: [PATCH 4/5] Add extra test --- tests/unit/test_units.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/test_units.py b/tests/unit/test_units.py index 328af55..168fe6b 100644 --- a/tests/unit/test_units.py +++ b/tests/unit/test_units.py @@ -155,10 +155,14 @@ def test_a(): def test_context(): CO2 = unit_registry("CO2") N2ON = unit_registry("N2ON") + N2O = unit_registry("N2O") with unit_registry.context("AR4GWP100"): np.testing.assert_allclose(CO2.to("N2ON").magnitude, 28 / (44 * 298)) np.testing.assert_allclose(N2ON.to("CO2").magnitude, 44 * 298 / 28) + np.testing.assert_allclose(CO2.to("N2O").magnitude, 1 / 298) + np.testing.assert_allclose(N2O.to("CO2").magnitude, 298) + def test_context_with_magnitude(): CO2 = 1 * unit_registry("CO2") From e721d26bd4400efec01dc99ede96662e72032c1b Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 27 May 2021 20:12:21 +1000 Subject: [PATCH 5/5] Add extra test --- tests/unit/test_units.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_units.py b/tests/unit/test_units.py index 168fe6b..68de607 100644 --- a/tests/unit/test_units.py +++ b/tests/unit/test_units.py @@ -49,6 +49,8 @@ def test_nox(): np.testing.assert_allclose(N.to("NOx").magnitude, 46 / 14) np.testing.assert_allclose(NO2.to("NOx").magnitude, 1) np.testing.assert_allclose(NOx.to("NO2").magnitude, 1) + with pytest.raises(DimensionalityError): + NOx.to("N2O") def test_ammonia():