From 159f732b36020688a3e1c630c0cb3e697fa0dbe7 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Fri, 2 Nov 2018 11:41:41 +0100 Subject: [PATCH 01/24] Improve documentation and use properties in core --- docs/conf.py | 18 ++++- openscm/core.py | 180 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 140 insertions(+), 58 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 62aff4cf..1dfd1fc6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -147,7 +147,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, "OpenSCM.tex", "OpenSCM Documentation", "Robert Gieseke, Zebedee Nicholls, Sven Willner", "manual") + ( + master_doc, + "OpenSCM.tex", + "OpenSCM Documentation", + "Robert Gieseke, Zebedee Nicholls, Sven Willner", + "manual", + ) ] @@ -176,4 +182,14 @@ ] +def skip(app, what, name, obj, skip, options): + if name == "__init__": + return False + return skip + + +def setup(app): + app.connect("autodoc-skip-member", skip) + + # -- Extension configuration ------------------------------------------------- diff --git a/openscm/core.py b/openscm/core.py index 3ab9d52a..9c013d52 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -37,30 +37,54 @@ class ParameterTypeError(Exception): class ParameterView: """ Generic view to a parameter (scalar or timeseries). + """ - Parameters - ---------- - name - Hierarchical name - region - Region (hierarchy) - unit - Unit + _name: Tuple[str] + """Hierarchical name""" - Attributes - ---------- - name - Hierarchical name (read-only) - region - Region (hierarchy) (read-only) - unit - Unit (read-only) - """ + _region: Tuple[str] + """Region (hierarchy)""" + + _unit: str + """Unit""" def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): - self.name = name - self.region = region - self.unit = unit + """ + Initialize. + + Parameters + ---------- + name + Hierarchical name + region + Region (hierarchy) + unit + Unit + """ + self._name = name + self._region = region + self._unit = unit + + @property + def name(self) -> Tuple[str]: + """ + Hierarchical name + """ + return self._name + + @property + def region(self) -> Tuple[str]: + """ + Region (hierarchy) + """ + return self._region + + @property + def unit(self) -> str: + """ + Unit + """ + return self._unit @property def is_empty(self) -> bool: @@ -73,14 +97,26 @@ def is_empty(self) -> bool: class ParameterInfo(ParameterView): """ Provides information about a parameter. - - Attributes - ---------- - parameter_type - Type (``"scalar"`` or ``"timeseries"``) """ - parameter_type: str + _parameter_type: str + """Type (``"scalar"`` or ``"timeseries"``)""" + + def __init__(self, parameter_type: str): + """ + Initialize. + + Parameters + ---------- + parameter_type + Type (``"scalar"`` or ``"timeseries"``) + """ + self._parameter_type = parameter_type + + @property + def parameter_type(self) -> str: + """Type (``"scalar"`` or ``"timeseries"``)""" + return self._parameter_type class ScalarView(ParameterView): @@ -364,42 +400,65 @@ class Core: OpenSCM core class. Represents a model run with a particular simple climate model. + """ - Parameters - ---------- - model - Name of the SCM to run - start_time - Beginning of the time range to run over (seconds since 1970-01-01 00:00:00) - end_time - End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + _end_time: int + """End of the time range to run over (including; seconds since 1970-01-01 00:00:00)""" - Raises - ------ - KeyError - No adapter for SCM named ``model`` found - ValueError - ``end_time`` before ``start_time`` - - Attributes - ---------- - end_time - End of the time range to run over (read-only) - model - Name of the SCM to run (read-only) - parameterset - Set of parameters for the run (read-only) - start_time - Beginning of the time range to run over (read-only) - """ + _model: str + """Name of the SCM to run""" - parameters: ParameterSet + _parameters: ParameterSet + """Set of parameters for the run""" + + _start_time: int + """Beginning of the time range to run over (seconds since 1970-01-01 00:00:00)""" def __init__(self, model: str, start_time: int, end_time: int): - self.model = model - self.start_time = start_time - self.end_time = end_time - self.parameters = ParameterSet() + """ + Initialize. + + Attributes + ---------- + model + Name of the SCM to run + start_time + Beginning of the time range to run over (seconds since 1970-01-01 00:00:00) + end_time + End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + + Raises + ------ + KeyError + No adapter for SCM named ``model`` found + ValueError + ``end_time`` before ``start_time`` + """ + self._model = model + self._start_time = start_time + self._end_time = end_time + self._parameters = ParameterSet() + + @property + def end_time(self) -> int: + """ + End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + """ + return self._end_time + + @property + def model(self) -> str: + """ + Name of the SCM to run + """ + return self._model + + @property + def parameters(self) -> ParameterSet: + """ + Set of parameters for the run + """ + return self._parameterset def run(self) -> None: """ @@ -407,6 +466,13 @@ def run(self) -> None: """ raise NotImplementedError + @property + def start_time(self) -> int: + """ + Beginning of the time range to run over (seconds since 1970-01-01 00:00:00) + """ + return self._start_time + def step(self) -> int: """ Do a single time step. From 5639d3779de36793cc8768b5d8b9949187690407 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 3 Nov 2018 15:29:49 +0100 Subject: [PATCH 02/24] Add _Parameter and _Region --- openscm/core.py | 383 ++++++++++++++++++++++++++++++++++++++-- tests/unit/test_core.py | 10 ++ 2 files changed, 376 insertions(+), 17 deletions(-) create mode 100644 tests/unit/test_core.py diff --git a/openscm/core.py b/openscm/core.py index 9c013d52..c6f413b7 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -8,17 +8,288 @@ be easily implementable in several programming languages. """ -from typing import Sequence, Tuple +from enum import Enum +from typing import Any, Dict, Sequence, Tuple -class ParameterLengthError(Exception): +class ParameterType(Enum): + """ + Parameter type. + """ + + SCALAR = 1 + TIMESERIES = 2 + + +class _Parameter: + """ + Represents a parameter in the parameter hierarchy. + """ + + _children: Dict[str, "_Parameter"] + """Child parameters""" + + _data: Any + """Data""" + + _has_been_aggregated: bool + """ + Tells if parameter has already been read in an aggregated way, i.e., aggregating + over child parameters + """ + + _has_been_written_to: bool + """Tells if parameter data has already been changed""" + + _name: str + """Name""" + + _parent: "_Parameter" + """Parent parameter""" + + _type: ParameterType + """Parameter type""" + + _unit: str + """Unit""" + + def __init__(self, name: str): + """ + Initialize. + + Parameters + ---------- + name + Name + """ + self._children = {} + self._has_been_aggregated = False + self._has_been_written_to = False + self._name = name + self._parent = None + self._type = None + self._unit = None + + def get_or_create_child_parameter( + self, name: str, unit: str, parameter_type: ParameterType + ) -> "_Parameter": + """ + Get a (direct) child parameter of this parameter. Create and add it if not + found. + + Parameters + ---------- + name + Name + unit + Unit + parameter_type + Parameter type + + Raises + ------ + RegionAggregatedError + If the subregion would need to be added and a parameter of this region has + already been read in an aggregated way. In this case a subregion cannot be + created. + """ + res = self._children.get(name, None) + if res is None: + if self._has_been_written_to: + raise ParameterWrittenError + if self._has_been_aggregated: + raise ParameterAggregatedError + res = _Parameter(name) + res._parent = self + res._type = parameter_type + res._unit = unit + self._children[name] = res + return res + + def attempt_aggregate(self, parameter_type: ParameterType) -> None: + """ + Tell parameter that it will be read from in an aggregated way, i.e., aggregating + over child parameters. + + Parameters + ---------- + parameter_type + Parameter type to be read + + Raises + ------ + ParameterTypeError + If parameter has already been read from or written to in a different type. + """ + if self._type is not None and self._type != parameter_type: + raise ParameterTypeError + self._type = parameter_type + self._has_been_aggregated = True + + def attempt_write(self, parameter_type: ParameterType) -> None: + """ + Tell parameter that its data will be written to. + + Parameters + ---------- + parameter_type + Parameter type to be written + + Raises + ------ + ParameterReadonlyError + If parameter is read-only because it has child parameters. + """ + if len(self._children) > 0: + raise ParameterReadonlyError + self.attempt_read(parameter_type) + self._has_been_written_to = True + + @property + def full_name(self) -> Tuple[str]: + """ + Full hierarchical name + """ + p = self + r = [] + while p is not None: + r.append(p.name) + p = p._parent + return tuple(reversed(r)) + + @property + def name(self) -> str: + """ + Name + """ + return self._name + + @property + def parameter_type(self) -> ParameterType: + """ + Parameter type + """ + return self._type + + @property + def parent(self) -> "_Parameter": + """ + Parent parameter + """ + return self._parent + + @property + def unit(self) -> str: + """ + Unit + """ + return self._unit + + +class _Region: + """ + Represents a region in the region hierarchy. + """ + + _children: Dict[str, "_Region"] + """Subregions""" + + _has_been_aggregated: bool + """Tells if a parameter of this region has already been read in an aggregated way""" + + _name: str + """Name""" + + _parameters: Dict[str, _Parameter] + """Parameters""" + + _parent: "_Region" + """Parent region (or `None` if root region)""" + + def __init__(self, name: str): + """ + Initialize + + Parameters + ---------- + name + Name + """ + self._name = name + self._children = {} + self._has_been_aggregated = False + self._parameters = {} + self._parent = None + + def get_or_create_subregion(self, name: str) -> "_Region": + """ + Get a (direct) subregion of this region. Create and add it if not found. + + Parameters + ---------- + name + Name + + Raises + ------ + RegionAggregatedError + If the subregion would need to be added and a parameter of this region has + already been read in an aggregated way. In this case a subregion cannot be + created. + """ + res = self._children.get(name, None) + if res is None: + if self._has_been_aggregated: + raise RegionAggregatedError + res = _Region(name) + res._parent = self + self._children[name] = res + return res + + def get_or_create_parameter(self, name: str) -> _Parameter: + """ + Get a root parameter for this region. Create and add it if not found. + + Parameters + ---------- + name + Name + """ + res = self._parameters.get(name, None) + if res is None: + res = _Parameter(name) + self._parameters[name] = res + return res + + @property + def name(self) -> str: + """ + Name + """ + return self._name + + @property + def parent(self) -> "_Region": + """ + Parent region (or `None` if root region) + """ + return self._parent + + +class ParameterError(Exception): + """ + Exception relating to a parameter. Used as super class. + """ + + +class ParameterLengthError(ParameterError): """ Exception raised when sequences in timeseries do not match run size. """ -class ParameterReadonlyError(Exception): +class ParameterReadonlyError(ParameterError): """ Exception raised when a requested parameter is read-only. @@ -27,13 +298,33 @@ class ParameterReadonlyError(Exception): """ -class ParameterTypeError(Exception): +class ParameterTypeError(ParameterError): """ Exception raised when a parameter is of a different type than requested (scalar or timeseries). """ +class ParameterAggregatedError(ParameterError): + """ + Exception raised when a parameter has been read from (raised, e.g., when attempting + to create a child parameter). + """ + + +class ParameterWrittenError(ParameterError): + """ + Exception raised when a parameter has been written to (raised, e.g., when attempting + to create a child parameter). + """ + + +class RegionAggregatedError(Exception): + """ + Exception raised when a region has been read from in a region-aggregated way. + """ + + class ParameterView: """ Generic view to a parameter (scalar or timeseries). @@ -43,7 +334,7 @@ class ParameterView: """Hierarchical name""" _region: Tuple[str] - """Region (hierarchy)""" + """Hierarchical region name""" _unit: str """Unit""" @@ -57,7 +348,7 @@ def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): name Hierarchical name region - Region (hierarchy) + Hierarchical region name unit Unit """ @@ -75,7 +366,7 @@ def name(self) -> Tuple[str]: @property def region(self) -> Tuple[str]: """ - Region (hierarchy) + Hierarchical region name """ return self._region @@ -99,24 +390,24 @@ class ParameterInfo(ParameterView): Provides information about a parameter. """ - _parameter_type: str - """Type (``"scalar"`` or ``"timeseries"``)""" + _type: ParameterType + """Parameter type""" - def __init__(self, parameter_type: str): + def __init__(self, parameter: _Parameter): """ Initialize. Parameters ---------- - parameter_type - Type (``"scalar"`` or ``"timeseries"``) + parameter + Parameter """ - self._parameter_type = parameter_type + self._type = parameter.parameter_type @property - def parameter_type(self) -> str: - """Type (``"scalar"`` or ``"timeseries"``)""" - return self._parameter_type + def parameter_type(self) -> ParameterType: + """Parameter type""" + return self._type class ScalarView(ParameterView): @@ -228,6 +519,64 @@ class ParameterSet: Collates a set of parameters. """ + _world: _Region + """Root region (contains all parameters)""" + + def __init__(self): + """ + Initialize. + """ + self._world = _Region(None) + + def _get_or_create_region(self, name: Tuple[str]) -> _Region: + """ + Get a region. Create and add it if not found. + + Parameters + ---------- + name + Hierarchy name of the region + """ + if len(name) > 0: + p = self._get_or_create_region(name[:-1]) + return p.get_or_create_subregion(name[-1]) + else: + return self._world + + def _get_or_create_parameter( + self, + name: Tuple[str], + region: _Region, + unit: str, + parameter_type: ParameterType, + ) -> _Parameter: + """ + Get a parameter. Create and add it if not found. + + Parameters + ---------- + name + Hierarchy name of the parameter + region + Region + unit + Unit for the values in the view + parameter_type + Parameter type + + Raises + ------ + ValueError + Name not given + """ + if len(name) > 1: + p = self._get_or_create_parameter(name[:-1], region, unit, parameter_type) + return p.get_or_create_child_parameter(name[-1], unit, parameter_type) + elif len(name) == 1: + return region.get_or_create_parameter(name[0]) + else: # len(name) == 0 + raise ValueError + def get_scalar_view( self, name: Tuple[str], region: Tuple[str], unit: str ) -> ScalarView: @@ -241,7 +590,7 @@ def get_scalar_view( name Hierarchy name of the parameter region - Region (hierarchy) + Hierarchical region name unit Unit for the values in the view diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py new file mode 100644 index 00000000..07ae4933 --- /dev/null +++ b/tests/unit/test_core.py @@ -0,0 +1,10 @@ +from openscm.core import ParameterSet, ParameterType + + +def test_parameterset(): + parameterset = ParameterSet() + r = parameterset._get_or_create_region(("DEU", "BER")) + p = parameterset._get_or_create_parameter( + ("Emissions", "CO2", "Industry"), r, "CO2/a", ParameterType.TIMESERIES + ) + assert "|".join(p.full_name) == "Emissions|CO2|Industry" From 742d4311a13695299fde0a78fd3015ccb43213c0 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 3 Nov 2018 15:30:04 +0100 Subject: [PATCH 03/24] Fix comment wrapping --- openscm/core.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/openscm/core.py b/openscm/core.py index c6f413b7..69addeff 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -1,11 +1,10 @@ """ -Then OpenSCM low-level API includes the basic functionality to run a -particular simple climate model with OpenSCM as well as -setting/getting its parameter values. Mapping of parameter names and -units is done internally. +The OpenSCM low-level API includes the basic functionality to run a particular +simple climate model with OpenSCM as well as setting/getting its parameter values. +Mapping of parameter names and units is done internally. -Parts of this API definition seems unpythonic as it is designed to -be easily implementable in several programming languages. +Parts of this API definition seems unpythonic as it is designed to be easily +implementable in several programming languages. """ from enum import Enum @@ -723,7 +722,8 @@ def get_parameter_info(self, name: Tuple[str]) -> ParameterInfo: Returns ------- ParameterInfo - Information about the parameter or ``None`` if the parameter has not been created yet. + Information about the parameter or ``None`` if the parameter has not been + created yet. """ raise NotImplementedError @@ -752,7 +752,9 @@ class Core: """ _end_time: int - """End of the time range to run over (including; seconds since 1970-01-01 00:00:00)""" + """ + End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + """ _model: str """Name of the SCM to run""" @@ -774,7 +776,8 @@ def __init__(self, model: str, start_time: int, end_time: int): start_time Beginning of the time range to run over (seconds since 1970-01-01 00:00:00) end_time - End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + End of the time range to run over (including; seconds since 1970-01-01 + 00:00:00) Raises ------ From 658fc271ddddf1db82d7311ad16ea75944194a3e Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 3 Nov 2018 15:30:14 +0100 Subject: [PATCH 04/24] Adjust gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 03d11588..aae80eae 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ build/ dist/ docs/_build venv/ +.cov2emacs.log From 5e0fdf9bf11b8a79f52c925c239f5ace78adde1a Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 3 Nov 2018 15:42:22 +0100 Subject: [PATCH 05/24] Ignore coverage.py html output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aae80eae..f91b8db7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist/ docs/_build venv/ .cov2emacs.log +htmlcov/ From 194d2835888367a546c3d3fca66470507e7c75de Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 3 Nov 2018 16:15:59 +0100 Subject: [PATCH 06/24] Add further _Parameter and _Region tests --- openscm/core.py | 14 ++++++- tests/unit/test_core.py | 89 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/openscm/core.py b/openscm/core.py index 69addeff..a270d82e 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -141,7 +141,7 @@ def attempt_write(self, parameter_type: ParameterType) -> None: """ if len(self._children) > 0: raise ParameterReadonlyError - self.attempt_read(parameter_type) + self.attempt_aggregate(parameter_type) self._has_been_written_to = True @property @@ -194,7 +194,10 @@ class _Region: """Subregions""" _has_been_aggregated: bool - """Tells if a parameter of this region has already been read in an aggregated way""" + """ + Tells if a parameter of this region has already been read in an aggregated way, + i.e., aggregating over subregions + """ _name: str """Name""" @@ -260,6 +263,13 @@ def get_or_create_parameter(self, name: str) -> _Parameter: self._parameters[name] = res return res + def attempt_aggregate(self) -> None: + """ + Tell region that one of its parameters will be read from in an aggregated way, + i.e., aggregating over subregions. + """ + self._has_been_aggregated = True + @property def name(self) -> str: """ diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 07ae4933..0988e397 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -1,10 +1,85 @@ -from openscm.core import ParameterSet, ParameterType +from openscm.core import ( + ParameterAggregatedError, + ParameterReadonlyError, + ParameterSet, + ParameterType, + ParameterTypeError, + ParameterWrittenError, + RegionAggregatedError, +) +import pytest +parameterset = ParameterSet() -def test_parameterset(): - parameterset = ParameterSet() - r = parameterset._get_or_create_region(("DEU", "BER")) - p = parameterset._get_or_create_parameter( - ("Emissions", "CO2", "Industry"), r, "CO2/a", ParameterType.TIMESERIES + +def test_region(): + region_deu = parameterset._get_or_create_region(("DEU",)) + assert region_deu.name == "DEU" + + region_ber = parameterset._get_or_create_region(("DEU", "BER")) + assert region_ber.name == "BER" + assert region_ber.parent == region_deu + + region_deu.attempt_aggregate() + with pytest.raises(RegionAggregatedError): + parameterset._get_or_create_region(("DEU", "BRB")) + + +def test_parameter(): + region_ber = parameterset._get_or_create_region(("DEU", "BER")) + + with pytest.raises(ValueError): + parameterset._get_or_create_parameter( + (), region_ber, "GtCO2/a", ParameterType.TIMESERIES + ) + + param_co2 = parameterset._get_or_create_parameter( + ("Emissions", "CO2"), region_ber, "GtCO2/a", ParameterType.TIMESERIES ) - assert "|".join(p.full_name) == "Emissions|CO2|Industry" + assert param_co2.full_name == ("Emissions", "CO2") + assert param_co2.name == "CO2" + + param_emissions = param_co2.parent + assert param_emissions.full_name == ("Emissions",) + assert param_emissions.name == "Emissions" + # Before any read/write attempt these should be None: + assert param_emissions.parameter_type is None + assert param_emissions.unit is None + + param_industry = parameterset._get_or_create_parameter( + ("Emissions", "CO2", "Industry"), + region_ber, + "GtCO2/a", + ParameterType.TIMESERIES, + ) + assert param_industry.full_name == ("Emissions", "CO2", "Industry") + assert param_industry.name == "Industry" + assert param_industry.parameter_type == ParameterType.TIMESERIES + assert param_industry.unit == "GtCO2/a" + + with pytest.raises(ParameterReadonlyError): + param_co2.attempt_write(ParameterType.TIMESERIES) + + with pytest.raises(ParameterTypeError): + param_co2.attempt_aggregate(ParameterType.SCALAR) + + param_co2.attempt_aggregate(ParameterType.TIMESERIES) + with pytest.raises(ParameterAggregatedError): + parameterset._get_or_create_parameter( + ("Emissions", "CO2", "Landuse"), + region_ber, + "GtCO2/a", + ParameterType.TIMESERIES, + ) + + with pytest.raises(ParameterTypeError): + param_industry.attempt_write(ParameterType.SCALAR) + + param_industry.attempt_write(ParameterType.TIMESERIES) + with pytest.raises(ParameterWrittenError): + parameterset._get_or_create_parameter( + ("Emissions", "CO2", "Industry", "Other"), + region_ber, + "GtCO2/a", + ParameterType.TIMESERIES, + ) From 24da54d7126ddee98702e35dd1fe07dc99a54ca2 Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Mon, 5 Nov 2018 16:46:49 +0100 Subject: [PATCH 07/24] Update openscm/core.py Co-Authored-By: swillner --- openscm/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openscm/core.py b/openscm/core.py index a270d82e..06a3536c 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -33,7 +33,7 @@ class _Parameter: _has_been_aggregated: bool """ - Tells if parameter has already been read in an aggregated way, i.e., aggregating + If True, parameter has already been read in an aggregated way, i.e., aggregating over child parameters """ From c937a5ef5325280efdaec7b9e241a1cf97eb8fff Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Mon, 5 Nov 2018 16:46:55 +0100 Subject: [PATCH 08/24] Update openscm/core.py Co-Authored-By: swillner --- openscm/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openscm/core.py b/openscm/core.py index 06a3536c..f24c9fa2 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -38,7 +38,7 @@ class _Parameter: """ _has_been_written_to: bool - """Tells if parameter data has already been changed""" + """If True, parameter data has already been changed""" _name: str """Name""" From 27d8d86ee2ce7572d2e7649edd17fd93ede9238f Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Mon, 5 Nov 2018 16:47:50 +0100 Subject: [PATCH 09/24] Update openscm/core.py Co-Authored-By: swillner --- openscm/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openscm/core.py b/openscm/core.py index f24c9fa2..9126cc0b 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -139,7 +139,7 @@ def attempt_write(self, parameter_type: ParameterType) -> None: ParameterReadonlyError If parameter is read-only because it has child parameters. """ - if len(self._children) > 0: + if self._children: raise ParameterReadonlyError self.attempt_aggregate(parameter_type) self._has_been_written_to = True From a4caa7c49e84edabd6dd72489d9fdba3a74aae0b Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Tue, 6 Nov 2018 10:50:55 +0100 Subject: [PATCH 10/24] Add doc references --- docs/units.rst | 2 ++ openscm/core.py | 44 +++++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/units.rst b/docs/units.rst index 6cc80950..f67e51df 100644 --- a/docs/units.rst +++ b/docs/units.rst @@ -1,3 +1,5 @@ +.. _units: + Units ----- diff --git a/openscm/core.py b/openscm/core.py index 9126cc0b..749b9fd5 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -1,7 +1,8 @@ """ The OpenSCM low-level API includes the basic functionality to run a particular -simple climate model with OpenSCM as well as setting/getting its parameter values. -Mapping of parameter names and units is done internally. +simple climate model with OpenSCM as well as setting/getting its :ref:`parameter +` values. Mapping of :ref:`parameter names ` and +:ref:`units ` is done internally. Parts of this API definition seems unpythonic as it is designed to be easily implementable in several programming languages. @@ -22,7 +23,8 @@ class ParameterType(Enum): class _Parameter: """ - Represents a parameter in the parameter hierarchy. + Represents a :ref:`parameter ` in the :ref:`parameter hierarchy + `. """ _children: Dict[str, "_Parameter"] @@ -147,7 +149,7 @@ def attempt_write(self, parameter_type: ParameterType) -> None: @property def full_name(self) -> Tuple[str]: """ - Full hierarchical name + Full :ref:`hierarchical name ` """ p = self r = [] @@ -336,11 +338,11 @@ class RegionAggregatedError(Exception): class ParameterView: """ - Generic view to a parameter (scalar or timeseries). + Generic view to a :ref:`parameter ` (scalar or timeseries). """ _name: Tuple[str] - """Hierarchical name""" + """:ref:`Hierarchical name `""" _region: Tuple[str] """Hierarchical region name""" @@ -355,7 +357,7 @@ def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): Parameters ---------- name - Hierarchical name + :ref:`Hierarchical name ` region Hierarchical region name unit @@ -368,7 +370,7 @@ def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): @property def name(self) -> Tuple[str]: """ - Hierarchical name + :ref:`Hierarchical name ` """ return self._name @@ -396,7 +398,7 @@ def is_empty(self) -> bool: class ParameterInfo(ParameterView): """ - Provides information about a parameter. + Provides information about a :ref:`parameter `. """ _type: ParameterType @@ -525,7 +527,7 @@ def set(self, value: float, index: int) -> None: class ParameterSet: """ - Collates a set of parameters. + Collates a set of :ref:`parameters `. """ _world: _Region @@ -565,7 +567,7 @@ def _get_or_create_parameter( Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter region Region unit @@ -597,7 +599,7 @@ def get_scalar_view( Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter region Hierarchical region name unit @@ -623,9 +625,9 @@ def get_writable_scalar_view( Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter region - Region (hierarchy) + Hierarchical region name unit Unit for the values in the view @@ -659,9 +661,9 @@ def get_timeseries_view( Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter region - Region (hierarchy) + Hierarchical region name unit Unit for the values in the view start_time @@ -694,9 +696,9 @@ def get_writable_timeseries_view( Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter region - Region (hierarchy) + Hierarchical region name unit Unit for the values in the view start_time @@ -722,7 +724,7 @@ def get_parameter_info(self, name: Tuple[str]) -> ParameterInfo: Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter Raises ------ @@ -744,7 +746,7 @@ def has_parameter(self, name: Tuple[str]) -> bool: Parameters ---------- name - Hierarchy name of the parameter + :ref:`Hierarchical name ` of the parameter Raises ------ @@ -770,7 +772,7 @@ class Core: """Name of the SCM to run""" _parameters: ParameterSet - """Set of parameters for the run""" + """Set of :ref:`parameters ` for the run""" _start_time: int """Beginning of the time range to run over (seconds since 1970-01-01 00:00:00)""" From f1b6e52609139da25ce2f44cb9b5f7a77f41608d Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Tue, 6 Nov 2018 10:56:46 +0100 Subject: [PATCH 11/24] Fix documentation --- openscm/core.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openscm/core.py b/openscm/core.py index 749b9fd5..d882e8f1 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -89,10 +89,13 @@ def get_or_create_child_parameter( Raises ------ - RegionAggregatedError - If the subregion would need to be added and a parameter of this region has - already been read in an aggregated way. In this case a subregion cannot be - created. + ParameterAggregatedError + If the child paramater would need to be added, but this parameter has + already been read in an aggregated way. In this case a child parameter + cannot be added. + ParameterWrittenError + If the child paramater would need to be added, but this parameter has + already been written to. In this case a child parameter cannot be added. """ res = self._children.get(name, None) if res is None: @@ -197,7 +200,7 @@ class _Region: _has_been_aggregated: bool """ - Tells if a parameter of this region has already been read in an aggregated way, + If True, a parameter of this region has already been read in an aggregated way, i.e., aggregating over subregions """ From 29b19b3f41889d82b67504f9a7f6afefe89e7609 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Tue, 6 Nov 2018 11:01:12 +0100 Subject: [PATCH 12/24] Add ValueError message --- openscm/core.py | 2 +- tests/unit/test_core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openscm/core.py b/openscm/core.py index d882e8f1..47ddf11c 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -589,7 +589,7 @@ def _get_or_create_parameter( elif len(name) == 1: return region.get_or_create_parameter(name[0]) else: # len(name) == 0 - raise ValueError + raise ValueError("No region name given") def get_scalar_view( self, name: Tuple[str], region: Tuple[str], unit: str diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 0988e397..7f2d9e80 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -28,7 +28,7 @@ def test_region(): def test_parameter(): region_ber = parameterset._get_or_create_region(("DEU", "BER")) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="No region name given"): parameterset._get_or_create_parameter( (), region_ber, "GtCO2/a", ParameterType.TIMESERIES ) From 32f0e1dd1e78448db9998481448ff97c4a415499 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Tue, 6 Nov 2018 11:03:33 +0100 Subject: [PATCH 13/24] Fix tests: Use fixture --- tests/unit/test_core.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 7f2d9e80..e670e996 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -9,10 +9,15 @@ ) import pytest -parameterset = ParameterSet() +@pytest.fixture +def parameterset(): + res = ParameterSet() + res._get_or_create_region(("DEU", "BER")) + return res -def test_region(): + +def test_region(parameterset): region_deu = parameterset._get_or_create_region(("DEU",)) assert region_deu.name == "DEU" @@ -25,7 +30,7 @@ def test_region(): parameterset._get_or_create_region(("DEU", "BRB")) -def test_parameter(): +def test_parameter(parameterset): region_ber = parameterset._get_or_create_region(("DEU", "BER")) with pytest.raises(ValueError, match="No region name given"): From 8dbffaacffb9da71dfd07d968168cd3636af98c6 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Thu, 8 Nov 2018 17:46:38 +0100 Subject: [PATCH 14/24] Add changelog entry --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0e55080c..c6e42bbb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,3 +5,4 @@ master ------ - (`#35 `_) Add units module +- (`#40 `_) Add parameter handling in core module From 2d1c75d97bd72b021dcc10c6d4d5608baaae0f58 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Thu, 8 Nov 2018 18:06:31 +0100 Subject: [PATCH 15/24] Fix documentation --- openscm/core.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/openscm/core.py b/openscm/core.py index 47ddf11c..d2582750 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -670,7 +670,7 @@ def get_timeseries_view( unit Unit for the values in the view start_time - Time for first point in timeseries (seconds since 1970-01-01 00:00:00) + Time for first point in timeseries (seconds since ``1970-01-01 00:00:00``) period_length Length of single time step in seconds @@ -705,7 +705,7 @@ def get_writable_timeseries_view( unit Unit for the values in the view start_time - Time for first point in timeseries (seconds since 1970-01-01 00:00:00) + Time for first point in timeseries (seconds since ``1970-01-01 00:00:00``) period_length Length of single time step in seconds @@ -768,7 +768,8 @@ class Core: _end_time: int """ - End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + End of the time range to run over (including; seconds since + ``1970-01-01 00:00:00``) """ _model: str @@ -778,7 +779,10 @@ class Core: """Set of :ref:`parameters ` for the run""" _start_time: int - """Beginning of the time range to run over (seconds since 1970-01-01 00:00:00)""" + """ + Beginning of the time range to run over (seconds since + ``1970-01-01 00:00:00``) + """ def __init__(self, model: str, start_time: int, end_time: int): """ @@ -789,10 +793,11 @@ def __init__(self, model: str, start_time: int, end_time: int): model Name of the SCM to run start_time - Beginning of the time range to run over (seconds since 1970-01-01 00:00:00) + Beginning of the time range to run over (seconds since + ``1970-01-01 00:00:00``) end_time - End of the time range to run over (including; seconds since 1970-01-01 - 00:00:00) + End of the time range to run over (including; seconds since + ``1970-01-01 00:00:00``) Raises ------ @@ -809,7 +814,8 @@ def __init__(self, model: str, start_time: int, end_time: int): @property def end_time(self) -> int: """ - End of the time range to run over (including; seconds since 1970-01-01 00:00:00) + End of the time range to run over (including; seconds since + ``1970-01-01 00:00:00``) """ return self._end_time @@ -836,7 +842,8 @@ def run(self) -> None: @property def start_time(self) -> int: """ - Beginning of the time range to run over (seconds since 1970-01-01 00:00:00) + Beginning of the time range to run over (seconds since + ``1970-01-01 00:00:00``) """ return self._start_time @@ -847,6 +854,6 @@ def step(self) -> int: Returns ------- int - Current time (seconds since 1970-01-01 00:00:00) + Current time (seconds since ``1970-01-01 00:00:00``) """ raise NotImplementedError From f30c9304bb946f9bf15856d4a7f1372f52ee5a00 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Thu, 8 Nov 2018 21:16:36 +0100 Subject: [PATCH 16/24] Move .cov2emacs to global gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index f91b8db7..ced95cec 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,4 @@ build/ dist/ docs/_build venv/ -.cov2emacs.log htmlcov/ From 4419b696e51754eae3c9c90f47b000227bc29adc Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sun, 16 Dec 2018 16:35:56 +0100 Subject: [PATCH 17/24] Separate into submodules --- openscm/core.py | 573 +++---------------------------------- openscm/errors.py | 47 +++ openscm/parameter_views.py | 190 ++++++++++++ openscm/parameters.py | 204 +++++++++++++ openscm/regions.py | 141 +++++++++ tests/unit/test_core.py | 7 +- 6 files changed, 628 insertions(+), 534 deletions(-) create mode 100644 openscm/errors.py create mode 100644 openscm/parameter_views.py create mode 100644 openscm/parameters.py create mode 100644 openscm/regions.py diff --git a/openscm/core.py b/openscm/core.py index d2582750..b359167e 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -8,524 +8,16 @@ implementable in several programming languages. """ -from enum import Enum -from typing import Any, Dict, Sequence, Tuple - - -class ParameterType(Enum): - """ - Parameter type. - """ - - SCALAR = 1 - TIMESERIES = 2 - - -class _Parameter: - """ - Represents a :ref:`parameter ` in the :ref:`parameter hierarchy - `. - """ - - _children: Dict[str, "_Parameter"] - """Child parameters""" - - _data: Any - """Data""" - - _has_been_aggregated: bool - """ - If True, parameter has already been read in an aggregated way, i.e., aggregating - over child parameters - """ - - _has_been_written_to: bool - """If True, parameter data has already been changed""" - - _name: str - """Name""" - - _parent: "_Parameter" - """Parent parameter""" - - _type: ParameterType - """Parameter type""" - - _unit: str - """Unit""" - - def __init__(self, name: str): - """ - Initialize. - - Parameters - ---------- - name - Name - """ - self._children = {} - self._has_been_aggregated = False - self._has_been_written_to = False - self._name = name - self._parent = None - self._type = None - self._unit = None - - def get_or_create_child_parameter( - self, name: str, unit: str, parameter_type: ParameterType - ) -> "_Parameter": - """ - Get a (direct) child parameter of this parameter. Create and add it if not - found. - - Parameters - ---------- - name - Name - unit - Unit - parameter_type - Parameter type - - Raises - ------ - ParameterAggregatedError - If the child paramater would need to be added, but this parameter has - already been read in an aggregated way. In this case a child parameter - cannot be added. - ParameterWrittenError - If the child paramater would need to be added, but this parameter has - already been written to. In this case a child parameter cannot be added. - """ - res = self._children.get(name, None) - if res is None: - if self._has_been_written_to: - raise ParameterWrittenError - if self._has_been_aggregated: - raise ParameterAggregatedError - res = _Parameter(name) - res._parent = self - res._type = parameter_type - res._unit = unit - self._children[name] = res - return res - - def attempt_aggregate(self, parameter_type: ParameterType) -> None: - """ - Tell parameter that it will be read from in an aggregated way, i.e., aggregating - over child parameters. - - Parameters - ---------- - parameter_type - Parameter type to be read - - Raises - ------ - ParameterTypeError - If parameter has already been read from or written to in a different type. - """ - if self._type is not None and self._type != parameter_type: - raise ParameterTypeError - self._type = parameter_type - self._has_been_aggregated = True - - def attempt_write(self, parameter_type: ParameterType) -> None: - """ - Tell parameter that its data will be written to. - - Parameters - ---------- - parameter_type - Parameter type to be written - - Raises - ------ - ParameterReadonlyError - If parameter is read-only because it has child parameters. - """ - if self._children: - raise ParameterReadonlyError - self.attempt_aggregate(parameter_type) - self._has_been_written_to = True - - @property - def full_name(self) -> Tuple[str]: - """ - Full :ref:`hierarchical name ` - """ - p = self - r = [] - while p is not None: - r.append(p.name) - p = p._parent - return tuple(reversed(r)) - - @property - def name(self) -> str: - """ - Name - """ - return self._name - - @property - def parameter_type(self) -> ParameterType: - """ - Parameter type - """ - return self._type - - @property - def parent(self) -> "_Parameter": - """ - Parent parameter - """ - return self._parent - - @property - def unit(self) -> str: - """ - Unit - """ - return self._unit - - -class _Region: - """ - Represents a region in the region hierarchy. - """ - - _children: Dict[str, "_Region"] - """Subregions""" - - _has_been_aggregated: bool - """ - If True, a parameter of this region has already been read in an aggregated way, - i.e., aggregating over subregions - """ - - _name: str - """Name""" - - _parameters: Dict[str, _Parameter] - """Parameters""" - - _parent: "_Region" - """Parent region (or `None` if root region)""" - - def __init__(self, name: str): - """ - Initialize - - Parameters - ---------- - name - Name - """ - self._name = name - self._children = {} - self._has_been_aggregated = False - self._parameters = {} - self._parent = None - - def get_or_create_subregion(self, name: str) -> "_Region": - """ - Get a (direct) subregion of this region. Create and add it if not found. - - Parameters - ---------- - name - Name - - Raises - ------ - RegionAggregatedError - If the subregion would need to be added and a parameter of this region has - already been read in an aggregated way. In this case a subregion cannot be - created. - """ - res = self._children.get(name, None) - if res is None: - if self._has_been_aggregated: - raise RegionAggregatedError - res = _Region(name) - res._parent = self - self._children[name] = res - return res - - def get_or_create_parameter(self, name: str) -> _Parameter: - """ - Get a root parameter for this region. Create and add it if not found. - - Parameters - ---------- - name - Name - """ - res = self._parameters.get(name, None) - if res is None: - res = _Parameter(name) - self._parameters[name] = res - return res - - def attempt_aggregate(self) -> None: - """ - Tell region that one of its parameters will be read from in an aggregated way, - i.e., aggregating over subregions. - """ - self._has_been_aggregated = True - - @property - def name(self) -> str: - """ - Name - """ - return self._name - - @property - def parent(self) -> "_Region": - """ - Parent region (or `None` if root region) - """ - return self._parent - - -class ParameterError(Exception): - """ - Exception relating to a parameter. Used as super class. - """ - - -class ParameterLengthError(ParameterError): - """ - Exception raised when sequences in timeseries do not match run - size. - """ - - -class ParameterReadonlyError(ParameterError): - """ - Exception raised when a requested parameter is read-only. - - This can happen, for instance, if a parameter's parent parameter - in the parameter hierarchy has already been requested as writable. - """ - - -class ParameterTypeError(ParameterError): - """ - Exception raised when a parameter is of a different type than - requested (scalar or timeseries). - """ - - -class ParameterAggregatedError(ParameterError): - """ - Exception raised when a parameter has been read from (raised, e.g., when attempting - to create a child parameter). - """ - - -class ParameterWrittenError(ParameterError): - """ - Exception raised when a parameter has been written to (raised, e.g., when attempting - to create a child parameter). - """ - - -class RegionAggregatedError(Exception): - """ - Exception raised when a region has been read from in a region-aggregated way. - """ - - -class ParameterView: - """ - Generic view to a :ref:`parameter ` (scalar or timeseries). - """ - - _name: Tuple[str] - """:ref:`Hierarchical name `""" - - _region: Tuple[str] - """Hierarchical region name""" - - _unit: str - """Unit""" - - def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): - """ - Initialize. - - Parameters - ---------- - name - :ref:`Hierarchical name ` - region - Hierarchical region name - unit - Unit - """ - self._name = name - self._region = region - self._unit = unit - - @property - def name(self) -> Tuple[str]: - """ - :ref:`Hierarchical name ` - """ - return self._name - - @property - def region(self) -> Tuple[str]: - """ - Hierarchical region name - """ - return self._region - - @property - def unit(self) -> str: - """ - Unit - """ - return self._unit - - @property - def is_empty(self) -> bool: - """ - Check if parameter is empty, i.e. has not yet been written to. - """ - raise NotImplementedError - - -class ParameterInfo(ParameterView): - """ - Provides information about a :ref:`parameter `. - """ - - _type: ParameterType - """Parameter type""" - - def __init__(self, parameter: _Parameter): - """ - Initialize. - - Parameters - ---------- - parameter - Parameter - """ - self._type = parameter.parameter_type - - @property - def parameter_type(self) -> ParameterType: - """Parameter type""" - return self._type - - -class ScalarView(ParameterView): - """ - Read-only view of a scalar parameter. - """ - - def get(self) -> float: - """ - Get current value of scalar parameter. - """ - raise NotImplementedError - - -class WritableScalarView(ScalarView): - """ - View of a scalar parameter whose value can be changed. - """ - - def set(self, value: float) -> None: - """ - Set current value of scalar parameter. - - Parameters - ---------- - value - Value - """ - raise NotImplementedError - - -class TimeseriesView(ParameterView): - """ - Read-only :class:`ParameterView` of a timeseries. - """ - - def get_series(self) -> Sequence[float]: - """ - Get values of the full timeseries. - """ - raise NotImplementedError - - def get(self, index: int) -> float: - """ - Get value at a particular time. - - Parameters - ---------- - index - Time step index - - Raises - ------ - IndexError - ``time`` is out of run time range. - """ - raise NotImplementedError - - def length(self) -> int: - """ - Get length of time series. - """ - raise NotImplementedError - - -class WritableTimeseriesView(TimeseriesView): - """ - View of a timeseries whose values can be changed. - """ - - def set_series(self, values: Sequence[float]) -> None: - """ - Set value for whole time series. - - Parameters - ---------- - values - Values to set. The length of this sequence (list/1-D - array/...) of ``float`` values must equal size. - - Raises - ------ - ParameterLengthError - Length of ``values`` does not equal size. - """ - raise NotImplementedError - - def set(self, value: float, index: int) -> None: - """ - Set value for a particular time in the time series. - - Parameters - ---------- - value - Value - index - Time step index - - Raises - ------ - IndexError - ``index`` is out of range. - """ - raise NotImplementedError +from typing import Tuple +from .parameter_views import ( + ParameterInfo, + ScalarView, + WritableScalarView, + TimeseriesView, + WritableTimeseriesView, +) +from .parameters import _Parameter, ParameterType +from .regions import _Region class ParameterSet: @@ -549,7 +41,7 @@ def _get_or_create_region(self, name: Tuple[str]) -> _Region: Parameters ---------- name - Hierarchy name of the region + Hierarchical name of the region or ``()`` for "World". """ if len(name) > 0: p = self._get_or_create_region(name[:-1]) @@ -557,6 +49,17 @@ def _get_or_create_region(self, name: Tuple[str]) -> _Region: else: return self._world + def _get_region(self, name: Tuple[str]) -> _Region: + """ + Get a region or ``None`` if not found. + + Parameters + ---------- + name + Hierarchical name of the region or ``()`` for "World". + """ + return self._world.get_subregion(name) + def _get_or_create_parameter( self, name: Tuple[str], @@ -589,7 +92,7 @@ def _get_or_create_parameter( elif len(name) == 1: return region.get_or_create_parameter(name[0]) else: # len(name) == 0 - raise ValueError("No region name given") + raise ValueError("No parameter name given") def get_scalar_view( self, name: Tuple[str], region: Tuple[str], unit: str @@ -604,7 +107,7 @@ def get_scalar_view( name :ref:`Hierarchical name ` of the parameter region - Hierarchical region name + Hierarchical name of the region or ``()`` for "World". unit Unit for the values in the view @@ -630,7 +133,7 @@ def get_writable_scalar_view( name :ref:`Hierarchical name ` of the parameter region - Hierarchical region name + Hierarchical name of the region or ``()`` for "World". unit Unit for the values in the view @@ -666,7 +169,7 @@ def get_timeseries_view( name :ref:`Hierarchical name ` of the parameter region - Hierarchical region name + Hierarchical name of the region or ``()`` for "World". unit Unit for the values in the view start_time @@ -701,7 +204,7 @@ def get_writable_timeseries_view( name :ref:`Hierarchical name ` of the parameter region - Hierarchical region name + Hierarchical name of the region or ``()`` for "World". unit Unit for the values in the view start_time @@ -720,7 +223,7 @@ def get_writable_timeseries_view( """ raise NotImplementedError - def get_parameter_info(self, name: Tuple[str]) -> ParameterInfo: + def get_parameter_info(self, name: Tuple[str], region: Tuple[str]) -> ParameterInfo: """ Get information about a parameter. @@ -728,6 +231,8 @@ def get_parameter_info(self, name: Tuple[str]) -> ParameterInfo: ---------- name :ref:`Hierarchical name ` of the parameter + region + Hierarchical name of the region or ``()`` for "World". Raises ------ @@ -740,9 +245,14 @@ def get_parameter_info(self, name: Tuple[str]) -> ParameterInfo: Information about the parameter or ``None`` if the parameter has not been created yet. """ - raise NotImplementedError + region = self._get_region(region) + if region is not None: + parameter = region.get_parameter(name) + if parameter is not None: + return ParameterInfo(parameter) + return None - def has_parameter(self, name: Tuple[str]) -> bool: + def has_parameter(self, name: Tuple[str], region: Tuple[str]) -> bool: """ Query if parameter set has a specific parameter. @@ -750,13 +260,16 @@ def has_parameter(self, name: Tuple[str]) -> bool: ---------- name :ref:`Hierarchical name ` of the parameter + region + Hierarchical name of the region or ``()`` for "World". Raises ------ ValueError - Name not given + Name or region not given """ - raise NotImplementedError + region = self._get_region(region) + return region is not None and region.get_parameter(name) is not None class Core: @@ -837,7 +350,7 @@ def run(self) -> None: """ Run the model over the full time range. """ - raise NotImplementedError + self._model.run() @property def start_time(self) -> int: diff --git a/openscm/errors.py b/openscm/errors.py new file mode 100644 index 00000000..571be953 --- /dev/null +++ b/openscm/errors.py @@ -0,0 +1,47 @@ +class ParameterError(Exception): + """ + Exception relating to a parameter. Used as super class. + """ + + +class ParameterLengthError(ParameterError): + """ + Exception raised when sequences in timeseries do not match run + size. + """ + + +class ParameterReadonlyError(ParameterError): + """ + Exception raised when a requested parameter is read-only. + + This can happen, for instance, if a parameter's parent parameter + in the parameter hierarchy has already been requested as writable. + """ + + +class ParameterTypeError(ParameterError): + """ + Exception raised when a parameter is of a different type than + requested (scalar or timeseries). + """ + + +class ParameterAggregatedError(ParameterError): + """ + Exception raised when a parameter has been read from (raised, e.g., when attempting + to create a child parameter). + """ + + +class ParameterWrittenError(ParameterError): + """ + Exception raised when a parameter has been written to (raised, e.g., when attempting + to create a child parameter). + """ + + +class RegionAggregatedError(Exception): + """ + Exception raised when a region has been read from in a region-aggregated way. + """ diff --git a/openscm/parameter_views.py b/openscm/parameter_views.py new file mode 100644 index 00000000..7b8625e7 --- /dev/null +++ b/openscm/parameter_views.py @@ -0,0 +1,190 @@ +from typing import Sequence, Tuple +from .parameters import _Parameter, ParameterType + +class ParameterView: + """ + Generic view to a :ref:`parameter ` (scalar or timeseries). + """ + + _name: Tuple[str] + """:ref:`Hierarchical name `""" + + _region: Tuple[str] + """Hierarchical region name""" + + _unit: str + """Unit""" + + def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): + """ + Initialize. + + Parameters + ---------- + name + :ref:`Hierarchical name ` + region + Hierarchical region name + unit + Unit + """ + self._name = name + self._region = region + self._unit = unit + + @property + def name(self) -> Tuple[str]: + """ + :ref:`Hierarchical name ` + """ + return self._name + + @property + def region(self) -> Tuple[str]: + """ + Hierarchical region name + """ + return self._region + + @property + def unit(self) -> str: + """ + Unit + """ + return self._unit + + @property + def is_empty(self) -> bool: + """ + Check if parameter is empty, i.e. has not yet been written to. + """ + raise NotImplementedError + + +class ParameterInfo(ParameterView): + """ + Provides information about a :ref:`parameter `. + """ + + _type: ParameterType + """Parameter type""" + + def __init__(self, parameter: _Parameter): + """ + Initialize. + + Parameters + ---------- + parameter + Parameter + """ + self._type = parameter.parameter_type + + @property + def parameter_type(self) -> ParameterType: + """Parameter type""" + return self._type + + +class ScalarView(ParameterView): + """ + Read-only view of a scalar parameter. + """ + + def get(self) -> float: + """ + Get current value of scalar parameter. + """ + raise NotImplementedError + + +class WritableScalarView(ScalarView): + """ + View of a scalar parameter whose value can be changed. + """ + + def set(self, value: float) -> None: + """ + Set current value of scalar parameter. + + Parameters + ---------- + value + Value + """ + raise NotImplementedError + + +class TimeseriesView(ParameterView): + """ + Read-only :class:`ParameterView` of a timeseries. + """ + + def get_series(self) -> Sequence[float]: + """ + Get values of the full timeseries. + """ + raise NotImplementedError + + def get(self, index: int) -> float: + """ + Get value at a particular time. + + Parameters + ---------- + index + Time step index + + Raises + ------ + IndexError + ``time`` is out of run time range. + """ + raise NotImplementedError + + def length(self) -> int: + """ + Get length of time series. + """ + raise NotImplementedError + + +class WritableTimeseriesView(TimeseriesView): + """ + View of a timeseries whose values can be changed. + """ + + def set_series(self, values: Sequence[float]) -> None: + """ + Set value for whole time series. + + Parameters + ---------- + values + Values to set. The length of this sequence (list/1-D + array/...) of ``float`` values must equal size. + + Raises + ------ + ParameterLengthError + Length of ``values`` does not equal size. + """ + raise NotImplementedError + + def set(self, value: float, index: int) -> None: + """ + Set value for a particular time in the time series. + + Parameters + ---------- + value + Value + index + Time step index + + Raises + ------ + IndexError + ``index`` is out of range. + """ + raise NotImplementedError diff --git a/openscm/parameters.py b/openscm/parameters.py new file mode 100644 index 00000000..fbe2dd67 --- /dev/null +++ b/openscm/parameters.py @@ -0,0 +1,204 @@ +from enum import Enum +from typing import Any, Dict, Tuple +from .errors import ( + ParameterAggregatedError, + ParameterReadonlyError, + ParameterTypeError, + ParameterWrittenError, +) + + +class ParameterType(Enum): + """ + Parameter type. + """ + + SCALAR = 1 + TIMESERIES = 2 + + +class _Parameter: + """ + Represents a :ref:`parameter ` in the :ref:`parameter hierarchy + `. + """ + + _children: Dict[str, "_Parameter"] + """Child parameters""" + + _data: Any + """Data""" + + _has_been_aggregated: bool + """ + If True, parameter has already been read in an aggregated way, i.e., aggregating + over child parameters + """ + + _has_been_written_to: bool + """If True, parameter data has already been changed""" + + _name: str + """Name""" + + _parent: "_Parameter" + """Parent parameter""" + + _type: ParameterType + """Parameter type""" + + _unit: str + """Unit""" + + def __init__(self, name: str): + """ + Initialize. + + Parameters + ---------- + name + Name + """ + self._children = {} + self._has_been_aggregated = False + self._has_been_written_to = False + self._name = name + self._parent = None + self._type = None + self._unit = None + + def get_or_create_child_parameter( + self, name: str, unit: str, parameter_type: ParameterType + ) -> "_Parameter": + """ + Get a (direct) child parameter of this parameter. Create and add it if not + found. + + Parameters + ---------- + name + Name + unit + Unit + parameter_type + Parameter type + + Raises + ------ + ParameterAggregatedError + If the child paramater would need to be added, but this parameter has + already been read in an aggregated way. In this case a child parameter + cannot be added. + ParameterWrittenError + If the child paramater would need to be added, but this parameter has + already been written to. In this case a child parameter cannot be added. + """ + res = self._children.get(name, None) + if res is None: + if self._has_been_written_to: + raise ParameterWrittenError + if self._has_been_aggregated: + raise ParameterAggregatedError + res = _Parameter(name) + res._parent = self + res._type = parameter_type + res._unit = unit + self._children[name] = res + return res + + def get_subparameter(self, name: Tuple[str]) -> "_Parameter": + """ + Get a sub parameter of this parameter or ``None`` if not found. + + Parameters + ---------- + name + :ref:`Hierarchical name ` of the subparameter below this + parameter or ``()`` for this parameter + """ + if len(name) > 0: + res = self._children.get(name[0], None) + if res is not None: + res = res.get_subparameter(name[1:]) + return res + else: + return self + + def attempt_aggregate(self, parameter_type: ParameterType) -> None: + """ + Tell parameter that it will be read from in an aggregated way, i.e., aggregating + over child parameters. + + Parameters + ---------- + parameter_type + Parameter type to be read + + Raises + ------ + ParameterTypeError + If parameter has already been read from or written to in a different type. + """ + if self._type is not None and self._type != parameter_type: + raise ParameterTypeError + self._type = parameter_type + self._has_been_aggregated = True + + def attempt_write(self, parameter_type: ParameterType) -> None: + """ + Tell parameter that its data will be written to. + + Parameters + ---------- + parameter_type + Parameter type to be written + + Raises + ------ + ParameterReadonlyError + If parameter is read-only because it has child parameters. + """ + if self._children: + raise ParameterReadonlyError + self.attempt_aggregate(parameter_type) + self._has_been_written_to = True + + @property + def full_name(self) -> Tuple[str]: + """ + Full :ref:`hierarchical name ` + """ + p = self + r = [] + while p is not None: + r.append(p.name) + p = p._parent + return tuple(reversed(r)) + + @property + def name(self) -> str: + """ + Name + """ + return self._name + + @property + def parameter_type(self) -> ParameterType: + """ + Parameter type + """ + return self._type + + @property + def parent(self) -> "_Parameter": + """ + Parent parameter + """ + return self._parent + + @property + def unit(self) -> str: + """ + Unit + """ + return self._unit diff --git a/openscm/regions.py b/openscm/regions.py new file mode 100644 index 00000000..7342426b --- /dev/null +++ b/openscm/regions.py @@ -0,0 +1,141 @@ +from typing import Dict, Tuple +from .errors import RegionAggregatedError +from .parameters import _Parameter + +class _Region: + """ + Represents a region in the region hierarchy. + """ + + _children: Dict[str, "_Region"] + """Subregions""" + + _has_been_aggregated: bool + """ + If True, a parameter of this region has already been read in an aggregated way, + i.e., aggregating over subregions + """ + + _name: str + """Name""" + + _parameters: Dict[str, _Parameter] + """Parameters""" + + _parent: "_Region" + """Parent region (or `None` if root region)""" + + def __init__(self, name: str): + """ + Initialize + + Parameters + ---------- + name + Name + """ + self._name = name + self._children = {} + self._has_been_aggregated = False + self._parameters = {} + self._parent = None + + def get_or_create_subregion(self, name: str) -> "_Region": + """ + Get a (direct) subregion of this region. Create and add it if not found. + + Parameters + ---------- + name + Name + + Raises + ------ + RegionAggregatedError + If the subregion would need to be added and a parameter of this region has + already been read in an aggregated way. In this case a subregion cannot be + created. + """ + res = self._children.get(name, None) + if res is None: + if self._has_been_aggregated: + raise RegionAggregatedError + res = _Region(name) + res._parent = self + self._children[name] = res + return res + + def get_subregion(self, name: Tuple[str]) -> "_Region": + """ + Get a subregion of this region or ``None`` if not found. + + Parameters + ---------- + name + Hierarchical name of the region below this region or ``()`` for this region. + """ + if len(name) > 0: + res = self._children.get(name[0], None) + if res is not None: + res = res.get_subregion(name[1:]) + return res + else: + return self + + def get_or_create_parameter(self, name: str) -> _Parameter: + """ + Get a root parameter for this region. Create and add it if not found. + + Parameters + ---------- + name + Name + """ + res = self._parameters.get(name, None) + if res is None: + res = _Parameter(name) + self._parameters[name] = res + return res + + def get_parameter(self, name: Tuple[str]) -> _Parameter: + """ + Get a (root or sub-) parameter for this region or ``None`` if not found. + + Parameters + ---------- + name + :ref:`Hierarchical name ` of the parameter + + Raises + ------ + ValueError + Name not given + """ + if len(name) > 0: + root_parameter = self._parameters.get(name, None) + if root_parameter is not None and len(name) > 1: + return root_parameter.get_subparameter(name[1:]) + return root_parameter + else: + raise ValueError + + def attempt_aggregate(self) -> None: + """ + Tell region that one of its parameters will be read from in an aggregated way, + i.e., aggregating over subregions. + """ + self._has_been_aggregated = True + + @property + def name(self) -> str: + """ + Name + """ + return self._name + + @property + def parent(self) -> "_Region": + """ + Parent region (or ``None`` if root region) + """ + return self._parent diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index e670e996..7d8b4798 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -1,12 +1,11 @@ -from openscm.core import ( +from openscm.errors import ( ParameterAggregatedError, ParameterReadonlyError, - ParameterSet, - ParameterType, ParameterTypeError, ParameterWrittenError, RegionAggregatedError, ) +from openscm.core import ParameterSet, ParameterType import pytest @@ -33,7 +32,7 @@ def test_region(parameterset): def test_parameter(parameterset): region_ber = parameterset._get_or_create_region(("DEU", "BER")) - with pytest.raises(ValueError, match="No region name given"): + with pytest.raises(ValueError, match="No parameter name given"): parameterset._get_or_create_parameter( (), region_ber, "GtCO2/a", ParameterType.TIMESERIES ) From 8c4045e35b782d6b875eb05a079d9e934c65b6d2 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Wed, 19 Dec 2018 19:18:18 +0100 Subject: [PATCH 18/24] Add coverage makefile target --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 88618ccf..352af58b 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ venv: dev-requirements.txt setup.py test: | venv ./venv/bin/pytest -rfsxEX --cov=openscm tests +coverage: test + coverage html + test_all: test | venv ./venv/bin/pytest -rfsxEX --nbval ./notebooks --sanitize ./notebooks/tests_sanitize.cfg @@ -45,4 +48,4 @@ test-pypi-install: | venv clean: rm -rf venv -.PHONY: clean test test-all black flake8 docs publish-on-pypi test-pypi-install +.PHONY: clean coverage test test-all black flake8 docs publish-on-pypi test-pypi-install From f3946ed219a41cfe9a7f83274b735634e2672f1c Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Wed, 19 Dec 2018 19:18:55 +0100 Subject: [PATCH 19/24] Fix timeframes length estimation --- openscm/timeframes.py | 39 ++++++++++++++++++++++++++++------- tests/unit/test_timeframes.py | 8 ++++++- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/openscm/timeframes.py b/openscm/timeframes.py index 85df222a..feccd192 100644 --- a/openscm/timeframes.py +++ b/openscm/timeframes.py @@ -74,7 +74,7 @@ def get_points(self, count: int) -> np.ndarray: np.ndarray Array of time points """ - return np.linspace(self.start_time, self.get_stop_time(count), count) + return np.linspace(self.start_time, self.get_stop_time(count - 1), count) def get_stop_time(self, count: int) -> int: """ @@ -91,7 +91,7 @@ def get_stop_time(self, count: int) -> int: int Time point (seconds since ``1970-01-01 00:00:00``) """ - return self.start_time + (count - 1) * self.period_length + return self.start_time + count * self.period_length def get_length_until(self, stop_time: int) -> int: """ @@ -107,7 +107,7 @@ def get_length_until(self, stop_time: int) -> int: int Number of time points """ - return (stop_time - self.start_time) // self.period_length + 1 + return (stop_time + 1 - self.start_time) // self.period_length def _calc_linearization_values(values: np.ndarray) -> np.ndarray: @@ -257,8 +257,8 @@ def _calc_interpolation_points( first_target_point = target.start_time + target.period_length first_interval_length = source.start_time - target.start_time - source_stop_time = source.get_stop_time(linearization_points_len) - target_len = 1 + (source_stop_time - first_target_point) // target.period_length + source_stop_time = source.get_stop_time(linearization_points_len - 1) + target_len = (source_stop_time - first_target_point) // target.period_length target_stop_time = target.get_stop_time(target_len) if source_stop_time > target_stop_time: @@ -268,11 +268,14 @@ def _calc_interpolation_points( interpolation_points, indices = np.unique( np.concatenate( - (target.get_points(target_len), source.get_points(linearization_points_len)) + ( + target.get_points(target_len + 1), + source.get_points(linearization_points_len), + ) ), return_index=True, ) - target_indices = np.where(indices < target_len)[0] + target_indices = np.where(indices <= target_len)[0] if source_stop_time <= target_stop_time: target_indices = target_indices[:-1] @@ -488,6 +491,28 @@ def convert_to(self, values: np.ndarray) -> np.ndarray: ) return result + def get_source_len(self, target_len: int) -> int: + """ + Get length of timeseries in source timeframe. + + Parameters + ---------- + target_len + Length of timeseries in target timeframe. + """ + return self._source.get_length_until(self._target.get_stop_time(target_len)) + + def get_target_len(self, source_len: int) -> int: + """ + Get length of timeseries in target timeframe. + + Parameters + ---------- + source_len + Length of timeseries in source timeframe. + """ + return self._target.get_length_until(self._source.get_stop_time(source_len)) + @property def source(self) -> Timeframe: """ diff --git a/tests/unit/test_timeframes.py b/tests/unit/test_timeframes.py index 02c85f35..b2385eaa 100644 --- a/tests/unit/test_timeframes.py +++ b/tests/unit/test_timeframes.py @@ -106,17 +106,23 @@ def test_conversion(source, target, source_values_index): np.testing.assert_allclose(values, target_values) assert len(values) == target.get_length_until( source.get_stop_time(len(source_values)) - ) + (1 if target.start_time >= source.start_time else 0) + ) def test_timeframeconverter(source, target, source_values_index): source_values, target_values = get_test_values(source, target, source_values_index) if target_values is not None: timeframeconverter = timeframes.TimeframeConverter(source, target) + assert timeframeconverter.get_target_len(len(source_values)) == len( + target_values + ) values = timeframeconverter.convert_from(source_values) np.testing.assert_allclose(values, target_values) timeframeconverter = timeframes.TimeframeConverter(target, source) + assert timeframeconverter.get_source_len(len(source_values)) == len( + target_values + ) values = timeframeconverter.convert_to(source_values) np.testing.assert_allclose(values, target_values) From 63af23c55eee536eb07e2a9b86045c0a9be5ddb3 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Wed, 19 Dec 2018 20:13:55 +0100 Subject: [PATCH 20/24] Finish basic functionality --- openscm/core.py | 78 +++++++--------- openscm/errors.py | 9 +- openscm/parameter_views.py | 127 ++++++++++++-------------- openscm/parameters.py | 180 ++++++++++++++++++++++++------------- openscm/regions.py | 28 ++++-- openscm/units.py | 1 + tests/unit/test_core.py | 163 +++++++++++++++++++++++++-------- 7 files changed, 356 insertions(+), 230 deletions(-) diff --git a/openscm/core.py b/openscm/core.py index b359167e..8d43ecdd 100644 --- a/openscm/core.py +++ b/openscm/core.py @@ -10,14 +10,14 @@ from typing import Tuple from .parameter_views import ( - ParameterInfo, ScalarView, WritableScalarView, TimeseriesView, WritableTimeseriesView, ) -from .parameters import _Parameter, ParameterType +from .parameters import _Parameter, ParameterInfo, ParameterType from .regions import _Region +from .timeframes import Timeframe class ParameterSet: @@ -60,13 +60,7 @@ def _get_region(self, name: Tuple[str]) -> _Region: """ return self._world.get_subregion(name) - def _get_or_create_parameter( - self, - name: Tuple[str], - region: _Region, - unit: str, - parameter_type: ParameterType, - ) -> _Parameter: + def _get_or_create_parameter(self, name: Tuple[str], region: _Region) -> _Parameter: """ Get a parameter. Create and add it if not found. @@ -76,10 +70,6 @@ def _get_or_create_parameter( :ref:`Hierarchical name ` of the parameter region Region - unit - Unit for the values in the view - parameter_type - Parameter type Raises ------ @@ -87,8 +77,8 @@ def _get_or_create_parameter( Name not given """ if len(name) > 1: - p = self._get_or_create_parameter(name[:-1], region, unit, parameter_type) - return p.get_or_create_child_parameter(name[-1], unit, parameter_type) + p = self._get_or_create_parameter(name[:-1], region) + return p.get_or_create_child_parameter(name[-1]) elif len(name) == 1: return region.get_or_create_parameter(name[0]) else: # len(name) == 0 @@ -118,7 +108,11 @@ def get_scalar_view( ValueError Name not given or invalid region """ - raise NotImplementedError + parameter = self._get_or_create_parameter( + name, self._get_or_create_region(region) + ) + parameter.attempt_read(unit, ParameterType.SCALAR) + return ScalarView(parameter, unit) def get_writable_scalar_view( self, name: Tuple[str], region: Tuple[str], unit: str @@ -146,7 +140,11 @@ def get_writable_scalar_view( ValueError Name not given or invalid region """ - raise NotImplementedError + parameter = self._get_or_create_parameter( + name, self._get_or_create_region(region) + ) + parameter.attempt_write(unit, ParameterType.SCALAR) + return WritableScalarView(parameter, unit) def get_timeseries_view( self, @@ -184,7 +182,12 @@ def get_timeseries_view( ValueError Name not given or invalid region """ - raise NotImplementedError + parameter = self._get_or_create_parameter( + name, self._get_or_create_region(region) + ) + timeframe = Timeframe(start_time, period_length) + parameter.attempt_read(unit, ParameterType.TIMESERIES, timeframe) + return TimeseriesView(parameter, unit, timeframe) def get_writable_timeseries_view( self, @@ -221,11 +224,16 @@ def get_writable_timeseries_view( ValueError Name not given or invalid region """ - raise NotImplementedError + parameter = self._get_or_create_parameter( + name, self._get_or_create_region(region) + ) + timeframe = Timeframe(start_time, period_length) + parameter.attempt_write(unit, ParameterType.TIMESERIES, timeframe) + return WritableTimeseriesView(parameter, unit, timeframe) def get_parameter_info(self, name: Tuple[str], region: Tuple[str]) -> ParameterInfo: """ - Get information about a parameter. + Get a parameter or ``None`` if not found. Parameters ---------- @@ -241,36 +249,16 @@ def get_parameter_info(self, name: Tuple[str], region: Tuple[str]) -> ParameterI Returns ------- - ParameterInfo - Information about the parameter or ``None`` if the parameter has not been - created yet. + _Parameter + Parameter or ``None`` if the parameter has not been created yet. """ region = self._get_region(region) if region is not None: parameter = region.get_parameter(name) if parameter is not None: - return ParameterInfo(parameter) + return parameter.info return None - def has_parameter(self, name: Tuple[str], region: Tuple[str]) -> bool: - """ - Query if parameter set has a specific parameter. - - Parameters - ---------- - name - :ref:`Hierarchical name ` of the parameter - region - Hierarchical name of the region or ``()`` for "World". - - Raises - ------ - ValueError - Name or region not given - """ - region = self._get_region(region) - return region is not None and region.get_parameter(name) is not None - class Core: """ @@ -344,13 +332,13 @@ def parameters(self) -> ParameterSet: """ Set of parameters for the run """ - return self._parameterset + return self._parameters def run(self) -> None: """ Run the model over the full time range. """ - self._model.run() + raise NotImplementedError @property def start_time(self) -> int: diff --git a/openscm/errors.py b/openscm/errors.py index 571be953..0b459dc2 100644 --- a/openscm/errors.py +++ b/openscm/errors.py @@ -4,13 +4,6 @@ class ParameterError(Exception): """ -class ParameterLengthError(ParameterError): - """ - Exception raised when sequences in timeseries do not match run - size. - """ - - class ParameterReadonlyError(ParameterError): """ Exception raised when a requested parameter is read-only. @@ -27,7 +20,7 @@ class ParameterTypeError(ParameterError): """ -class ParameterAggregatedError(ParameterError): +class ParameterReadError(ParameterError): """ Exception raised when a parameter has been read from (raised, e.g., when attempting to create a child parameter). diff --git a/openscm/parameter_views.py b/openscm/parameter_views.py index 7b8625e7..ee13745d 100644 --- a/openscm/parameter_views.py +++ b/openscm/parameter_views.py @@ -1,75 +1,46 @@ from typing import Sequence, Tuple +from .errors import ParameterTypeError from .parameters import _Parameter, ParameterType +from .timeframes import Timeframe, TimeframeConverter +from .units import UnitConverter + class ParameterView: """ Generic view to a :ref:`parameter ` (scalar or timeseries). """ - _name: Tuple[str] - """:ref:`Hierarchical name `""" - - _region: Tuple[str] - """Hierarchical region name""" + _parameter: _Parameter + """Parameter""" - _unit: str - """Unit""" - - def __init__(self, name: Tuple[str], region: Tuple[str], unit: str): + def __init__(self, parameter: _Parameter): """ Initialize. Parameters ---------- - name - :ref:`Hierarchical name ` - region - Hierarchical region name - unit - Unit - """ - self._name = name - self._region = region - self._unit = unit - - @property - def name(self) -> Tuple[str]: - """ - :ref:`Hierarchical name ` - """ - return self._name - - @property - def region(self) -> Tuple[str]: - """ - Hierarchical region name - """ - return self._region - - @property - def unit(self) -> str: - """ - Unit + parameter + Parameter """ - return self._unit + self._parameter = parameter @property def is_empty(self) -> bool: """ Check if parameter is empty, i.e. has not yet been written to. """ - raise NotImplementedError + return not self._parameter._has_been_written_to -class ParameterInfo(ParameterView): +class ScalarView(ParameterView): """ - Provides information about a :ref:`parameter `. + Read-only view of a scalar parameter. """ - _type: ParameterType - """Parameter type""" + _unit_converter: UnitConverter + """Unit converter""" - def __init__(self, parameter: _Parameter): + def __init__(self, parameter: _Parameter, unit: str): """ Initialize. @@ -77,25 +48,17 @@ def __init__(self, parameter: _Parameter): ---------- parameter Parameter + unit + Unit for the values in the view """ - self._type = parameter.parameter_type - - @property - def parameter_type(self) -> ParameterType: - """Parameter type""" - return self._type - - -class ScalarView(ParameterView): - """ - Read-only view of a scalar parameter. - """ + super().__init__(parameter) + self._unit_converter = UnitConverter(parameter._info._unit, unit) def get(self) -> float: """ Get current value of scalar parameter. """ - raise NotImplementedError + return self._unit_converter.convert_from(self._parameter._data) class WritableScalarView(ScalarView): @@ -112,7 +75,7 @@ def set(self, value: float) -> None: value Value """ - raise NotImplementedError + self._parameter._data = self._unit_converter.convert_to(value) class TimeseriesView(ParameterView): @@ -120,11 +83,38 @@ class TimeseriesView(ParameterView): Read-only :class:`ParameterView` of a timeseries. """ + _timeframe_converter: TimeframeConverter + """Timeframe converter""" + + _unit_converter: UnitConverter + """Unit converter""" + + def __init__(self, parameter: _Parameter, unit: str, timeframe: Timeframe): + """ + Initialize. + + Parameters + ---------- + parameter + Parameter + unit + Unit for the values in the view + timeframe + Timeframe + """ + super().__init__(parameter) + self._unit_converter = UnitConverter(parameter._info._unit, unit) + self._timeframe_converter = TimeframeConverter( + parameter._info._timeframe, timeframe + ) + def get_series(self) -> Sequence[float]: """ Get values of the full timeseries. """ - raise NotImplementedError + return self._timeframe_converter.convert_from( + self._unit_converter.convert_from(self._parameter._data) + ) def get(self, index: int) -> float: """ @@ -142,11 +132,12 @@ def get(self, index: int) -> float: """ raise NotImplementedError + @property def length(self) -> int: """ - Get length of time series. + Length of timeseries. """ - raise NotImplementedError + return self._timeframe_converter.get_target_len(len(self._parameter._data)) class WritableTimeseriesView(TimeseriesView): @@ -161,15 +152,11 @@ def set_series(self, values: Sequence[float]) -> None: Parameters ---------- values - Values to set. The length of this sequence (list/1-D - array/...) of ``float`` values must equal size. - - Raises - ------ - ParameterLengthError - Length of ``values`` does not equal size. + Values to set. """ - raise NotImplementedError + self._parameter._data = self._timeframe_converter.convert_to( + self._unit_converter.convert_to(values) + ) def set(self, value: float, index: int) -> None: """ diff --git a/openscm/parameters.py b/openscm/parameters.py index fbe2dd67..a01ee908 100644 --- a/openscm/parameters.py +++ b/openscm/parameters.py @@ -1,11 +1,14 @@ +from copy import copy from enum import Enum +import numpy as np from typing import Any, Dict, Tuple from .errors import ( - ParameterAggregatedError, + ParameterReadError, ParameterReadonlyError, ParameterTypeError, ParameterWrittenError, ) +from .timeframes import Timeframe class ParameterType(Enum): @@ -17,6 +20,72 @@ class ParameterType(Enum): TIMESERIES = 2 +class ParameterInfo: + """ + Information for a :ref:`parameter `. + """ + + _name: str + """Name""" + + _region: "_Region" + """Region this parameter belongs to""" + + _timeframe: Timeframe + """Timeframe; only for timeseries parameters""" + + _type: ParameterType + """Parameter type""" + + _unit: str + """Unit""" + + def __init__(self, name: str, region: "_Region"): + """ + Initialize. + + Parameters + ---------- + name + Name + region + Region + """ + self._name = name + self._region = region + self._timeframe = None + self._type = None + self._unit = None + + @property + def name(self) -> str: + """ + Name + """ + return self._name + + @property + def parameter_type(self) -> ParameterType: + """ + Parameter type + """ + return self._type + + @property + def region(self) -> Tuple[str]: + """ + Hierarchichal name of the region this parameter belongs to + """ + return self._region.full_name + + @property + def unit(self) -> str: + """ + Unit + """ + return self._unit + + class _Parameter: """ Represents a :ref:`parameter ` in the :ref:`parameter hierarchy @@ -29,28 +98,19 @@ class _Parameter: _data: Any """Data""" - _has_been_aggregated: bool - """ - If True, parameter has already been read in an aggregated way, i.e., aggregating - over child parameters - """ + _has_been_read_from: bool + """If True, parameter has already been read from""" _has_been_written_to: bool """If True, parameter data has already been changed""" - _name: str - """Name""" + _info: ParameterInfo + """Information about the parameter""" _parent: "_Parameter" """Parent parameter""" - _type: ParameterType - """Parameter type""" - - _unit: str - """Unit""" - - def __init__(self, name: str): + def __init__(self, name: str, region: "_Region"): """ Initialize. @@ -60,16 +120,12 @@ def __init__(self, name: str): Name """ self._children = {} - self._has_been_aggregated = False + self._has_been_read_from = False self._has_been_written_to = False - self._name = name + self._info = ParameterInfo(name, region) self._parent = None - self._type = None - self._unit = None - def get_or_create_child_parameter( - self, name: str, unit: str, parameter_type: ParameterType - ) -> "_Parameter": + def get_or_create_child_parameter(self, name: str) -> "_Parameter": """ Get a (direct) child parameter of this parameter. Create and add it if not found. @@ -79,30 +135,27 @@ def get_or_create_child_parameter( name Name unit - Unit + Unit for the parameter if it is going to be created parameter_type - Parameter type + Parameter type if it is going to be created Raises ------ - ParameterAggregatedError - If the child paramater would need to be added, but this parameter has - already been read in an aggregated way. In this case a child parameter - cannot be added. + ParameterReadError + If the child parameter would need to be added, but this parameter has + already been read from. In this case a child parameter cannot be added. ParameterWrittenError - If the child paramater would need to be added, but this parameter has + If the child parameter would need to be added, but this parameter has already been written to. In this case a child parameter cannot be added. """ res = self._children.get(name, None) if res is None: if self._has_been_written_to: raise ParameterWrittenError - if self._has_been_aggregated: - raise ParameterAggregatedError - res = _Parameter(name) + if self._has_been_read_from: + raise ParameterReadError + res = _Parameter(name, self._info._region) res._parent = self - res._type = parameter_type - res._unit = unit self._children[name] = res return res @@ -124,34 +177,55 @@ def get_subparameter(self, name: Tuple[str]) -> "_Parameter": else: return self - def attempt_aggregate(self, parameter_type: ParameterType) -> None: + def attempt_read( + self, unit: str, parameter_type: ParameterType, timeframe: Timeframe = None + ) -> None: """ - Tell parameter that it will be read from in an aggregated way, i.e., aggregating - over child parameters. + Tell parameter that it will be read from. If the parameter has child parameters + it will be read in in an aggregated way, i.e., aggregating over child + parameters. Parameters ---------- + unit + Unit to be read parameter_type Parameter type to be read + timeframe + Timeframe; only for timeseries parameters Raises ------ ParameterTypeError If parameter has already been read from or written to in a different type. """ - if self._type is not None and self._type != parameter_type: + # TODO aggregate + if self._info._type is not None and self._info._type != parameter_type: raise ParameterTypeError - self._type = parameter_type - self._has_been_aggregated = True + if self._info._unit is None: + self._info._unit = unit + self._info._type = parameter_type + if parameter_type == ParameterType.SCALAR: + self._data = float("NaN") + else: # parameter_type == ParameterType.TIMESERIES + self._data = np.array([]) + self._info._timeframe = copy(timeframe) + self._has_been_read_from = True - def attempt_write(self, parameter_type: ParameterType) -> None: + def attempt_write( + self, unit: str, parameter_type: ParameterType, timeframe: Timeframe = None + ) -> None: """ Tell parameter that its data will be written to. Parameters ---------- + unit + Unit to be written parameter_type Parameter type to be written + timeframe + Timeframe; only for timeseries parameters Raises ------ @@ -160,7 +234,7 @@ def attempt_write(self, parameter_type: ParameterType) -> None: """ if self._children: raise ParameterReadonlyError - self.attempt_aggregate(parameter_type) + self.attempt_read(unit, parameter_type, timeframe) self._has_been_written_to = True @property @@ -171,23 +245,16 @@ def full_name(self) -> Tuple[str]: p = self r = [] while p is not None: - r.append(p.name) + r.append(p._info._name) p = p._parent return tuple(reversed(r)) @property - def name(self) -> str: + def info(self) -> ParameterInfo: """ - Name + Parameter information """ - return self._name - - @property - def parameter_type(self) -> ParameterType: - """ - Parameter type - """ - return self._type + return self._info @property def parent(self) -> "_Parameter": @@ -195,10 +262,3 @@ def parent(self) -> "_Parameter": Parent parameter """ return self._parent - - @property - def unit(self) -> str: - """ - Unit - """ - return self._unit diff --git a/openscm/regions.py b/openscm/regions.py index 7342426b..1fa96453 100644 --- a/openscm/regions.py +++ b/openscm/regions.py @@ -2,6 +2,7 @@ from .errors import RegionAggregatedError from .parameters import _Parameter + class _Region: """ Represents a region in the region hierarchy. @@ -93,7 +94,7 @@ def get_or_create_parameter(self, name: str) -> _Parameter: """ res = self._parameters.get(name, None) if res is None: - res = _Parameter(name) + res = _Parameter(name, self) self._parameters[name] = res return res @@ -111,13 +112,12 @@ def get_parameter(self, name: Tuple[str]) -> _Parameter: ValueError Name not given """ - if len(name) > 0: - root_parameter = self._parameters.get(name, None) - if root_parameter is not None and len(name) > 1: - return root_parameter.get_subparameter(name[1:]) - return root_parameter - else: - raise ValueError + if name is None or len(name) == 0: + raise ValueError("No parameter name given") + root_parameter = self._parameters.get(name[0], None) + if root_parameter is not None and len(name) > 1: + return root_parameter.get_subparameter(name[1:]) + return root_parameter def attempt_aggregate(self) -> None: """ @@ -126,6 +126,18 @@ def attempt_aggregate(self) -> None: """ self._has_been_aggregated = True + @property + def full_name(self) -> Tuple[str]: + """ + Full hierarchical name + """ + p = self + r = [] + while p._parent is not None: + r.append(p.name) + p = p._parent + return tuple(reversed(r)) + @property def name(self) -> str: """ diff --git a/openscm/units.py b/openscm/units.py index 35b63a50..1b05b000 100644 --- a/openscm/units.py +++ b/openscm/units.py @@ -209,6 +209,7 @@ def _add_gases_to_unit_registry(unit_registry, gases): unit_registry.define("a = 1 * year = annum = yr") unit_registry.define("h = hour") +unit_registry.define("d = day") unit_registry.define("degreeC = degC") unit_registry.define("degreeF = degF") unit_registry.define("kt = 1000 * t") # since kt is used for "knot" in the defaults diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 7d8b4798..f3eaaf7e 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -1,26 +1,53 @@ +import numpy as np +from math import isnan +from openscm.core import Core, ParameterSet, ParameterType from openscm.errors import ( - ParameterAggregatedError, + ParameterReadError, ParameterReadonlyError, ParameterTypeError, ParameterWrittenError, RegionAggregatedError, ) -from openscm.core import ParameterSet, ParameterType +from openscm.units import DimensionalityError import pytest @pytest.fixture -def parameterset(): - res = ParameterSet() - res._get_or_create_region(("DEU", "BER")) - return res +def model(): + return "DICE" -def test_region(parameterset): +@pytest.fixture +def start_time(): + return 30 * 365 * 24 * 3600 + + +@pytest.fixture +def end_time(): + return 130 * 365 * 24 * 3600 + + +@pytest.fixture +def core(model, start_time, end_time): + core = Core(model, start_time, end_time) + core.parameters._get_or_create_region(("DEU", "BER")) + return core + + +def test_core(core, model, start_time, end_time): + assert core.start_time == start_time + assert core.end_time == end_time + assert core.model == model + + +def test_region(core): + parameterset = core.parameters region_deu = parameterset._get_or_create_region(("DEU",)) + assert region_deu.full_name == ("DEU",) assert region_deu.name == "DEU" region_ber = parameterset._get_or_create_region(("DEU", "BER")) + assert region_ber.full_name == ("DEU", "BER") assert region_ber.name == "BER" assert region_ber.parent == region_deu @@ -29,61 +56,119 @@ def test_region(parameterset): parameterset._get_or_create_region(("DEU", "BRB")) -def test_parameter(parameterset): +def test_parameter(core): + parameterset = core.parameters region_ber = parameterset._get_or_create_region(("DEU", "BER")) with pytest.raises(ValueError, match="No parameter name given"): - parameterset._get_or_create_parameter( - (), region_ber, "GtCO2/a", ParameterType.TIMESERIES - ) + parameterset._get_or_create_parameter((), region_ber) - param_co2 = parameterset._get_or_create_parameter( - ("Emissions", "CO2"), region_ber, "GtCO2/a", ParameterType.TIMESERIES - ) + param_co2 = parameterset._get_or_create_parameter(("Emissions", "CO2"), region_ber) + assert param_co2.get_subparameter(()) == param_co2 + assert param_co2.parent.get_subparameter(("CO2",)) == param_co2 + assert region_ber.get_parameter(("Emissions", "CO2")) == param_co2 assert param_co2.full_name == ("Emissions", "CO2") - assert param_co2.name == "CO2" + assert param_co2.info.region == ("DEU", "BER") + assert param_co2.info.name == "CO2" + assert ( + parameterset.get_parameter_info(("Emissions", "CO2"), ("DEU", "BER")) + == param_co2.info + ) + assert ( + parameterset.get_parameter_info(("Emissions",), ("DEU", "BER")) + == param_co2.parent.info + ) + assert parameterset.get_parameter_info(("Emissions", "NOx"), ("DEU", "BER")) is None + assert parameterset.get_parameter_info(("Emissions",), ("DEU", "BRB")) is None + + with pytest.raises(ValueError, match="No parameter name given"): + parameterset.get_parameter_info(None, ("DEU", "BER")) + with pytest.raises(ValueError, match="No parameter name given"): + parameterset.get_parameter_info((), ("DEU", "BER")) param_emissions = param_co2.parent assert param_emissions.full_name == ("Emissions",) - assert param_emissions.name == "Emissions" + assert param_emissions.info.name == "Emissions" # Before any read/write attempt these should be None: - assert param_emissions.parameter_type is None - assert param_emissions.unit is None + assert param_emissions.info.parameter_type is None + assert param_emissions.info.unit is None param_industry = parameterset._get_or_create_parameter( - ("Emissions", "CO2", "Industry"), - region_ber, - "GtCO2/a", - ParameterType.TIMESERIES, + ("Emissions", "CO2", "Industry"), region_ber ) assert param_industry.full_name == ("Emissions", "CO2", "Industry") - assert param_industry.name == "Industry" - assert param_industry.parameter_type == ParameterType.TIMESERIES - assert param_industry.unit == "GtCO2/a" + assert param_industry.info.name == "Industry" + + param_industry.attempt_read("GtCO2/a", ParameterType.TIMESERIES) + assert param_industry.info.parameter_type == ParameterType.TIMESERIES + assert param_industry.info.unit == "GtCO2/a" with pytest.raises(ParameterReadonlyError): - param_co2.attempt_write(ParameterType.TIMESERIES) + param_co2.attempt_write("GtCO2/a", ParameterType.TIMESERIES) + param_co2.attempt_read("GtCO2/a", ParameterType.TIMESERIES) with pytest.raises(ParameterTypeError): - param_co2.attempt_aggregate(ParameterType.SCALAR) + param_co2.attempt_read("GtCO2/a", ParameterType.SCALAR) - param_co2.attempt_aggregate(ParameterType.TIMESERIES) - with pytest.raises(ParameterAggregatedError): + with pytest.raises(ParameterReadError): parameterset._get_or_create_parameter( - ("Emissions", "CO2", "Landuse"), - region_ber, - "GtCO2/a", - ParameterType.TIMESERIES, + ("Emissions", "CO2", "Landuse"), region_ber ) with pytest.raises(ParameterTypeError): - param_industry.attempt_write(ParameterType.SCALAR) + param_industry.attempt_write("GtCO2/a", ParameterType.SCALAR) - param_industry.attempt_write(ParameterType.TIMESERIES) + param_industry.attempt_write("GtCO2/a", ParameterType.TIMESERIES) with pytest.raises(ParameterWrittenError): parameterset._get_or_create_parameter( - ("Emissions", "CO2", "Industry", "Other"), - region_ber, - "GtCO2/a", - ParameterType.TIMESERIES, + ("Emissions", "CO2", "Industry", "Other"), region_ber ) + + +def test_scalar_parameter_view(core): + parameterset = core.parameters + cs = parameterset.get_scalar_view(("Climate Sensitivity"), (), "degC") + assert isnan(cs.get()) + assert cs.is_empty + cs_writable = parameterset.get_writable_scalar_view( + ("Climate Sensitivity"), (), "degF" + ) + cs_writable.set(68) + assert cs_writable.get() == 68 + assert not cs.is_empty + np.testing.assert_allclose(cs.get(), 20) + with pytest.raises(ParameterTypeError): + parameterset.get_timeseries_view(("Climate Sensitivity"), (), "degC", 0, 1) + with pytest.raises(DimensionalityError): + parameterset.get_scalar_view(("Climate Sensitivity"), (), "kg") + + +@pytest.fixture( + params=[ + (range(5 * 365), [0.24373829, 0.7325541, 1.22136991, 1.71018572, 2.19900153]), + ([1] * 5 * 365, [365 * 44 / 12 / 1e6] * 5), + ] +) +def series(request): + return np.array(request.param[0]), np.array(request.param[1]) + + +def test_timeseries_parameter_view(core, start_time, series): + parameterset = core.parameters + carbon = parameterset.get_timeseries_view( + ("Emissions", "CO2"), (), "GtCO2/a", start_time, 365 * 24 * 3600 + ) + carbon_writable = parameterset.get_writable_timeseries_view( + ("Emissions", "CO2"), (), "ktC/d", start_time, 24 * 3600 + ) + inseries = series[0] + outseries = series[1] + carbon_writable.set_series(inseries) + assert carbon_writable.length == len(inseries) + np.testing.assert_allclose(carbon_writable.get_series(), inseries) + assert carbon.length == 5 + np.testing.assert_allclose(carbon.get_series(), outseries, rtol=1e-3) + with pytest.raises(ParameterTypeError): + parameterset.get_scalar_view(("Emissions", "CO2"), (), "GtCO2/a") + with pytest.raises(DimensionalityError): + parameterset.get_timeseries_view(("Emissions", "CO2"), (), "kg", 0, 1) From 11bedf0feaf5f09894be4e07deda2aff82a81665 Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Fri, 21 Dec 2018 18:34:10 +0100 Subject: [PATCH 21/24] Fix unused imports --- openscm/parameter_views.py | 5 ++--- tests/unit/test_core.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openscm/parameter_views.py b/openscm/parameter_views.py index ee13745d..d1a16bc7 100644 --- a/openscm/parameter_views.py +++ b/openscm/parameter_views.py @@ -1,6 +1,5 @@ -from typing import Sequence, Tuple -from .errors import ParameterTypeError -from .parameters import _Parameter, ParameterType +from typing import Sequence +from .parameters import _Parameter from .timeframes import Timeframe, TimeframeConverter from .units import UnitConverter diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index f3eaaf7e..fb6267ca 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -1,6 +1,6 @@ import numpy as np from math import isnan -from openscm.core import Core, ParameterSet, ParameterType +from openscm.core import Core, ParameterType from openscm.errors import ( ParameterReadError, ParameterReadonlyError, From f21365310f6de4cb2340b1774c11c792a7b3bb5a Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 22 Dec 2018 13:06:52 +0100 Subject: [PATCH 22/24] Clarify skip in docs configuration --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1dfd1fc6..3f00d9f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -182,14 +182,14 @@ ] -def skip(app, what, name, obj, skip, options): +def skip_init(app, what, name, obj, skip, options): if name == "__init__": return False return skip def setup(app): - app.connect("autodoc-skip-member", skip) + app.connect("autodoc-skip-member", skip_init) # -- Extension configuration ------------------------------------------------- From 49c95aac390123a15abc061a65793fc069d05de4 Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Sat, 22 Dec 2018 13:08:57 +0100 Subject: [PATCH 23/24] Update openscm/errors.py Co-Authored-By: swillner --- openscm/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openscm/errors.py b/openscm/errors.py index 0b459dc2..01e1467a 100644 --- a/openscm/errors.py +++ b/openscm/errors.py @@ -36,5 +36,5 @@ class ParameterWrittenError(ParameterError): class RegionAggregatedError(Exception): """ - Exception raised when a region has been read from in a region-aggregated way. + Exception raised when a region has already been read from in a region-aggregated way. """ From 5f76ab7c71838611ecefc49aeb04326f3b7322fc Mon Sep 17 00:00:00 2001 From: Sven Willner Date: Sat, 22 Dec 2018 13:12:20 +0100 Subject: [PATCH 24/24] Adjust notebook to added unit d --- notebooks/emissions-units-with-pint.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/notebooks/emissions-units-with-pint.ipynb b/notebooks/emissions-units-with-pint.ipynb index 93e2cbcb..19c7909c 100644 --- a/notebooks/emissions-units-with-pint.ipynb +++ b/notebooks/emissions-units-with-pint.ipynb @@ -303,6 +303,7 @@ " 'cup',\n", " 'curie',\n", " 'cycle',\n", + " 'd',\n", " 'dalton',\n", " 'darcy',\n", " 'day',\n",