From 00efcc41d8a7df3fa275be29fcbc16008e79e1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 12 Jun 2024 22:19:44 +0200 Subject: [PATCH 1/2] Remove cellular modelling stub The approach was experimental in incomplete, also did not allow to have more then two levels. The same models can be created without code in solph. --- docs/examples/cellular.rst | 7 - docs/examples/index.rst | 1 - docs/usage.rst | 124 ------------- docs/whatsnew/v0-5-3.rst | 2 + examples/cellular/cellular.py | 331 ---------------------------------- src/oemof/solph/_models.py | 49 +---- tests/test_models.py | 58 ------ 7 files changed, 12 insertions(+), 560 deletions(-) delete mode 100644 docs/examples/cellular.rst delete mode 100644 examples/cellular/cellular.py diff --git a/docs/examples/cellular.rst b/docs/examples/cellular.rst deleted file mode 100644 index 27ae45942..000000000 --- a/docs/examples/cellular.rst +++ /dev/null @@ -1,7 +0,0 @@ -Cellular energy system ----------------------- - -Building your system from smaller cells -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: cellular.cellular diff --git a/docs/examples/index.rst b/docs/examples/index.rst index d5c4c6813..6ff1fdfa3 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -11,7 +11,6 @@ Examples time_index.rst nametuple.rst simple_dispatch.rst - cellular.rst activity_costs.rst min_max_runtimes.rst startup_costs.rst diff --git a/docs/usage.rst b/docs/usage.rst index cb25937d9..eabcde5ba 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1282,130 +1282,6 @@ Besides the `invest` variable, new variables are introduced as well. These are: monthly periods, but you would need to be very careful in parameterizing your energy system and your model and also, this would mean monthly discounting (if applicable) as well as specifying your plants lifetimes in months. -Modelling cellular energy systems and modularizing energy system models ------------------------------------------------------------------------ - -The cellular approach is a concept proposed by the [VDE-ETG](https://shop.vde.com/en/vde-study-the-cellular-approach). It is -related to smart-grids and multi-microgrid systems but extends both. The idea is to group the components of an energy system -into a hierarchically aggregating structure of cells. For example, the sources, sinks, storages and converters of a household -could be a single cell. Then a group of physically neighboring households could form another cell, consisting of household-cells. -This behaviour can be scaled up. The real game-changer in the cellular approach is the way the cells are operated, which will -not be covered here. Here, we focus on the way such cellular energy systems can be modeled. - -So far, the implementation in solph is just a neat way to group different parts of a larger energy system into cells. However, -the implementation can also be regarded as a precursor for further functionality. Decomposition techniques such -as [Benders](https://en.wikipedia.org/wiki/Benders_decomposition) or -[Dantzig-Wolfe](https://en.wikipedia.org/wiki/Dantzig%E2%80%93Wolfe_decomposition) could be implemented in solph. These methods -are dependent on a special constraint matrix structure, which the cellular modelling approach presented here is helping to obtain. - -Modelling procedure -^^^^^^^^^^^^^^^^^^^ - -Similar to the creation of regular energy systems, the creation of energy cells is the first step in model creation. Essentially, -each energy cell is just an energy system, therefore we use the class :py:class:`oemof.solph.EnergySystem` to create energy cells. - -.. code-block:: python - - from oemof.solph import EnergySystem - - es = EnergySystem( - label="es", timeindex=timeindex, infer_last_interval=False - ) - ec_1 = EnergySystem( - label="ec_1", timeindex=timeindex, infer_last_interval=False - ) - ec_2 = EnergySystem( - label="ec_2", timeindex=timeindex, inver_last_interval=False - ) - -Now we can go on and add components to the energy cells just like we do with regular energy systems. - -.. code-block:: python - - from oemof import solph - - bus_el_es = solph.buses.Bus(label="bus_el_es") - es.add(bus_el_es) - - bus_el_ec_1 = solph.buses.Bus(label="bus_el_ec_1") - sink_el_ec_1 = solph.components.Sink( - label="sink_el_ec_1", - inputs={bus_el_ec_1: flows.Flow(fix=10, nominal_value=1)}, - ) - source_el_ec_1 = solph.components.Source( - label="source_el_ec_1", - outputs={ - bus_el_ec_1: flows.Flow( - max=30, nominal_value=1, variable_costs=10, - ), - }, - ) - ec_1.add(bus_el_ec_1, sink_el_ec_1, source_el_ec_1) - -.. note:: This is just an exemplary piece of code. A (little bit more interesting) working - example can be found in the examples. - -The next step would be to model the connections between cells. Here, we resort to the class -:py:class:`oemof.solph.components.Link`. Each connection Link has two inputs (from the -"parent cell" and the "child cell") and two outputs (to the "parent cell" and the "child -cell"). A connection between the "parent cell" `es` and the "child cell" `ec_1` could look -like this: - -.. code-block:: python - - connector_el_ec_1 = solph.buses.Bus( - label="connector_el_ec_1", - inputs={ - bus_el_es: flows.Flow(), - bus_el_ec_1: flows.Flow(), - }, - outputs={ - bus_el_es: flows.Flow(), - bus_el_ec_1: flows.Flow(), - }, - conversion_factors={ - (bus_el_es, bus_el_ec_1): 0.85, - (bus_el_ec_1, bus_el_es): 0.85 - } - ) - es.add(connector_el_ec_1) - -The `conversion_factors` can be used to model transmission losses. Here, a symmetrical -loss of 15% is assumed. -All connection Links are added to the upmost energy cell. - -.. note:: Note that we do not add the energy cells as components to their parent cells! - Instead, the hierarchical structure is flattened and all connections between the cells - are created as depicted above. - -The last step is to create (and solve) the model. Again, this is fairly similar to the -regular model creation. However, instead of passing just one instance of -:py:class:`oemof.solph.EnergySystem`, a list of energy systems is passed. - -.. warning:: - By convention the first element of the list is assumed to be the upmost energy cell. - The ordering afterwards does not play a role. - -.. note:: The resulting model is monolithic. This means that all components of all energy - cells are actually grouped into one pyomo model. It would, therefore, also be possible - to model all the components in one :py:class:`oemof.solph.EnergySystem` instance and - the results would be identical. - -.. code-block:: python - - cmodel = Model( - energysystem=[es, ec_1, ec_2, ...] - ) - cmodel.solve() - -As pointed out above, the resulting model is monolithic. Nonetheless, this modelling approach -holds some benefits: - -* Better overview through segmentation of the energy system -* (Facilitated) opportunity to model cellular energy systems where the energy exchanged between cells - is of interest -* Segmentation of the energy system is a necessary precursor for distributed optimization via Dantzig-Wolfe - Mixed Integer (Linear) Problems ------------------------------- diff --git a/docs/whatsnew/v0-5-3.rst b/docs/whatsnew/v0-5-3.rst index 06a9530da..8c91fdd8c 100644 --- a/docs/whatsnew/v0-5-3.rst +++ b/docs/whatsnew/v0-5-3.rst @@ -25,6 +25,7 @@ Other changes ############# * Unified (usage) documentation for `OffsetConverter` +* Remove approach to model cellular systes (was more confusing than it helped) Known issues ############ @@ -34,3 +35,4 @@ Contributors * Lennart Schürmann * Richard Keil +* Patrik Schönfeldt diff --git a/examples/cellular/cellular.py b/examples/cellular/cellular.py deleted file mode 100644 index caa7c48d8..000000000 --- a/examples/cellular/cellular.py +++ /dev/null @@ -1,331 +0,0 @@ -""" -General description -------------------- - -Cellular energy systems are proposed by the VDE-ETG. They are, as the name -implies, energy systems that consist of cells. Each cell can contain -multiple other cells. So there is a hierarchy between them. - -However, the hierarchical levels are abstracted here and the structure is flat. - -The connections between the cells are modelled as Links. Each connector Link -has two inputs (buses of the parent and child cell) and two outputs (buses of -the parent and child cell). Losses can be modelled by using the -`conversion_factors` of the Link class. - -Code ----- -Download source code: :download:`cellular.py ` - -.. dropdown:: Click to display code - - .. literalinclude:: /../examples/cellular/cellular.py - :language: python - :lines: 45- - -Installation requirements -------------------------- - -This example requires at least oemof.solph (v0.5.1), install by: - -.. code:: bash - - pip install oemof.solph[examples] - -Licence -------- - -Lennart Schürmann - -`MIT license `_ - -""" - -from oemof.solph import EnergySystem -from oemof.solph import Model -from oemof.solph import buses -from oemof.solph import components as cmp -from oemof.solph import create_time_index -from oemof.solph import flows -from oemof.solph import processing -from oemof.solph import views - - -def main(): - ########################################################################### - # define the cells of the cellular energy system - ########################################################################### - # define necessary parameters - - n_periods = 3 - - daterange = create_time_index(year=2020, interval=1, number=n_periods) - - mysolver = "cbc" - - # create the energy cells - es = EnergySystem(timeindex=daterange, infer_last_interval=False) - ec_1 = EnergySystem(timeindex=daterange, infer_last_interval=False) - ec_2 = EnergySystem(timeindex=daterange, infer_last_interval=False) - ec_1_1 = EnergySystem(timeindex=daterange, infer_last_interval=False) - ec_1_2 = EnergySystem(timeindex=daterange, infer_last_interval=False) - ec_2_1 = EnergySystem(timeindex=daterange, infer_last_interval=False) - ec_2_2 = EnergySystem(timeindex=daterange, infer_last_interval=False) - - demand_1 = [10] * n_periods - demand_2 = [10] * n_periods - demand_1_1 = [10] * n_periods - demand_1_2 = [10] * n_periods - demand_2_1 = [10] * n_periods - demand_2_2 = [80] * n_periods - - pv_1 = [10] * n_periods - pv_2 = [10] * n_periods - pv_1_1 = [80] * n_periods - pv_1_2 = [40] * n_periods - pv_2_1 = [10] * n_periods - pv_2_2 = [10] * n_periods - - bus_el_es = buses.Bus(label="bus_el_es") - bus_el_ec_1 = buses.Bus(label="bus_el_ec_1") - bus_el_ec_2 = buses.Bus(label="bus_el_ec_2") - bus_el_ec_1_1 = buses.Bus(label="bus_el_ec_1_1") - bus_el_ec_1_2 = buses.Bus(label="bus_el_ec_1_2") - bus_el_ec_2_1 = buses.Bus(label="bus_el_ec_2_1") - bus_el_ec_2_2 = buses.Bus(label="bus_el_ec_2_2") - - es.add(bus_el_es) - ec_1.add(bus_el_ec_1) - ec_2.add(bus_el_ec_2) - ec_1_1.add(bus_el_ec_1_1) - ec_1_2.add(bus_el_ec_1_2) - ec_2_1.add(bus_el_ec_2_1) - ec_2_2.add(bus_el_ec_2_2) - - sink_el_ec_1 = cmp.Sink( - label="sink_el_ec_1", - inputs={bus_el_ec_1: flows.Flow(fix=demand_1, nominal_value=1)}, - ) - sink_el_ec_2 = cmp.Sink( - label="sink_el_ec_2", - inputs={bus_el_ec_2: flows.Flow(fix=demand_2, nominal_value=1)}, - ) - sink_el_ec_1_1 = cmp.Sink( - label="sink_el_ec_1_1", - inputs={bus_el_ec_1_1: flows.Flow(fix=demand_1_1, nominal_value=1)}, - ) - sink_el_ec_1_2 = cmp.Sink( - label="sink_el_ec_1_2", - inputs={bus_el_ec_1_2: flows.Flow(fix=demand_1_2, nominal_value=1)}, - ) - sink_el_ec_2_1 = cmp.Sink( - label="sink_el_ec_2_1", - inputs={bus_el_ec_2_1: flows.Flow(fix=demand_2_1, nominal_value=1)}, - ) - sink_el_ec_2_2 = cmp.Sink( - label="sink_el_ec_2_2", - inputs={bus_el_ec_2_2: flows.Flow(fix=demand_2_2, nominal_value=1)}, - ) - - ec_1.add(sink_el_ec_1) - ec_2.add(sink_el_ec_2) - ec_1_1.add(sink_el_ec_1_1) - ec_1_2.add(sink_el_ec_1_2) - ec_2_1.add(sink_el_ec_2_1) - ec_2_2.add(sink_el_ec_2_2) - - source_el_ec_1 = cmp.Source( - label="source_el_ec_1", - outputs={ - bus_el_ec_1: flows.Flow( - max=pv_1, nominal_value=1, variable_costs=5 - ) - }, - ) - source_el_ec_2 = cmp.Source( - label="source_el_ec_2", - outputs={ - bus_el_ec_2: flows.Flow( - max=pv_2, nominal_value=1, variable_costs=5 - ) - }, - ) - source_el_ec_1_1 = cmp.Source( - label="source_el_ec_1_1", - outputs={ - bus_el_ec_1_1: flows.Flow( - max=pv_1_1, nominal_value=1, variable_costs=10 - ) - }, - ) - source_el_ec_1_2 = cmp.Source( - label="source_el_ec_1_2", - outputs={ - bus_el_ec_1_2: flows.Flow( - max=pv_1_2, nominal_value=1, variable_costs=1 - ) - }, - ) - source_el_ec_2_1 = cmp.Source( - label="source_el_ec_2_1", - outputs={ - bus_el_ec_2_1: flows.Flow( - max=pv_2_1, nominal_value=1, variable_costs=5 - ) - }, - ) - source_el_ec_2_2 = cmp.Source( - label="source_el_ec_2_2", - outputs={ - bus_el_ec_2_2: flows.Flow( - max=pv_2_2, nominal_value=1, variable_costs=5 - ) - }, - ) - - ec_1.add(source_el_ec_1) - ec_2.add(source_el_ec_2) - ec_1_1.add(source_el_ec_1_1) - ec_1_2.add(source_el_ec_1_2) - ec_2_1.add(source_el_ec_2_1) - ec_2_2.add(source_el_ec_2_2) - - connector_el_ec_1 = cmp.Link( - label="connector_el_ec_1", - inputs={ - bus_el_es: flows.Flow(), - bus_el_ec_1: flows.Flow(), - }, - outputs={ - bus_el_es: flows.Flow(), - bus_el_ec_1: flows.Flow(), - }, - conversion_factors={ - (bus_el_es, bus_el_ec_1): 1, - (bus_el_ec_1, bus_el_es): 1, - }, - ) - - connector_el_ec_2 = cmp.Link( - label="connector_el_ec_2", - inputs={ - bus_el_es: flows.Flow(), - bus_el_ec_2: flows.Flow(), - }, - outputs={ - bus_el_es: flows.Flow(), - bus_el_ec_2: flows.Flow(), - }, - conversion_factors={ - (bus_el_es, bus_el_ec_2): 1, - (bus_el_ec_2, bus_el_es): 1, - }, - ) - - connector_el_ec_1_1 = cmp.Link( - label="connector_el_ec_1_1", - inputs={ - bus_el_ec_1: flows.Flow(), - bus_el_ec_1_1: flows.Flow(), - }, - outputs={ - bus_el_ec_1: flows.Flow(), - bus_el_ec_1_1: flows.Flow(), - }, - conversion_factors={ - (bus_el_ec_1, bus_el_ec_1_1): 0.85, - (bus_el_ec_1_1, bus_el_ec_1): 0.85, - }, - ) - - connector_el_ec_1_2 = cmp.Link( - label="connector_el_ec_1_2", - inputs={ - bus_el_ec_1: flows.Flow(), - bus_el_ec_1_2: flows.Flow(), - }, - outputs={ - bus_el_ec_1: flows.Flow(), - bus_el_ec_1_2: flows.Flow(), - }, - conversion_factors={ - (bus_el_ec_1, bus_el_ec_1_2): 1, - (bus_el_ec_1_2, bus_el_ec_1): 1, - }, - ) - - connector_el_ec_2_1 = cmp.Link( - label="connector_el_ec_2_1", - inputs={ - bus_el_ec_2: flows.Flow(), - bus_el_ec_2_1: flows.Flow(), - }, - outputs={ - bus_el_ec_2: flows.Flow(), - bus_el_ec_2_1: flows.Flow(), - }, - conversion_factors={ - (bus_el_ec_2, bus_el_ec_2_1): 1, - (bus_el_ec_2_1, bus_el_ec_2): 1, - }, - ) - - connector_el_ec_2_2 = cmp.Link( - label="connector_el_ec_2_2", - inputs={ - bus_el_ec_2: flows.Flow(), - bus_el_ec_2_2: flows.Flow(), - }, - outputs={ - bus_el_ec_2: flows.Flow(), - bus_el_ec_2_2: flows.Flow(), - }, - conversion_factors={ - (bus_el_ec_2, bus_el_ec_2_2): 1, - (bus_el_ec_2_2, bus_el_ec_2): 1, - }, - ) - - # the connectors are all part of the overarching, the upmost es - es.add( - connector_el_ec_1, - connector_el_ec_2, - connector_el_ec_1_1, - connector_el_ec_1_2, - connector_el_ec_2_1, - connector_el_ec_2_2, - ) - - cmodel = Model( - energysystem=[es, ec_1, ec_2, ec_1_1, ec_1_2, ec_2_1, ec_2_2] - ) - - cmodel.solve(solver=mysolver) - - # evaluate and plot the results - results = processing.results(cmodel) - - print(views.node(results, "bus_el_ec_1")["sequences"].iloc[0, :]) - msg = ( - "\nAs we can see, a flow of 70 kW is going from the bus_el_ec_1 " - "to the connector_el_ec_1. It is composed of 30 kW from " - "connector_el_ec_1_2 (and therefore ec_1_2) and 40 kW from " - "connector_el_ec_1_1 (and therefore ec_1_1). Where does it go?\n" - ) - print(msg) - print(views.node(results, "bus_el_ec_2_2")["sequences"].iloc[0, :]) - msg = ( - "\nIt is going into bus_el_ec_2_2 (and therefore ec_2_2), to " - "supply the demand.\n" - ) - print(msg) - - msg = ( - "Here we can see that the conversion factors are in fact considered:\n" - ) - print(msg) - print(views.node(results, "connector_el_ec_1_1")["sequences"].iloc[0, :]) - - -if __name__ == "__main__": - main() diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 59ab74188..a0e8e8ec7 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -45,12 +45,8 @@ class BaseModel(po.ConcreteModel): Parameters ---------- - energysystem : EnergySystem object or (experimental) list + energysystem : EnergySystem object Object that holds the nodes of an oemof energy system graph. - Experimental: If a list is passed, the list needs to hold EnergySystem - objects and a cellular structure is assumed. In this case, the first - element needs to be the upmost energy cell (structurally containing - all other cells). constraint_groups : list (optional) Solph looks for these groups in the given energy system and uses them to create the constraints of the optimization problem. @@ -76,7 +72,7 @@ class BaseModel(po.ConcreteModel): name : str Name of the model es : solph.EnergySystem - Energy system of the model (upmost energy cell for cellular structures) + Energy system of the model meta : `pyomo.opt.results.results_.SolverResults` or None Solver results dual : `pyomo.core.base.suffix.Suffix` or None @@ -115,13 +111,8 @@ def __init__(self, energysystem, **kwargs): # ######################## Arguments ################################# self.name = kwargs.get("name", type(self).__name__) - self.is_cellular = isinstance(energysystem, list) - if self.is_cellular: - self.es = energysystem[0] - self.ec = energysystem[1:] - else: - self.es = energysystem + self.es = energysystem self.timeincrement = kwargs.get("timeincrement", self.es.timeincrement) self.objective_weighting = kwargs.get( @@ -132,27 +123,14 @@ def __init__(self, energysystem, **kwargs): "constraint_groups", [] ) - if self.is_cellular: - for es in energysystem: - self._constraint_groups += [ - i - for i in es.groups - if hasattr(i, "CONSTRAINT_GROUP") - and i not in self._constraint_groups - ] - else: - self._constraint_groups += [ - i - for i in self.es.groups - if hasattr(i, "CONSTRAINT_GROUP") - and i not in self._constraint_groups - ] + self._constraint_groups += [ + i + for i in self.es.groups + if hasattr(i, "CONSTRAINT_GROUP") + and i not in self._constraint_groups + ] self.flows = self.es.flows() - if self.is_cellular: - for cell in self.ec: - for io, f in cell.flows().items(): - self.flows.update({io: f}) self.solver_results = None self.dual = None @@ -306,10 +284,6 @@ class Model(BaseModel): ---------- energysystem : EnergySystem object or (experimental) list Object that holds the nodes of an oemof energy system graph. - Experimental: If a list is passed, the list needs to hold EnergySystem - objects and a cellular structure is assumed. In this case, the first - element needs to be the upmost energy cell (structurally containing - all other cells). constraint_groups : list Solph looks for these groups in the given energy system and uses them to create the constraints of the optimization problem. @@ -403,10 +377,7 @@ def _add_parent_block_sets(self): Also create sets PERIODS and TIMEINDEX used for multi-period models. """ self.nodes = list(self.es.nodes) - if self.is_cellular: - # collect all nodes from the child cells - for cell in self.ec: - self.nodes.extend(cell.nodes) + # create set with all nodes self.NODES = po.Set(initialize=[n for n in self.nodes]) diff --git a/tests/test_models.py b/tests/test_models.py index 95086ba5c..3f3e05d52 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -108,61 +108,3 @@ def test_multi_period_default_discount_rate(): with warnings.catch_warnings(record=True) as w: solph.Model(es) assert msg in str(w[0].message) - - -def test_cellular_structure_detection(): - """Test flag creation if list is passed as energysystem to model""" - timeindex = pd.date_range(start="2020-01-01", periods=1, freq="h") - es = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - ec_1 = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - ec_2 = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - m = solph.Model(energysystem=[es, ec_1, ec_2]) - assert m.is_cellular - - -def test_sub_cell_node_consideration(): - """ - Test if the nodes of sub-cells are considered for cellular - energysystems. - """ - timeindex = pd.date_range(start="2020-01-01", periods=1, freq="h") - es = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - ec_1 = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - bus_es = solph.buses.Bus(label="bus_es") - bus_ec_1 = solph.buses.Bus(label="bus_ec_1") - es.add(bus_es) - ec_1.add(bus_ec_1) - m = solph.Model(energysystem=[es, ec_1]) - assert bus_ec_1 in m.nodes - - -def test_sub_cell_flow_consideration(): - """ - Test if the flows of sub-cells are considered for cellular - energysystems. - """ - timeindex = pd.date_range(start="2020-01-01", periods=1, freq="h") - es = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - ec_1 = solph.EnergySystem(timeindex=timeindex, infer_last_interval=True) - bus_es = solph.buses.Bus(label="bus_es") - bus_ec_1 = solph.buses.Bus(label="bus_ec_1") - es.add(bus_es) - ec_1.add(bus_ec_1) - - connector_ec_1 = solph.buses.Bus( - label="connector_ec_1", - inputs={ - bus_es: solph.flows.Flow(), - bus_ec_1: solph.flows.Flow(), - }, - outputs={ - bus_es: solph.flows.Flow(), - bus_ec_1: solph.flows.Flow(), - }, - ) - es.add(connector_ec_1) - - test_flow = [io for io in ec_1.flows().keys()][0] - - m = solph.Model(energysystem=[es, ec_1]) - assert test_flow in m.FLOWS From 45262f5eefdcb2f42e1ac4828d6855f12238d637 Mon Sep 17 00:00:00 2001 From: lens <80690396+lensum@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:17:22 +0200 Subject: [PATCH 2/2] fixed typo in whatsnew\v0-5-3.rst --- docs/whatsnew/v0-5-3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whatsnew/v0-5-3.rst b/docs/whatsnew/v0-5-3.rst index 8c91fdd8c..53aa21dca 100644 --- a/docs/whatsnew/v0-5-3.rst +++ b/docs/whatsnew/v0-5-3.rst @@ -25,7 +25,7 @@ Other changes ############# * Unified (usage) documentation for `OffsetConverter` -* Remove approach to model cellular systes (was more confusing than it helped) +* Remove approach to model cellular systems (was more confusing than it helped) Known issues ############