From 12d277381737aa626ecb87496166f2b0222f9713 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson <oscar.gustafsson@gmail.com> Date: Fri, 25 Aug 2023 12:17:17 +0200 Subject: [PATCH] Add more WDF adaptors --- b_asic/__init__.py | 1 + b_asic/core_operations.py | 67 ------ b_asic/operation.py | 11 +- b_asic/sfg_generators.py | 8 +- b_asic/wdf_operations.py | 370 +++++++++++++++++++++++++++++ docs_sphinx/api/wdf_operations.rst | 12 + examples/lwdfallpass.py | 2 +- examples/thirdorderblwdf.py | 3 +- test/test_core_operations.py | 34 --- test/test_sfg.py | 2 +- test/test_sfg_generators.py | 7 +- test/test_wdf_operations.py | 189 +++++++++++++++ 12 files changed, 585 insertions(+), 121 deletions(-) create mode 100644 b_asic/wdf_operations.py create mode 100644 docs_sphinx/api/wdf_operations.rst create mode 100644 test/test_wdf_operations.py diff --git a/b_asic/__init__.py b/b_asic/__init__.py index f1fded40..fbf1fe4f 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -12,3 +12,4 @@ from b_asic.signal import * from b_asic.signal_flow_graph import * from b_asic.simulation import * from b_asic.special_operations import * +from b_asic.wdf_operations import * diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index e731a480..b83baa72 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -912,73 +912,6 @@ class MAD(AbstractOperation): p._index = i -class SymmetricTwoportAdaptor(AbstractOperation): - r""" - Wave digital filter symmetric twoport-adaptor operation. - - .. math:: - \begin{eqnarray} - y_0 & = & x_1 + \text{value}\times\left(x_1 - x_0\right)\\ - y_1 & = & x_0 + \text{value}\times\left(x_1 - x_0\right) - \end{eqnarray} - """ - is_linear = True - is_swappable = True - - def __init__( - self, - value: Num = 0, - src0: Optional[SignalSourceProvider] = None, - src1: Optional[SignalSourceProvider] = None, - name: Name = Name(""), - latency: Optional[int] = None, - latency_offsets: Optional[Dict[str, int]] = None, - execution_time: Optional[int] = None, - ): - """Construct a SymmetricTwoportAdaptor operation.""" - super().__init__( - input_count=2, - output_count=2, - name=Name(name), - input_sources=[src0, src1], - latency=latency, - latency_offsets=latency_offsets, - execution_time=execution_time, - ) - self.value = value - - @classmethod - def type_name(cls) -> TypeName: - return TypeName("sym2p") - - def evaluate(self, a, b): - tmp = self.value * (b - a) - return b + tmp, a + tmp - - @property - def value(self) -> Num: - """Get the constant value of this operation.""" - return self.param("value") - - @value.setter - def value(self, value: Num) -> None: - """Set the constant value of this operation.""" - if -1 <= value <= 1: - self.set_param("value", value) - else: - raise ValueError('value must be between -1 and 1 (inclusive)') - - def swap_io(self) -> None: - # Swap inputs and outputs and change sign of coefficient - self._input_ports.reverse() - for i, p in enumerate(self._input_ports): - p._index = i - self._output_ports.reverse() - for i, p in enumerate(self._output_ports): - p._index = i - self.set_param("value", -self.value) - - class Reciprocal(AbstractOperation): r""" Reciprocal operation. diff --git a/b_asic/operation.py b/b_asic/operation.py index 3598eb2d..4e586402 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -668,13 +668,12 @@ class AbstractOperation(Operation, AbstractGraphComponent): f" {self.input_count}, got {len(input_values)})" ) - values = self.evaluate( - *( - self.quantize_inputs(input_values, bits_override) - if quantize - else input_values - ) + input_values = ( + self.quantize_inputs(input_values, bits_override) + if quantize + else input_values ) + values = self.evaluate(*input_values) if isinstance(values, collections.abc.Sequence): if len(values) != self.output_count: raise RuntimeError( diff --git a/b_asic/sfg_generators.py b/b_asic/sfg_generators.py index 8682db1b..1f1bf2c7 100644 --- a/b_asic/sfg_generators.py +++ b/b_asic/sfg_generators.py @@ -7,15 +7,11 @@ from typing import Dict, Optional, Sequence, Union import numpy as np -from b_asic.core_operations import ( - Addition, - ConstantMultiplication, - Name, - SymmetricTwoportAdaptor, -) +from b_asic.core_operations import Addition, ConstantMultiplication, Name from b_asic.signal import Signal from b_asic.signal_flow_graph import SFG from b_asic.special_operations import Delay, Input, Output +from b_asic.wdf_operations import SymmetricTwoportAdaptor def wdf_allpass( diff --git a/b_asic/wdf_operations.py b/b_asic/wdf_operations.py new file mode 100644 index 00000000..95213e83 --- /dev/null +++ b/b_asic/wdf_operations.py @@ -0,0 +1,370 @@ +""" +B-ASIC Core Operations Module. + +Contains wave digital filter adaptors. +""" +from typing import Dict, Iterable, Optional, Tuple + +from b_asic.graph_component import Name, TypeName +from b_asic.operation import AbstractOperation +from b_asic.port import SignalSourceProvider +from b_asic.types import Num + + +class SymmetricTwoportAdaptor(AbstractOperation): + r""" + Wave digital filter symmetric twoport-adaptor operation. + + .. math:: + \begin{eqnarray} + y_0 & = & x_1 + \text{value}\times\left(x_1 - x_0\right)\\ + y_1 & = & x_0 + \text{value}\times\left(x_1 - x_0\right) + \end{eqnarray} + """ + is_linear = True + is_swappable = True + + def __init__( + self, + value: Num = 0, + src0: Optional[SignalSourceProvider] = None, + src1: Optional[SignalSourceProvider] = None, + name: Name = Name(""), + latency: Optional[int] = None, + latency_offsets: Optional[Dict[str, int]] = None, + execution_time: Optional[int] = None, + ): + """Construct a SymmetricTwoportAdaptor operation.""" + super().__init__( + input_count=2, + output_count=2, + name=Name(name), + input_sources=[src0, src1], + latency=latency, + latency_offsets=latency_offsets, + execution_time=execution_time, + ) + self.value = value + + @classmethod + def type_name(cls) -> TypeName: + return TypeName("sym2p") + + def evaluate(self, a, b): + tmp = self.value * (b - a) + return b + tmp, a + tmp + + @property + def value(self) -> Num: + """Get the constant value of this operation.""" + return self.param("value") + + @value.setter + def value(self, value: Num) -> None: + """Set the constant value of this operation.""" + if -1 <= value <= 1: + self.set_param("value", value) + else: + raise ValueError('value must be between -1 and 1 (inclusive)') + + def swap_io(self) -> None: + # Swap inputs and outputs and change sign of coefficient + self._input_ports.reverse() + for i, p in enumerate(self._input_ports): + p._index = i + self._output_ports.reverse() + for i, p in enumerate(self._output_ports): + p._index = i + self.set_param("value", -self.value) + + +class SeriesTwoportAdaptor(AbstractOperation): + r""" + Wave digital filter series twoport-adaptor operation. + + .. math:: + \begin{eqnarray} + y_0 & = & x_0 - \text{value}\times\left(x_0 + x_1\right)\\ + y_1 & = & x_1 - (2-\text{value})\times\left(x_0 + x_1\right)\\ + & = & -2x_0 - x_1 + \text{value}\times\left(x_0 + x_1\right) + \end{eqnarray} + + Port 1 is the dependent port. + """ + is_linear = True + is_swappable = True + + def __init__( + self, + value: Num = 0, + src0: Optional[SignalSourceProvider] = None, + src1: Optional[SignalSourceProvider] = None, + name: Name = Name(""), + latency: Optional[int] = None, + latency_offsets: Optional[Dict[str, int]] = None, + execution_time: Optional[int] = None, + ): + """Construct a SeriesTwoportAdaptor operation.""" + super().__init__( + input_count=2, + output_count=2, + name=Name(name), + input_sources=[src0, src1], + latency=latency, + latency_offsets=latency_offsets, + execution_time=execution_time, + ) + self.value = value + + @classmethod + def type_name(cls) -> TypeName: + return TypeName("ser2p") + + def evaluate(self, a, b): + s = a + b + val = self.value + t = val * a + y0 = a - t + y1 = -(s + y0) + return y0, y1 + + @property + def value(self) -> Num: + """Get the constant value of this operation.""" + return self.param("value") + + @value.setter + def value(self, value: Num) -> None: + """Set the constant value of this operation.""" + if 0 <= value <= 2: + self.set_param("value", value) + else: + raise ValueError('value must be between 0 and 2 (inclusive)') + + def swap_io(self) -> None: + # Swap inputs and outputs and, hence, which port is dependent + self._input_ports.reverse() + for i, p in enumerate(self._input_ports): + p._index = i + self._output_ports.reverse() + for i, p in enumerate(self._output_ports): + p._index = i + self.set_param("value", 2 - self.value) + + +class ParallelTwoportAdaptor(AbstractOperation): + r""" + Wave digital filter parallel twoport-adaptor operation. + + .. math:: + \begin{eqnarray} + y_0 & = & - x_0 + \text{value}\times x_0 + (2 - \text{value}) \times x_1\\ + & = & 2x_1 - x_0 + \text{value}\times \left(x_0 - x_1\right) + y_1 & = & - x_1 + \text{value}\times x_0 + (2 - \text{value}) \times x_1\\ + & = & x_1 + \text{value}\times\left(x_0 - x_1\right) + \end{eqnarray} + + Port 1 is the dependent port. + """ + is_linear = True + is_swappable = True + + def __init__( + self, + value: Num = 0, + src0: Optional[SignalSourceProvider] = None, + src1: Optional[SignalSourceProvider] = None, + name: Name = Name(""), + latency: Optional[int] = None, + latency_offsets: Optional[Dict[str, int]] = None, + execution_time: Optional[int] = None, + ): + """Construct a ParallelTwoportAdaptor operation.""" + super().__init__( + input_count=2, + output_count=2, + name=Name(name), + input_sources=[src0, src1], + latency=latency, + latency_offsets=latency_offsets, + execution_time=execution_time, + ) + self.value = value + + @classmethod + def type_name(cls) -> TypeName: + return TypeName("par2p") + + def evaluate(self, a, b): + s = b - a + val = self.value + t = val * s + y1 = b - t + y0 = y1 + s + return y0, y1 + + @property + def value(self) -> Num: + """Get the constant value of this operation.""" + return self.param("value") + + @value.setter + def value(self, value: Num) -> None: + """Set the constant value of this operation.""" + if 0 <= value <= 2: + self.set_param("value", value) + else: + raise ValueError('value must be between 0 and 2 (inclusive)') + + def swap_io(self) -> None: + # Swap inputs and outputs and, hence, which port is dependent + self._input_ports.reverse() + for i, p in enumerate(self._input_ports): + p._index = i + self._output_ports.reverse() + for i, p in enumerate(self._output_ports): + p._index = i + self.set_param("value", 2 - self.value) + + +class SeriesThreeportAdaptor(AbstractOperation): + r""" + Wave digital filter series threeport-adaptor operation. + + .. math:: + \begin{eqnarray} + y_0 & = & x_0 - \text{value}_0\times\left(x_0 + x_1 + x_2\right)\\ + y_1 & = & x_1 - \text{value}_1\times\left(x_0 + x_1 + x_2\right)\\ + y_2 & = & x_2 - \left(2 - \text{value}_0 - \text{value}_1\right)\times\left(x_0 + + x_1 + x_2\right) + \end{eqnarray} + + Port 2 is the dependent port. + + """ + is_linear = True + is_swappable = True + + def __init__( + self, + value: Tuple[Num, Num] = (0, 0), + src0: Optional[SignalSourceProvider] = None, + src1: Optional[SignalSourceProvider] = None, + src2: Optional[SignalSourceProvider] = None, + name: Name = Name(""), + latency: Optional[int] = None, + latency_offsets: Optional[Dict[str, int]] = None, + execution_time: Optional[int] = None, + ): + """Construct a SeriesThreeportAdaptor operation.""" + super().__init__( + input_count=3, + output_count=3, + name=Name(name), + input_sources=[src0, src1, src2], + latency=latency, + latency_offsets=latency_offsets, + execution_time=execution_time, + ) + self.value = value + + @classmethod + def type_name(cls) -> TypeName: + return TypeName("ser3p") + + def evaluate(self, a, b, c): + s = a + b + c + val0, val1 = self.value + y0 = a - val0 * s + y1 = b - val1 * s + y2 = -(y0 + y1 + s) + return y0, y1, y2 + + @property + def value(self) -> Tuple[Num, Num]: + """Get the constant value of this operation.""" + return self.param("value") + + @value.setter + def value(self, value: Tuple[Num, Num]) -> None: + """Set the constant value of this operation.""" + if not all(0 <= v <= 2 for v in value): + raise ValueError('each value must be between 0 and 2 (inclusive)') + if 0 <= sum(value) <= 2: + self.set_param("value", value) + else: + raise ValueError('sum of values must be between 0 and 2 (inclusive)') + + +class ReflectionFreeSeriesThreeportAdaptor(AbstractOperation): + r""" + Wave digital filter reflection free series threeport-adaptor operation. + + .. math:: + \begin{eqnarray} + y_0 & = & x_0 - \text{value}\times\left(x_0 + x_1 + x_2\right)\\ + y_1 & = & -x_0 - x_2\\ + y_2 & = & x_2 - \left(1 - \text{value}\right)\times\left(x_0 + + x_1 + x_2\right) \\ + & = & -x_0 - x_1 + \text{value}\times\left(x_0 + + x_1 + x_2\right) + \end{eqnarray} + + Port 1 is the reflection-free port and port 2 is the dependent port. + """ + is_linear = True + is_swappable = True + + def __init__( + self, + value: Num = 0, + src0: Optional[SignalSourceProvider] = None, + src1: Optional[SignalSourceProvider] = None, + src2: Optional[SignalSourceProvider] = None, + name: Name = Name(""), + latency: Optional[int] = None, + latency_offsets: Optional[Dict[str, int]] = None, + execution_time: Optional[int] = None, + ): + """Construct a ReflectionFreeSeriesThreeportAdaptor operation.""" + super().__init__( + input_count=3, + output_count=3, + name=Name(name), + input_sources=[src0, src1, src2], + latency=latency, + latency_offsets=latency_offsets, + execution_time=execution_time, + ) + self.value = value + + @classmethod + def type_name(cls) -> TypeName: + return TypeName("rfs3p") + + def evaluate(self, a, b, c): + s = a + c + y1 = -s + y0 = a - self.value * (b + s) + y2 = -(y0 + b) + return y0, y1, y2 + + @property + def value(self) -> Num: + """Get the constant value of this operation.""" + return self.param("value") + + @value.setter + def value(self, value: Num) -> None: + """Set the constant value of this operation.""" + if 0 <= value <= 1: + self.set_param("value", value) + else: + raise ValueError('value must be between 0 and 1 (inclusive)') + + def inputs_required_for_output(self, output_index: int) -> Iterable[int]: + """ + Get the input indices of all inputs in this operation whose values are + required in order to evaluate the output at the given output index. + """ + return {0: (0, 1, 2), 1: (0, 2), 2: (0, 1, 2)} diff --git a/docs_sphinx/api/wdf_operations.rst b/docs_sphinx/api/wdf_operations.rst new file mode 100644 index 00000000..10ecdee5 --- /dev/null +++ b/docs_sphinx/api/wdf_operations.rst @@ -0,0 +1,12 @@ +************************* +``b_asic.wdf_operations`` +************************* + +.. inheritance-diagram:: b_asic.wdf_operations + :parts: 1 + :top-classes: b_asic.graph_component.GraphComponent, b_asic.port.SignalSourceProvider + +.. automodule:: b_asic.wdf_operations + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/lwdfallpass.py b/examples/lwdfallpass.py index 281856fe..68b9b082 100644 --- a/examples/lwdfallpass.py +++ b/examples/lwdfallpass.py @@ -7,10 +7,10 @@ LWDF first-order allpass section This has different latency offsets for the different inputs/outputs. """ -from b_asic.core_operations import SymmetricTwoportAdaptor from b_asic.schedule import Schedule from b_asic.signal_flow_graph import SFG from b_asic.special_operations import Delay, Input, Output +from b_asic.wdf_operations import SymmetricTwoportAdaptor in0 = Input() diff --git a/examples/thirdorderblwdf.py b/examples/thirdorderblwdf.py index fc289e24..02b45486 100644 --- a/examples/thirdorderblwdf.py +++ b/examples/thirdorderblwdf.py @@ -8,12 +8,13 @@ Small bireciprocal lattice wave digital filter. import numpy as np from mplsignal.freq_plots import freqz_fir -from b_asic.core_operations import Addition, SymmetricTwoportAdaptor +from b_asic.core_operations import Addition from b_asic.schedule import Schedule from b_asic.signal_flow_graph import SFG from b_asic.signal_generator import Impulse from b_asic.simulation import Simulation from b_asic.special_operations import Delay, Input, Output +from b_asic.wdf_operations import SymmetricTwoportAdaptor in0 = Input("x") D0 = Delay(in0) diff --git a/test/test_core_operations.py b/test/test_core_operations.py index f9d13fd6..ba99d8a0 100644 --- a/test/test_core_operations.py +++ b/test/test_core_operations.py @@ -22,7 +22,6 @@ from b_asic import ( Sink, SquareRoot, Subtraction, - SymmetricTwoportAdaptor, ) @@ -349,39 +348,6 @@ class TestButterfly: assert test_operation.evaluate_output(1, [2 + 1j, 3 - 2j]) == -1 + 3j -class TestSymmetricTwoportAdaptor: - """Tests for SymmetricTwoportAdaptor class.""" - - def test_symmetrictwoportadaptor_positive(self): - test_operation = SymmetricTwoportAdaptor(0.5) - assert test_operation.evaluate_output(0, [2, 3]) == 3.5 - assert test_operation.evaluate_output(1, [2, 3]) == 2.5 - assert test_operation.value == 0.5 - - def test_symmetrictwoportadaptor_negative(self): - test_operation = SymmetricTwoportAdaptor(0.5) - assert test_operation.evaluate_output(0, [-2, -3]) == -3.5 - assert test_operation.evaluate_output(1, [-2, -3]) == -2.5 - - def test_symmetrictwoportadaptor_complex(self): - test_operation = SymmetricTwoportAdaptor(0.5) - assert test_operation.evaluate_output(0, [2 + 1j, 3 - 2j]) == 3.5 - 3.5j - assert test_operation.evaluate_output(1, [2 + 1j, 3 - 2j]) == 2.5 - 0.5j - - def test_symmetrictwoportadaptor_swap_io(self): - test_operation = SymmetricTwoportAdaptor(0.5) - assert test_operation.value == 0.5 - test_operation.swap_io() - assert test_operation.value == -0.5 - - def test_symmetrictwoportadaptor_error(self): - with pytest.raises(ValueError, match="value must be between -1 and 1"): - _ = SymmetricTwoportAdaptor(-2) - test_operation = SymmetricTwoportAdaptor(0) - with pytest.raises(ValueError, match="value must be between -1 and 1"): - test_operation.value = 2 - - class TestReciprocal: """Tests for Absolute class.""" diff --git a/test/test_sfg.py b/test/test_sfg.py index ab38f328..c0b68965 100644 --- a/test/test_sfg.py +++ b/test/test_sfg.py @@ -18,7 +18,6 @@ from b_asic.core_operations import ( Multiplication, SquareRoot, Subtraction, - SymmetricTwoportAdaptor, ) from b_asic.operation import ResultKey from b_asic.save_load_structure import python_to_sfg, sfg_to_python @@ -26,6 +25,7 @@ from b_asic.sfg_generators import wdf_allpass from b_asic.signal_flow_graph import SFG, GraphID from b_asic.simulation import Simulation from b_asic.special_operations import Delay +from b_asic.wdf_operations import SymmetricTwoportAdaptor class TestInit: diff --git a/test/test_sfg_generators.py b/test/test_sfg_generators.py index 31293462..b7771315 100644 --- a/test/test_sfg_generators.py +++ b/test/test_sfg_generators.py @@ -1,11 +1,7 @@ import numpy as np import pytest -from b_asic.core_operations import ( - Addition, - ConstantMultiplication, - SymmetricTwoportAdaptor, -) +from b_asic.core_operations import Addition, ConstantMultiplication from b_asic.sfg_generators import ( direct_form_fir, transposed_direct_form_fir, @@ -14,6 +10,7 @@ from b_asic.sfg_generators import ( from b_asic.signal_generator import Impulse from b_asic.simulation import Simulation from b_asic.special_operations import Delay +from b_asic.wdf_operations import SymmetricTwoportAdaptor def test_wdf_allpass(): diff --git a/test/test_wdf_operations.py b/test/test_wdf_operations.py new file mode 100644 index 00000000..be86b532 --- /dev/null +++ b/test/test_wdf_operations.py @@ -0,0 +1,189 @@ +"""B-ASIC test suite for the core operations.""" +import pytest + +from b_asic.wdf_operations import ( + ParallelTwoportAdaptor, + ReflectionFreeSeriesThreeportAdaptor, + SeriesThreeportAdaptor, + SeriesTwoportAdaptor, + SymmetricTwoportAdaptor, +) + + +class TestSymmetricTwoportAdaptor: + """Tests for SymmetricTwoportAdaptor class.""" + + def test_symmetrictwoportadaptor_positive(self): + test_operation = SymmetricTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [2, 3]) == 3.5 + assert test_operation.evaluate_output(1, [2, 3]) == 2.5 + assert test_operation.value == 0.5 + + def test_symmetrictwoportadaptor_negative(self): + test_operation = SymmetricTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [-2, -3]) == -3.5 + assert test_operation.evaluate_output(1, [-2, -3]) == -2.5 + + def test_symmetrictwoportadaptor_complex(self): + test_operation = SymmetricTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [2 + 1j, 3 - 2j]) == 3.5 - 3.5j + assert test_operation.evaluate_output(1, [2 + 1j, 3 - 2j]) == 2.5 - 0.5j + + def test_symmetrictwoportadaptor_swap_io(self): + test_operation = SymmetricTwoportAdaptor(0.5) + assert test_operation.value == 0.5 + test_operation.swap_io() + assert test_operation.value == -0.5 + + def test_symmetrictwoportadaptor_error(self): + with pytest.raises(ValueError, match="value must be between -1 and 1"): + _ = SymmetricTwoportAdaptor(-2) + test_operation = SymmetricTwoportAdaptor(0) + with pytest.raises(ValueError, match="value must be between -1 and 1"): + test_operation.value = 2 + + +class TestSeriesTwoportAdaptor: + """Tests for SeriesTwoportAdaptor class.""" + + def test_seriestwoportadaptor_positive(self): + test_operation = SeriesTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [2, 3]) == 1.0 + assert test_operation.evaluate_output(1, [2, 3]) == -6.0 + assert test_operation.value == 0.5 + + def test_seriestwoportadaptor_negative(self): + test_operation = SeriesTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [-2, -3]) == -1.0 + assert test_operation.evaluate_output(1, [-2, -3]) == 6.0 + + def test_seriestwoportadaptor_complex(self): + test_operation = SeriesTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [2 + 1j, 3 - 2j]) == 1 + 0.5j + assert test_operation.evaluate_output(1, [2 + 1j, 3 - 2j]) == -6 + 0.5j + + def test_seriestwoportadaptor_swap_io(self): + test_operation = SeriesTwoportAdaptor(0.5) + assert test_operation.value == 0.5 + test_operation.swap_io() + assert test_operation.value == 1.5 + + def test_seriestwoportadaptor_error(self): + with pytest.raises(ValueError, match="value must be between 0 and 2"): + _ = SeriesTwoportAdaptor(-1) + test_operation = SeriesTwoportAdaptor(0) + with pytest.raises(ValueError, match="value must be between 0 and 2"): + test_operation.value = 3 + + +class TestParallelTwoportAdaptor: + """Tests for ParallelTwoportAdaptor class.""" + + def test_seriestwoportadaptor_positive(self): + test_operation = ParallelTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [2, 3]) == 3.5 + assert test_operation.evaluate_output(1, [2, 3]) == 2.5 + assert test_operation.value == 0.5 + + def test_seriestwoportadaptor_negative(self): + test_operation = ParallelTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [-2, -3]) == -3.5 + assert test_operation.evaluate_output(1, [-2, -3]) == -2.5 + + def test_seriestwoportadaptor_complex(self): + test_operation = ParallelTwoportAdaptor(0.5) + assert test_operation.evaluate_output(0, [2 + 1j, 3 - 2j]) == 3.5 - 3.5j + assert test_operation.evaluate_output(1, [2 + 1j, 3 - 2j]) == 2.5 - 0.5j + + def test_seriestwoportadaptor_swap_io(self): + test_operation = ParallelTwoportAdaptor(0.5) + assert test_operation.value == 0.5 + test_operation.swap_io() + assert test_operation.value == 1.5 + + def test_seriestwoportadaptor_error(self): + with pytest.raises(ValueError, match="value must be between 0 and 2"): + _ = ParallelTwoportAdaptor(-1) + test_operation = ParallelTwoportAdaptor(0) + with pytest.raises(ValueError, match="value must be between 0 and 2"): + test_operation.value = 3 + + +class TestSeriesThreeportAdaptor: + """Tests for SeriesThreeportAdaptor class.""" + + def test_seriesthreeportadaptor_positive(self): + test_operation = SeriesThreeportAdaptor((0.5, 1.25)) + assert test_operation.evaluate_output(0, [2, 3, 4]) == -2.5 + assert test_operation.evaluate_output(1, [2, 3, 4]) == -8.25 + assert test_operation.evaluate_output(2, [2, 3, 4]) == 1.75 + assert test_operation.value == (0.5, 1.25) + + def test_seriesthreeportadaptor_negative(self): + test_operation = SeriesThreeportAdaptor((0.5, 1.25)) + assert test_operation.evaluate_output(0, [-2, -3, -4]) == 2.5 + assert test_operation.evaluate_output(1, [-2, -3, -4]) == 8.25 + assert test_operation.evaluate_output(2, [-2, -3, -4]) == -1.75 + + def test_seriesthreeportadaptor_complex(self): + test_operation = SeriesThreeportAdaptor((0.5, 1.25)) + assert test_operation.evaluate_output(0, [2 + 1j, 3 - 2j, 4 + 3j]) == -2.5 + 0j + assert ( + test_operation.evaluate_output(1, [2 + 1j, 3 - 2j, 4 + 3j]) == -8.25 - 4.5j + ) + assert ( + test_operation.evaluate_output(2, [2 + 1j, 3 - 2j, 4 + 3j]) == 1.75 + 2.5j + ) + + def test_seriesthreeportadaptor_error(self): + with pytest.raises(ValueError, match="each value must be between 0 and 2"): + _ = SeriesThreeportAdaptor((-1, 3)) + with pytest.raises(ValueError, match="sum of values must be between 0 and 2"): + _ = SeriesThreeportAdaptor((1.5, 1.5)) + test_operation = SeriesThreeportAdaptor((0, 0.5)) + with pytest.raises(ValueError, match="each value must be between 0 and 2"): + test_operation.value = (-0.5, 1) + with pytest.raises(ValueError, match="sum of values must be between 0 and 2"): + test_operation.value = (0.5, 2) + + +class TestReflectionFreeSeriesThreeportAdaptor: + """Tests for ReflectionFreeSeriesThreeportAdaptor class.""" + + def test_reflectionfreeseriesthreeportadaptor_positive(self): + test_operation = ReflectionFreeSeriesThreeportAdaptor(0.25) + assert test_operation.evaluate_output(0, [2, 3, 4]) == -0.25 + assert test_operation.evaluate_output(1, [2, 3, 4]) == -6 + assert test_operation.evaluate_output(2, [2, 3, 4]) == -2.75 + assert test_operation.value == 0.25 + + def test_reflectionfreeseriesthreeportadaptor_negative(self): + test_operation = ReflectionFreeSeriesThreeportAdaptor(0.25) + assert test_operation.evaluate_output(0, [-2, -3, -4]) == 0.25 + assert test_operation.evaluate_output(1, [-2, -3, -4]) == 6 + assert test_operation.evaluate_output(2, [-2, -3, -4]) == 2.75 + + def test_reflectionfreeseriesthreeportadaptor_complex(self): + test_operation = ReflectionFreeSeriesThreeportAdaptor(0.25) + assert ( + test_operation.evaluate_output(0, [2 + 1j, 3 - 2j, 4 + 3j]) == -0.25 + 0.5j + ) + assert test_operation.evaluate_output(1, [2 + 1j, 3 - 2j, 4 + 3j]) == -6 - 4j + assert ( + test_operation.evaluate_output(2, [2 + 1j, 3 - 2j, 4 + 3j]) == -2.75 + 1.5j + ) + + def test_reflectionfreeseriesthreeportadaptor_error(self): + with pytest.raises(ValueError, match="value must be between 0 and 1"): + _ = ReflectionFreeSeriesThreeportAdaptor(-1) + test_operation = ReflectionFreeSeriesThreeportAdaptor(0.5) + with pytest.raises(ValueError, match="value must be between 0 and 1"): + test_operation.value = 4 + + def test_reflectionfree_equals_normal(self): + test_operation1 = SeriesThreeportAdaptor((0.25, 1)) + test_operation2 = ReflectionFreeSeriesThreeportAdaptor(0.25) + for port in range(3): + assert test_operation1.evaluate_output( + port, [2, 3, 4] + ) == test_operation2.evaluate_output(port, [2, 3, 4]) -- GitLab