diff --git a/examples/example1.ipynb b/examples/example1.ipynb index 983e0a5..f2e42c1 100644 --- a/examples/example1.ipynb +++ b/examples/example1.ipynb @@ -60,7 +60,7 @@ "source": [ "time = Dimension(name='Time', letter='t', items=list(range(1980,2011)))\n", "elements = Dimension(name='Elements', letter='e', items=['single material', ])\n", - "dimensions = DimensionSet(dimensions=[time, elements])\n", + "dimensions = DimensionSet(dim_list=[time, elements])\n", "\n", "parameters = {\n", " 'D': Parameter(name='inflow', dims=dimensions, values=np.arange(0, 31).reshape(31, 1)),\n", @@ -140,35 +140,6 @@ "execution_count": 6, "metadata": {}, "outputs": [ - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { "application/vnd.plotly.v1+json": { @@ -262,7 +233,6 @@ } ], "layout": { - "autosize": true, "legend": { "tracegroupgap": 0 }, @@ -1087,65 +1057,26 @@ }, "xaxis": { "anchor": "y", - "autorange": true, "domain": [ 0, 1 ], - "range": [ - 1980, - 2010 - ], "title": { "text": "Year" - }, - "type": "linear" + } }, "yaxis": { "anchor": "x", - "autorange": true, "domain": [ 0, 1 ], - "range": [ - -1.6666666666666665, - 31.666666666666668 - ], "title": { "text": "Mt/yr" - }, - "type": "linear" + } } } - }, - "text/html": [ - "
" - ] + } }, "metadata": {}, "output_type": "display_data" @@ -1260,7 +1191,6 @@ } ], "layout": { - "autosize": true, "legend": { "tracegroupgap": 0 }, @@ -2085,65 +2015,26 @@ }, "xaxis": { "anchor": "y", - "autorange": true, "domain": [ 0, 1 ], - "range": [ - 1980, - 2010 - ], "title": { "text": "Year" - }, - "type": "linear" + } }, "yaxis": { "anchor": "x", - "autorange": true, "domain": [ 0, 1 ], - "range": [ - -28.33333333333333, - 538.3333333333333 - ], "title": { "text": "Mt/yr" - }, - "type": "linear" + } } } - }, - "text/html": [ - "
" - ] + } }, "metadata": {}, "output_type": "display_data" @@ -2184,7 +2075,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/examples/example2.ipynb b/examples/example2.ipynb index 86e5105..2432be7 100644 --- a/examples/example2.ipynb +++ b/examples/example2.ipynb @@ -289,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 13, "id": "e5877592", "metadata": {}, "outputs": [ @@ -299,7 +299,7 @@ "Text(0.5, 0.98, 'Amount of copper and manganese in secondary steel')" ] }, - "execution_count": 15, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" }, @@ -332,7 +332,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 14, "id": "ebbe470a-ec04-4432-9f55-7a0931f9062f", "metadata": {}, "outputs": [ @@ -342,7 +342,7 @@ "Text(0.5, 0.98, 'Share of copper and manganese in secondary steel')" ] }, - "execution_count": 17, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "id": "5582ae7a-4d41-4a8b-8726-dff414c96cde", "metadata": {}, "outputs": [ @@ -406,7 +406,7 @@ "Text(0.5, 0.98, 'Manganese lost in the remelting process')" ] }, - "execution_count": 16, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" }, @@ -480,12 +480,14 @@ "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\jakobdu\\AppData\\Local\\Temp\\ipykernel_19240\\2953891482.py:20: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown\n", - " fig.show()\n" - ] + "data": { + "text/plain": [ + "Text(0.5, 0.98, 'Material concentration in secondary steel')" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" }, { "data": { @@ -506,7 +508,7 @@ "concentration_b = flow_b / flow_b.sum_nda_over(('e'))\n", "\n", "scenarios = Dimension(name='Scenarios', letter='s', items=['Standard', 'Updated shredder yield', 'Increased buildings demolition'])\n", - "new_dims = DimensionSet(dimensions=remelted_shares.dims.dimensions + [scenarios])\n", + "new_dims = remelted_shares.dims.expand_by([scenarios])\n", "concentrations = remelted_shares.cast_to(new_dims)\n", "concentrations['Updated shredder yield'] = concentration_a\n", "concentrations['Increased buildings demolition'] = concentration_b\n", diff --git a/sodym/data_reader.py b/sodym/data_reader.py index 241759c..017e11f 100644 --- a/sodym/data_reader.py +++ b/sodym/data_reader.py @@ -15,7 +15,7 @@ class DataReader(ABC): """ def read_dimensions(self, dimension_definitions: List[DimensionDefinition]) -> DimensionSet: dimensions = [self.read_dimension(definition) for definition in dimension_definitions] - return DimensionSet(dimensions=dimensions) + return DimensionSet(dim_list=dimensions) @abstractmethod def read_dimension(self, dimension_definition: DimensionDefinition) -> Dimension: diff --git a/sodym/dimensions.py b/sodym/dimensions.py index a684d07..8f34af8 100644 --- a/sodym/dimensions.py +++ b/sodym/dimensions.py @@ -57,7 +57,7 @@ class DimensionSet(PydanticBaseModel): """ - dimensions: list[Dimension] + dim_list: list[Dimension] @model_validator(mode='after') def no_repeated_dimensions(self): @@ -69,10 +69,10 @@ def no_repeated_dimensions(self): def drop(self, key: str, inplace: bool=False): dim_to_drop = self._dict[key] if not inplace: - dimensions = copy(self.dimensions) + dimensions = copy(self.dim_list) dimensions.remove(dim_to_drop) - return DimensionSet(dimensions=dimensions) - self.dimensions.remove(dim_to_drop) + return DimensionSet(dim_list=dimensions) + self.dim_list.remove(dim_to_drop) @property def _dict(self) -> Dict[str, Dimension]: @@ -80,18 +80,18 @@ def _dict(self) -> Dict[str, Dimension]: letter --> dim object and name --> dim object """ - return {dim.name: dim for dim in self.dimensions} | {dim.letter: dim for dim in self.dimensions} + return {dim.name: dim for dim in self.dim_list} | {dim.letter: dim for dim in self.dim_list} def __getitem__(self, key) -> Dimension: if isinstance(key, str): return self._dict[key] elif isinstance(key, int): - return self.dimensions[key] + return self.dim_list[key] else: raise TypeError("Key must be string or int") def __iter__(self): - return iter(self.dimensions) + return iter(self.dim_list) def size(self, key: str): return self._dict[key].len @@ -101,26 +101,41 @@ def shape(self, keys: tuple = None): return tuple(self.size(key) for key in keys) def get_subset(self, dims: tuple = None) -> 'DimensionSet': - """Selects :py:class:`Dimension` objects from the object attribute dimensions, + """Selects :py:class:`Dimension` objects from the object attribute dim_list, according to the dims passed, which can be either letters or names. Returns a copy if dims are not given. """ subset = copy(self) if dims is not None: - subset.dimensions = [self._dict[dim_key] for dim_key in dims] + subset.dim_list = [self._dict[dim_key] for dim_key in dims] return subset + def expand_by(self, added_dims: list[Dimension]) -> 'DimensionSet': + """Expands the DimensionSet by adding new dimensions to it. + """ + if not all([dim.letter not in self.letters for dim in added_dims]): + raise ValueError('DimensionSet already contains one or more of the dimensions to be added.') + return DimensionSet(dim_list=self.dim_list + added_dims) + + def intersect_with(self, other: 'DimensionSet') -> 'DimensionSet': + intersection_letters = [dim.letter for dim in self.dim_list if dim.letter in other.letters] + return self.get_subset(intersection_letters) + + def union_with(self, other: 'DimensionSet') -> 'DimensionSet': + added_dims = [dim for dim in other.dim_list if dim.letter not in self.letters] + return self.expand_by(added_dims) + @property def names(self): - return tuple([dim.name for dim in self.dimensions]) + return tuple([dim.name for dim in self.dim_list]) @property def letters(self): - return tuple([dim.letter for dim in self.dimensions]) + return tuple([dim.letter for dim in self.dim_list]) @property def string(self): return "".join(self.letters) def index(self, key): - return [d.letter for d in self.dimensions].index(key) + return [d.letter for d in self.dim_list].index(key) diff --git a/sodym/export/helper.py b/sodym/export/helper.py index 1228f72..95d3ef5 100644 --- a/sodym/export/helper.py +++ b/sodym/export/helper.py @@ -83,7 +83,7 @@ def fill_fig_ax(self): def get_x_array_like_value_array(self): if self.x_array is None: x_dim_obj = self.array.dims[self.intra_line_dim] - x_dimset = DimensionSet(dimensions=[x_dim_obj]) + x_dimset = DimensionSet(dim_list=[x_dim_obj]) self.x_array = NamedDimArray(dims=x_dimset, values=np.array(x_dim_obj.items), name=self.intra_line_dim) self.x_array = self.x_array.cast_to(self.array.dims) diff --git a/sodym/named_dim_array_helper.py b/sodym/named_dim_array_helper.py index c9ab45c..47e8b78 100644 --- a/sodym/named_dim_array_helper.py +++ b/sodym/named_dim_array_helper.py @@ -8,7 +8,7 @@ def named_dim_array_stack(named_dim_arrays: list[NamedDimArray], dimension: Dime Method can be applied to `NamedDimArray`s, `StockArray`s, `Parameter`s and `Flow`s. """ named_dim_array0 = named_dim_arrays[0] - extended_dimensions = DimensionSet(dimensions=named_dim_array0.dims.dimensions+[dimension]) + extended_dimensions = named_dim_array0.dims.expand_by([dimension]) extended = NamedDimArray(dims=extended_dimensions) for item, nda in zip(dimension.items, named_dim_arrays): extended[{dimension.letter: item}] = nda diff --git a/sodym/named_dim_arrays.py b/sodym/named_dim_arrays.py index b7b6393..2fed409 100644 --- a/sodym/named_dim_arrays.py +++ b/sodym/named_dim_arrays.py @@ -143,44 +143,29 @@ def _prepare_other(self, other): other = NamedDimArray(dims=self.dims, values=other * np.ones(self.shape)) return other - def intersect_dims_with(self, other): - matching_dims = [] - for dim in self.dims.dimensions: - if dim.letter in other.dims.letters: - matching_dims.append(dim) - return DimensionSet(dimensions=matching_dims) - - def union_dims_with(self, other): - all_dims = copy(self.dims.dimensions) - letters_self = self.dims.letters - for dim in other.dims.dimensions: - if dim.letter not in letters_self: - all_dims.append(dim) - return DimensionSet(dimensions=all_dims) - def __add__(self, other): other = self._prepare_other(other) - dims_out = self.intersect_dims_with(other) + dims_out = self.dims.intersect_with(other.dims) return NamedDimArray( dims=dims_out, values=self.sum_values_to(dims_out.letters) + other.sum_values_to(dims_out.letters) ) def __sub__(self, other): other = self._prepare_other(other) - dims_out = self.intersect_dims_with(other) + dims_out = self.dims.intersect_with(other.dims) return NamedDimArray( dims=dims_out, values=self.sum_values_to(dims_out.letters) - other.sum_values_to(dims_out.letters) ) def __mul__(self, other): other = self._prepare_other(other) - dims_out = self.union_dims_with(other) + dims_out = self.dims.union_with(other.dims) values_out = np.einsum(f"{self.dims.string},{other.dims.string}->{dims_out.string}", self.values, other.values) return NamedDimArray(dims=dims_out, values=values_out) def __truediv__(self, other): other = self._prepare_other(other) - dims_out = self.union_dims_with(other) + dims_out = self.dims.union_with(other.dims) values_out = np.einsum( f"{self.dims.string},{other.dims.string}->{dims_out.string}", self.values, 1.0 / other.values ) @@ -188,13 +173,13 @@ def __truediv__(self, other): def minimum(self, other): other = self._prepare_other(other) - dims_out = self.intersect_dims_with(other) + dims_out = self.dims.intersect_with(other.dims) values_out = np.minimum(self.sum_values_to(dims_out.letters), other.sum_values_to(dims_out.letters)) return NamedDimArray(dims=dims_out, values=values_out) def maximum(self, other): other = self._prepare_other(other) - dims_out = self.intersect_dims_with(other) + dims_out = self.dims.intersect_with(other.dims) values_out = np.maximum(self.sum_values_to(dims_out.letters), other.sum_values_to(dims_out.letters)) return NamedDimArray(dims=dims_out, values=values_out) @@ -302,7 +287,7 @@ def to_dict_single_item(self, item): "docstring." ) dict_out = None - for d in self.nda.dims.dimensions: + for d in self.nda.dims: if item in d.items: if dict_out is not None: raise ValueError( @@ -353,7 +338,7 @@ def to_nda(self) -> 'NamedDimArray': assert ( not self.has_dim_with_several_items ), "Cannot convert to NamedDimArray if there are dimensions with several items" - + return NamedDimArray(dims=self.dims, values=self.values_pointer, name=self.nda.name) def _init_ids(self): diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index 865cbdf..0763700 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -10,14 +10,14 @@ def test_validate_dimension_set(): {'name': 'time', 'letter': 't', 'items': [1990, 2000, 2010]}, {'name': 'place', 'letter': 'p', 'items': ['World', ]} ] - DimensionSet(dimensions=dimensions) + DimensionSet(dim_list=dimensions) # example with repeated dimension letters in DimensionSet dimensions.append( {'name': 'another_time', 'letter': 't', 'items': [2020, 2030]} ) with pytest.raises(ValidationError) as error_msg: - DimensionSet(dimensions=dimensions) + DimensionSet(dim_list=dimensions) assert 'letter' in str(error_msg.value) @@ -29,11 +29,11 @@ def test_get_subset(): material_dimension = {'name': 'material', 'letter': 'm', 'items': ['material_0', 'material_1']} parent_dimensions = subset_dimensions + [material_dimension] - dimension_set = DimensionSet(dimensions=parent_dimensions) + dimension_set = DimensionSet(dim_list=parent_dimensions) # example of subsetting the dimension set using dimension letters subset_from_letters = dimension_set.get_subset(dims=('t', 'p')) - assert subset_from_letters == DimensionSet(dimensions=subset_dimensions) + assert subset_from_letters == DimensionSet(dim_list=subset_dimensions) # example of subsetting the dimension set using dimension names subset_from_names = dimension_set.get_subset(dims=('time', 'place')) diff --git a/tests/test_named_dim_arrays.py b/tests/test_named_dim_arrays.py index b37c178..7cc7594 100644 --- a/tests/test_named_dim_arrays.py +++ b/tests/test_named_dim_arrays.py @@ -10,12 +10,12 @@ {'name': 'place', 'letter': 'p', 'items': ['Earth', 'Sun', 'Moon', 'Venus']}, {'name': 'time', 'letter': 't', 'items': [1990, 2000, 2010]}, ] -dims = DimensionSet(dimensions=dimensions) +dims = DimensionSet(dim_list=dimensions) values = np.random.rand(4, 3) numbers = NamedDimArray(name='two', dims=dims, values=values) animals = {'name': 'animal', 'letter': 'a', 'items': ['cat', 'mouse']} -dims_incl_animals = DimensionSet(dimensions=dimensions+[animals]) +dims_incl_animals = DimensionSet(dim_list=dimensions+[animals]) animal_values = np.random.rand(4, 3, 2) space_animals = NamedDimArray(name='space_animals', dims=dims_incl_animals, values=animal_values) @@ -25,7 +25,7 @@ def test_named_dim_array_validations(): {'name': 'place', 'letter': 'p', 'items': ['World', ]}, {'name': 'time', 'letter': 't', 'items': [1990, 2000, 2010]}, ] - dims = DimensionSet(dimensions=dimensions) + dims = DimensionSet(dim_list=dimensions) # example with values with the correct shape NamedDimArray(name='numbers', dims=dims, values=np.array([[1, 2, 3], ])) @@ -52,7 +52,7 @@ def test_cast_to(): assert_almost_equal(np.sum(casted_named_dim_array.values), 2 * np.sum(values)) # example with differently ordered dimensions - target_dims = DimensionSet(dimensions=[animals]+dimensions[::-1]) + target_dims = DimensionSet(dim_list=[animals]+dimensions[::-1]) casted_named_dim_array = numbers.cast_to(target_dims=target_dims) assert casted_named_dim_array.values.shape == (2, 3, 4) @@ -60,7 +60,7 @@ def test_cast_to(): def test_sum_nda_to(): # sum over one dimension summed_named_dim_array = space_animals.sum_nda_to(result_dims=('p', 't')) - assert summed_named_dim_array.dims == DimensionSet(dimensions=dimensions) + assert summed_named_dim_array.dims == DimensionSet(dim_list=dimensions) assert_array_almost_equal(summed_named_dim_array.values, np.sum(animal_values, axis=2)) # sum over two dimensions