From d299345ddf673e22aee1eeafa2db01d0cd76a329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 12 Aug 2024 13:00:39 +0200 Subject: [PATCH 01/10] Replace _Sequence by _FakeSequence The auto-stretch feature of the _Sequence contained hidden magic, as it tracked the last "entry" that has been accessed and pretended to have a length based on this. So, outcomes depend on something that is assumed to be a read access. Also, self.data was erived from UserList but never really supported all of the expected functionality in a consistent way. The new _FakeSequence is a lot simpler. In particular it has an explicit (fake) length that needs to be set before casting to (stretched) lists. The default length is still set, because there is an entry, and to allow to directly iterate over a _FakeSequence, e.g. in sum(). --- src/oemof/solph/_plumbing.py | 60 +++++++++---------- .../solph/components/_generic_storage.py | 2 +- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index bf18dbdae..e6456c2e8 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -47,8 +47,6 @@ def sequence(iterable_or_scalar): >>> x[10] 10 - >>> print(x) - [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] """ if isinstance(iterable_or_scalar, abc.Iterable) and not isinstance( @@ -56,55 +54,55 @@ def sequence(iterable_or_scalar): ): return np.array(iterable_or_scalar) else: - return _Sequence(default=iterable_or_scalar) + return _FakeSequence(value=iterable_or_scalar) -class _Sequence(UserList): +class _FakeSequence: """Emulates a list whose length is not known in advance. Parameters ---------- - source: - default: + value : scalar + length : integer Examples -------- - >>> s = _Sequence(default=42) - >>> len(s) - 1 - >>> s[1] - 42 - >>> s[2] - 42 - >>> len(s) - 3 + >>> s = _FakeSequence(value=42, length=5) >>> s - [42, 42, 42] - >>> s[8] + [42, 42, 42, 42, 42] + >>> s = _FakeSequence(value=42) + >>> # undefined lenght, access still works + >>> s[1337] 42 - - """ - def __init__(self, *args, **kwargs): - self.default = kwargs["default"] - self.default_changed = False - self.highest_index = 0 - super().__init__(*args) + def __init__(self, value, length=1): + self._value = value + self._length = length + + @property + def length(self): + return self._length - def __getitem__(self, key): - self.highest_index = max(self.highest_index, key) - return self.default + @length.setter + def length(self, value): + self._length = value - def __init_list(self): - self.data = [self.default] * (self.highest_index + 1) + def __getitem__(self, _): + return self._value def __repr__(self): return str([i for i in self]) def __len__(self): - return max(len(self.data), self.highest_index + 1) + return self._length def __iter__(self): - return repeat(self.default, self.highest_index + 1) + return repeat(self._value, self._length) + + def to_numpy(self, length=None): + if length is not None: + return np.full(length, self._value) + else: + return np.full(len(self), self._value) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index ab8141789..b2c32fdfc 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -1127,7 +1127,7 @@ def _create(self, group=None): "For a multi-period investment model, fixed absolute" " losses are not supported. Please remove parameter." ) - if n.fixed_losses_absolute.default != 0: + if n.fixed_losses_absolute[0] != 0: raise ValueError(error_fixed_absolute_losses) error_initial_storage_level = ( "For a multi-period model, initial_storage_level is" From 398e62d813d999834e2096093b1d6c094faf74ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 12 Aug 2024 19:31:08 +0200 Subject: [PATCH 02/10] Let default length of _FakeSequence be None To allow this, some checks need to more explicit, now. As solph.sequence will return a _FakeSequence or a numpy.array, other members of _FakeSequence mimic the API of numpy.array. In particular, this allows min/max/sum operations in constant time. --- src/oemof/solph/_plumbing.py | 32 ++++++++++++++++--- .../solph/components/_generic_storage.py | 4 +-- .../solph/components/_offset_converter.py | 15 ++++----- src/oemof/solph/flows/_flow.py | 2 +- .../solph/flows/_investment_flow_block.py | 6 +--- .../solph/flows/_non_convex_flow_block.py | 4 +-- tests/test_plumbing.py | 14 ++++---- .../test_variable_chp/test_variable_chp.py | 5 ++- 8 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index e6456c2e8..4404471dd 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -52,6 +52,7 @@ def sequence(iterable_or_scalar): if isinstance(iterable_or_scalar, abc.Iterable) and not isinstance( iterable_or_scalar, str ): + return np.array(iterable_or_scalar) else: return _FakeSequence(value=iterable_or_scalar) @@ -77,23 +78,32 @@ class _FakeSequence: 42 """ - def __init__(self, value, length=1): + def __init__(self, value, length=None): self._value = value self._length = length @property - def length(self): + def size(self): return self._length - @length.setter - def length(self, value): + @size.setter + def size(self, value): self._length = value + def __int__(self): + return int(self._value) + + def __float__(self): + return float(self._value) + def __getitem__(self, _): return self._value def __repr__(self): - return str([i for i in self]) + if self._length is not None: + return str([i for i in self]) + else: + return f"[{self._value}, {self._value}, ..., {self._value}]" def __len__(self): return self._length @@ -101,6 +111,18 @@ def __len__(self): def __iter__(self): return repeat(self._value, self._length) + def max(self): + return self._value + + def min(self): + return self._value + + def sum(self): + if self._length in None: + return np.inf + else: + return self._length * self._value + def to_numpy(self, length=None): if length is not None: return np.full(length, self._value) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index b2c32fdfc..a9c5e1c8a 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -282,9 +282,9 @@ def _check_invest_attributes(self): raise AttributeError(e2) if ( self.investment - and sum(solph_sequence(self.fixed_losses_absolute)) != 0 + and self.fixed_losses_absolute.max() != 0 and self.investment.existing == 0 - and self.investment.minimum[0] == 0 + and self.investment.minimum.min() == 0 ): e3 = ( "With fixed_losses_absolute > 0, either investment.existing " diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index d5a09bcba..f6e6c6fb0 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -19,6 +19,7 @@ SPDX-License-Identifier: MIT """ +import numpy as np from warnings import warn @@ -280,16 +281,14 @@ def normed_offset_and_conversion_factors_from_coefficients( input_bus = list(self.inputs.values())[0].input for flow in self.outputs.values(): - max_len = max( - len(flow.max), - len(flow.min), - len(coefficients[0]), - len(coefficients[1]), - ) + if flow.max.size is not None: + target_len = flow.max.size + else: + target_len = 1 slope = [] offset = [] - for i in range(max_len): + for i in range(target_len): eta_at_max = ( flow.max[i] * coefficients[1][i] @@ -307,7 +306,7 @@ def normed_offset_and_conversion_factors_from_coefficients( slope.append(c0) offset.append(c1) - if max_len == 1: + if target_len == 1: slope = slope[0] offset = offset[0] diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index ae9b4e6e6..633e2bfee 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -293,7 +293,7 @@ def __init__( if ( self.investment and self.nonconvex - and not np.isfinite(self.investment.maximum) + and not np.isfinite(self.investment.maximum.max()) ): raise AttributeError( "Investment into a non-convex flows needs a maximum " diff --git a/src/oemof/solph/flows/_investment_flow_block.py b/src/oemof/solph/flows/_investment_flow_block.py index 02f3c8f37..99efec0dc 100644 --- a/src/oemof/solph/flows/_investment_flow_block.py +++ b/src/oemof/solph/flows/_investment_flow_block.py @@ -141,11 +141,7 @@ def _create_sets(self, group): ) self.MIN_INVESTFLOWS = Set( - initialize=[ - (g[0], g[1]) - for g in group - if (g[2].min[0] != 0 or len(g[2].min) > 1) - ] + initialize=[(g[0], g[1]) for g in group if g[2].min.min() != 0] ) self.EXISTING_INVESTFLOWS = Set( diff --git a/src/oemof/solph/flows/_non_convex_flow_block.py b/src/oemof/solph/flows/_non_convex_flow_block.py index 5034ab176..aa1cf457c 100644 --- a/src/oemof/solph/flows/_non_convex_flow_block.py +++ b/src/oemof/solph/flows/_non_convex_flow_block.py @@ -230,14 +230,14 @@ def _sets_for_non_convex_flows(self, group): initialize=[ (g[0], g[1]) for g in group - if max(g[2].nonconvex.minimum_uptime) > 0 + if g[2].nonconvex.minimum_uptime.max() > 0 ] ) self.MINDOWNTIMEFLOWS = Set( initialize=[ (g[0], g[1]) for g in group - if max(g[2].nonconvex.minimum_downtime) > 0 + if g[2].nonconvex.minimum_downtime.max() > 0 ] ) self.NEGATIVE_GRADIENT_FLOWS = Set( diff --git a/tests/test_plumbing.py b/tests/test_plumbing.py index 4ed99c110..14bc7400b 100644 --- a/tests/test_plumbing.py +++ b/tests/test_plumbing.py @@ -14,21 +14,21 @@ def test_sequence(): seq0 = sequence(0) assert seq0[0] == 0 - assert len(seq0) == 1 + assert seq0.size == None assert seq0[10] == 0 - assert len(seq0) == 11 + assert seq0.size == None - assert max(seq0) == 0 + assert seq0.max() == 0 seq10 = sequence(10) - assert max(seq10) == 10 + assert seq10.max() == 10 assert seq10[0] == 10 - assert len(seq10) == 1 + assert seq10.size == None - assert seq10[10] == 10 - assert len(seq10) == 11 + seq10.size = 10 + assert seq10.size == 10 seq12 = sequence([1, 3]) assert max(seq12) == 3 diff --git a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py index eec3f7267..343513251 100644 --- a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py +++ b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py @@ -191,12 +191,11 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): ]["label"] == "('fixed_chp', 'gas')" ) - assert ( + assert float( parameter[(energysystem.groups["('fixed_chp', 'gas')"], None)][ "scalars" ]["conversion_factors_('electricity', 2)"] - == 0.3 - ) + ) == pytest.approx(0.3) # objective function assert solph.processing.meta_results(om)["objective"] == pytest.approx( From ff479789f4172eb4aa7ced27cc51a8e20fa9f561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 12 Aug 2024 20:09:35 +0200 Subject: [PATCH 03/10] Fix import order --- src/oemof/solph/components/_offset_converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index f6e6c6fb0..9f233d757 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -19,10 +19,9 @@ SPDX-License-Identifier: MIT """ -import numpy as np - from warnings import warn +import numpy as np from oemof.network import Node from pyomo.core import BuildAction from pyomo.core.base.block import ScalarBlock From 4cd3d47232f53bf99d2553f8b649ca37ae1718c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 12 Aug 2024 21:41:53 +0200 Subject: [PATCH 04/10] Add value getter for _FakeSequence --- src/oemof/solph/_plumbing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index 4404471dd..08172e348 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -128,3 +128,7 @@ def to_numpy(self, length=None): return np.full(length, self._value) else: return np.full(len(self), self._value) + + @property + def value(self): + return self._value From 3e94cccf4cfb38233feaac6055306f8ca1432162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 12 Aug 2024 21:52:14 +0200 Subject: [PATCH 05/10] Simplify scalar/sequence check in processing We can test for the _FakeSequence class to see that something really is a scalar. --- src/oemof/solph/processing.py | 48 +++++++++---------- .../test_variable_chp/test_variable_chp.py | 2 +- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/oemof/solph/processing.py b/src/oemof/solph/processing.py index 0ff75e70e..6623aa5be 100644 --- a/src/oemof/solph/processing.py +++ b/src/oemof/solph/processing.py @@ -17,6 +17,7 @@ """ import sys +from collections import abc from itertools import groupby import numpy as np @@ -25,6 +26,7 @@ from pyomo.core.base.piecewise import IndexedPiecewise from pyomo.core.base.var import Var +from ._plumbing import _FakeSequence from .helpers import flatten @@ -510,7 +512,8 @@ def __separate_attrs( """ def detect_scalars_and_sequences(com): - com_data = {"scalars": {}, "sequences": {}} + scalars = {} + sequences = {} default_exclusions = [ "__", @@ -538,13 +541,13 @@ def detect_scalars_and_sequences(com): # "investment" prefix to component data: if attr_value.__class__.__name__ == "Investment": invest_data = detect_scalars_and_sequences(attr_value) - com_data["scalars"].update( + scalars.update( { "investment_" + str(k): v for k, v in invest_data["scalars"].items() } ) - com_data["sequences"].update( + sequences.update( { "investment_" + str(k): v for k, v in invest_data["sequences"].items() @@ -553,7 +556,7 @@ def detect_scalars_and_sequences(com): continue if isinstance(attr_value, str): - com_data["scalars"][a] = attr_value + scalars[a] = attr_value continue # If the label is a tuple it is iterable, therefore it should be @@ -561,16 +564,19 @@ def detect_scalars_and_sequences(com): if a == "label": attr_value = str(attr_value) - # check if attribute is iterable - # see: https://stackoverflow.com/questions/1952464/ - # in-python-how-do-i-determine-if-an-object-is-iterable - try: - _ = (e for e in attr_value) - com_data["sequences"][a] = attr_value - except TypeError: - com_data["scalars"][a] = attr_value + if isinstance(attr_value, abc.Iterable): + sequences[a] = attr_value + elif isinstance(attr_value, _FakeSequence): + scalars[a] = attr_value.value + else: + scalars[a] = attr_value - com_data["sequences"] = flatten(com_data["sequences"]) + sequences = flatten(sequences) + + com_data = { + "scalars": scalars, + "sequences": sequences, + } move_undetected_scalars(com_data) if exclude_none: remove_nones(com_data) @@ -586,19 +592,11 @@ def move_undetected_scalars(com): if isinstance(value, str): com["scalars"][ckey] = value del com["sequences"][ckey] - continue - try: - _ = (e for e in value) - except TypeError: - com["scalars"][ckey] = value + elif isinstance(value, _FakeSequence): + com["scalars"][ckey] = value.value + del com["sequences"][ckey] + elif len(value) == 0: del com["sequences"][ckey] - else: - try: - if not value.default_changed: - com["scalars"][ckey] = value.default - del com["sequences"][ckey] - except AttributeError: - pass def remove_nones(com): for ckey, value in list(com["scalars"].items()): diff --git a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py index 343513251..4b790bd22 100644 --- a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py +++ b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py @@ -191,7 +191,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): ]["label"] == "('fixed_chp', 'gas')" ) - assert float( + assert ( parameter[(energysystem.groups["('fixed_chp', 'gas')"], None)][ "scalars" ]["conversion_factors_('electricity', 2)"] From 7c8bc4c085f17864d3821809350a91c6cddf3b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 12 Aug 2024 21:57:19 +0200 Subject: [PATCH 06/10] Adhere to style conventions --- src/oemof/solph/_plumbing.py | 1 - src/oemof/solph/components/_offset_converter.py | 1 - tests/test_plumbing.py | 6 +++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index 08172e348..f361075c8 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -11,7 +11,6 @@ """ -from collections import UserList from collections import abc from itertools import repeat diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index 9f233d757..b078c9ced 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -21,7 +21,6 @@ """ from warnings import warn -import numpy as np from oemof.network import Node from pyomo.core import BuildAction from pyomo.core.base.block import ScalarBlock diff --git a/tests/test_plumbing.py b/tests/test_plumbing.py index 14bc7400b..ecf80ef70 100644 --- a/tests/test_plumbing.py +++ b/tests/test_plumbing.py @@ -14,10 +14,10 @@ def test_sequence(): seq0 = sequence(0) assert seq0[0] == 0 - assert seq0.size == None + assert seq0.size is None assert seq0[10] == 0 - assert seq0.size == None + assert seq0.size is None assert seq0.max() == 0 @@ -25,7 +25,7 @@ def test_sequence(): assert seq10.max() == 10 assert seq10[0] == 10 - assert seq10.size == None + assert seq10.size is None seq10.size = 10 assert seq10.size == 10 From 8fdd87e49cee55fa8f5ddd4d771ed64f9c09db42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 13 Aug 2024 10:14:10 +0200 Subject: [PATCH 07/10] Do not cast strings to sequence of strings --- src/oemof/solph/_plumbing.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index f361075c8..f8878fdf4 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -18,7 +18,7 @@ def sequence(iterable_or_scalar): - """Tests if an object is iterable (except string) or scalar and returns + """Checks if an object is iterable (except string) or scalar and returns the original sequence if object is an iterable and an 'emulated' sequence object of class _Sequence if object is a scalar or string. @@ -48,10 +48,9 @@ def sequence(iterable_or_scalar): 10 """ - if isinstance(iterable_or_scalar, abc.Iterable) and not isinstance( - iterable_or_scalar, str - ): - + if isinstance(iterable_or_scalar, str): + return iterable_or_scalar + elif isinstance(iterable_or_scalar, abc.Iterable): return np.array(iterable_or_scalar) else: return _FakeSequence(value=iterable_or_scalar) @@ -117,7 +116,7 @@ def min(self): return self._value def sum(self): - if self._length in None: + if self._length is None: return np.inf else: return self._length * self._value From 519f95c2166fd093e376abbb595cb82f21565286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 13 Aug 2024 10:14:55 +0200 Subject: [PATCH 08/10] Delete scalar cast for _FakeSequence --- src/oemof/solph/_plumbing.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index f8878fdf4..bb6ed5b80 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -88,12 +88,6 @@ def size(self): def size(self, value): self._length = value - def __int__(self): - return int(self._value) - - def __float__(self): - return float(self._value) - def __getitem__(self, _): return self._value From 41ff6082e1790612e77c023125eac75cbfd90d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 13 Aug 2024 10:25:01 +0200 Subject: [PATCH 09/10] Add tests for sequence module --- tests/test_plumbing.py | 60 ++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/tests/test_plumbing.py b/tests/test_plumbing.py index ecf80ef70..b51187be8 100644 --- a/tests/test_plumbing.py +++ b/tests/test_plumbing.py @@ -6,34 +6,60 @@ SPDX-License-Identifier: MIT """ +import numpy as np import pytest +from oemof.solph._plumbing import _FakeSequence from oemof.solph._plumbing import sequence -def test_sequence(): - seq0 = sequence(0) - assert seq0[0] == 0 +def test_fake_sequence(): + seq0 = _FakeSequence(42) + assert seq0[0] == 42 assert seq0.size is None - assert seq0[10] == 0 + assert seq0[10] == 42 assert seq0.size is None - assert seq0.max() == 0 + assert seq0.max() == 42 + assert seq0.min() == 42 + assert seq0.value == 42 + assert seq0.sum() == np.inf + + assert str(seq0) == "[42, 42, ..., 42]" + + with pytest.raises(TypeError): + seq0.to_numpy() + assert (seq0.to_numpy(length=5) == np.array(5 * [42])).all() + + with pytest.raises(TypeError): + len(seq0) + + seq0.size = 2 + assert seq0.size == 2 + assert len(seq0) == 2 - seq10 = sequence(10) - assert seq10.max() == 10 + assert seq0.max() == 42 + assert seq0.min() == 42 + assert seq0.value == 42 + assert seq0.sum() == 84 - assert seq10[0] == 10 - assert seq10.size is None + assert str(seq0) == "[42, 42]" - seq10.size = 10 - assert seq10.size == 10 + assert (seq0.to_numpy() == np.array(2 * [42])).all() + assert (seq0.to_numpy(length=5) == np.array(5 * [42])).all() + + +def test_sequence(): + seq0 = sequence(0) + assert isinstance(seq0, _FakeSequence) + assert seq0.value == 0 + assert seq0.size is None - seq12 = sequence([1, 3]) - assert max(seq12) == 3 - assert seq12[0] == 1 - assert seq12[1] == 3 + seq13 = sequence([1, 3]) + assert isinstance(seq13, np.ndarray) + assert (seq13 == np.array([1, 3])).all() - with pytest.raises(IndexError): - _ = seq12[2] + seq_ab = sequence("ab") + assert isinstance(seq_ab, str) + assert seq_ab == "ab" From 6cb4aeffe71d05efb2493eb3fc1afa7b78298b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 13 Aug 2024 10:31:04 +0200 Subject: [PATCH 10/10] Add FakeSequence to changelog --- docs/whatsnew/v0-5-4.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/whatsnew/v0-5-4.rst b/docs/whatsnew/v0-5-4.rst index 942635137..74ce1207c 100644 --- a/docs/whatsnew/v0-5-4.rst +++ b/docs/whatsnew/v0-5-4.rst @@ -21,6 +21,8 @@ Bug fixes Other changes ############# +* Refined internal sequence generation. (For e.g. constraint formulations, + Scalars are internally mapped to fake sequences without a defined length.) Known issues ############