From f363e7b22eb502778f83c09c24a383c641ba22bb Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 15 Dec 2024 22:48:07 -0800 Subject: [PATCH 01/21] updated iosys class/factory function documentation + docstring unit testing --- control/statesp.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/control/statesp.py b/control/statesp.py index 070be2e15..852cce8d0 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1575,6 +1575,19 @@ def ss(*args, **kwargs): name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + remove_useless_states : bool, optional + If `True`, remove states that have no effect on the input/output + dynamics. If not specified, the value is read from + `config.defaults['statesp.remove_useless_states']` (default = False). + method : str, optional + Set the method used for converting a transfer function to a state + space system. Current methods are 'slycot' and 'scipy'. If set to + None (default), try 'slycot' first and then 'scipy' (SISO only). + + Returns + ------- + out: StateSpace + Linear input/output system. Raises ------ From 50762986d3301910446135ee86e7dbbe53eda9ed Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 7 Dec 2024 14:05:50 -0800 Subject: [PATCH 02/21] update state space system repr() to include signal/system names --- control/iosys.py | 38 +++++++++++++++++++++++++++++++++++ control/statesp.py | 16 +++++++++++---- control/tests/statesp_test.py | 22 ++++++++++++-------- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 2c1f9cea7..db3a5d782 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -250,6 +250,44 @@ def __str__(self): str += f"States ({self.nstates}): {self.state_labels}" return str + def _label_repr(self, show_count=True): + out, count = "", 0 + + # Include the system name if not generic + if not self._generic_name_check(): + name_spec = f"name='{self.name}'" + count += len(name_spec) + out += name_spec + + # Include the state, output, and input names if not generic + for sig_name, sig_default, sig_labels in zip( + ['states', 'outputs', 'inputs'], + ['x', 'y', 'u'], # TODO: replace with defaults + [self.state_labels, self.output_labels, self.input_labels]): + if sig_name == 'states' and self.nstates is None: + continue + + # Check if the signal labels are generic + if any([re.match(r'^' + sig_default + r'\[\d*\]$', label) is None + for label in sig_labels]): + spec = f"{sig_name}={sig_labels}" + elif show_count: + spec = f"{sig_name}={len(sig_labels)}" + + # Append the specification string to the output, with wrapping + if count == 0: + count = len(spec) # no system name => suppress comma + elif count + len(spec) > 72: + # TODO: check to make sure a single line is enough (minor) + out += ",\n" + count = len(spec) + else: + out += ", " + count += len(spec) + 2 + out += spec + + return out + # Find a list of signals by name, index, or pattern def _find_signals(self, name_list, sigdict): if not isinstance(name_list, (list, tuple)): diff --git a/control/statesp.py b/control/statesp.py index 852cce8d0..4a268dbd4 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -392,11 +392,19 @@ def __str__(self): # represent to implement a re-loadable version def __repr__(self): """Print state-space system in loadable form.""" - # TODO: add input/output names (?) - return "StateSpace({A}, {B}, {C}, {D}{dt})".format( + out = "StateSpace(\n{A},\n{B},\n{C},\n{D}".format( A=self.A.__repr__(), B=self.B.__repr__(), - C=self.C.__repr__(), D=self.D.__repr__(), - dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '') + C=self.C.__repr__(), D=self.D.__repr__()) + + if config.defaults['control.default_dt'] != self.dt: + out += ",\ndt={dt}".format( + dt='None' if self.dt is None else self.dt) + + if len(labels := self._label_repr()) > 0: + out += ",\n" + labels + + out += ")" + return out def _latex_partitioned_stateless(self): """`Partitioned` matrix LaTeX representation for stateless systems diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 647db4567..1e94c0aff 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -735,17 +735,23 @@ def test_lft(self): def test_repr(self, sys322): """Test string representation""" - ref322 = "\n".join(["StateSpace(array([[-3., 4., 2.],", - " [-1., -3., 0.],", - " [ 2., 5., 3.]]), array([[ 1., 4.],", - " [-3., -3.],", - " [-2., 1.]]), array([[ 4., 2., -3.],", - " [ 1., 4., 3.]]), array([[-2., 4.],", - " [ 0., 1.]]){dt})"]) + ref322 = "\n".join( + ["StateSpace(", + "array([[-3., 4., 2.],", + " [-1., -3., 0.],", + " [ 2., 5., 3.]]),", + "array([[ 1., 4.],", + " [-3., -3.],", + " [-2., 1.]]),", + "array([[ 4., 2., -3.],", + " [ 1., 4., 3.]]),", + "array([[-2., 4.],", + " [ 0., 1.]]){dt},", + "name='sys322', states=3, outputs=2, inputs=2)"]) assert repr(sys322) == ref322.format(dt='') sysd = StateSpace(sys322.A, sys322.B, sys322.C, sys322.D, 0.4) - assert repr(sysd), ref322.format(dt=" == 0.4") + assert repr(sysd), ref322.format(dt="\ndt=0.4") array = np.array # noqa sysd2 = eval(repr(sysd)) np.testing.assert_allclose(sysd.A, sysd2.A) From 6f7cbfa3a323b408b9ad620350f18c2f7f4ab269 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 7 Dec 2024 14:10:57 -0800 Subject: [PATCH 03/21] update transfer function repr() + signal/system names --- control/tests/xferfcn_test.py | 34 ++++++++++++++++++++++------------ control/xferfcn.py | 35 +++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 3f87ef1d2..d0e5d5d15 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1086,24 +1086,34 @@ def test_latex_repr(self): @pytest.mark.parametrize( "Hargs, ref", [(([-1., 4.], [1., 3., 5.]), - "TransferFunction(array([-1., 4.]), array([1., 3., 5.]))"), + "TransferFunction(\n" + "array([-1., 4.]),\n" + "array([1., 3., 5.]),\n" + "outputs=1, inputs=1)"), (([2., 3., 0.], [1., -3., 4., 0], 2.0), - "TransferFunction(array([2., 3., 0.])," - " array([ 1., -3., 4., 0.]), 2.0)"), - + "TransferFunction(\n" + "array([2., 3., 0.]),\n" + "array([ 1., -3., 4., 0.]),\n" + "dt=2.0,\n" + "outputs=1, inputs=1)"), (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3], [0, 1]]]), - "TransferFunction([[array([1]), array([2, 3])]," - " [array([4, 5]), array([6, 7])]]," - " [[array([6, 7]), array([4, 5])]," - " [array([2, 3]), array([1])]])"), + "TransferFunction(\n" + "[[array([1]), array([2, 3])],\n" + " [array([4, 5]), array([6, 7])]],\n" + "[[array([6, 7]), array([4, 5])],\n" + " [array([2, 3]), array([1])]],\n" + "outputs=2, inputs=2)"), (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3], [0, 1]]], 0.5), - "TransferFunction([[array([1]), array([2, 3])]," - " [array([4, 5]), array([6, 7])]]," - " [[array([6, 7]), array([4, 5])]," - " [array([2, 3]), array([1])]], 0.5)") + "TransferFunction(\n" + "[[array([1]), array([2, 3])],\n" + " [array([4, 5]), array([6, 7])]],\n" + "[[array([6, 7]), array([4, 5])],\n" + " [array([2, 3]), array([1])]],\n" + "dt=0.5,\n" + "outputs=2, inputs=2)"), ]) def test_repr(self, Hargs, ref): """Test __repr__ printout.""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 5e391d30b..40c3a8094 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -194,6 +194,7 @@ def __init__(self, *args, **kwargs): % type(args[0])) num = args[0].num den = args[0].den + # TODO: copy over signal names else: raise TypeError("Needs 1, 2 or 3 arguments; received %i." @@ -491,28 +492,34 @@ def __str__(self, var=None): def __repr__(self): """Print transfer function in loadable form.""" if self.issiso(): - return "TransferFunction({num}, {den}{dt})".format( - num=self.num_array[0, 0].__repr__(), - den=self.den_array[0, 0].__repr__(), - dt=', {}'.format(self.dt) if isdtime(self, strict=True) - else '') + out = "TransferFunction(\n{num},\n{den}".format( + num=self.num[0][0].__repr__(), den=self.den[0][0].__repr__()) else: - out = "TransferFunction([" + out = "TransferFunction(\n[" for entry in [self.num_array, self.den_array]: for i in range(self.noutputs): - out += "[" if i == 0 else " [" + out += "[" if i == 0 else "\n [" + linelen = 0 for j in range(self.ninputs): out += ", " if j != 0 else "" numstr = np.array_repr(entry[i, j]) + if linelen + len(numstr) > 72: + out += "\n " + linelen = 0 out += numstr + linelen += len(numstr) out += "]," if i < self.noutputs - 1 else "]" - out += "], [" if entry is self.num_array else "]" + out += "],\n[" if entry is self.num_array else "]" - if config.defaults['control.default_dt'] != self.dt: - out += ", {dt}".format( - dt='None' if self.dt is None else self.dt) - out += ")" - return out + if config.defaults['control.default_dt'] != self.dt: + out += ",\ndt={dt}".format( + dt='None' if self.dt is None else self.dt) + + if len(labels := self._label_repr()) > 0: + out += ",\n" + labels + + out += ")" + return out def _repr_latex_(self, var=None): """LaTeX representation of transfer function, for Jupyter notebook.""" @@ -777,7 +784,7 @@ def __getitem__(self, key): indices[0], self.output_labels, slice_to_list=True) inpdx, inputs = _process_subsys_index( indices[1], self.input_labels, slice_to_list=True) - + # Construct the transfer function for the subsyste num = _create_poly_array((len(outputs), len(inputs))) den = _create_poly_array(num.shape) From 4cedb6cc408856fad5a49489753e48162ae4096f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 7 Dec 2024 14:14:31 -0800 Subject: [PATCH 04/21] update FRD repr() + copy signal/system names on sampling --- control/frdata.py | 13 +++++++++++-- control/tests/frd_test.py | 10 ++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 64a1e8227..ea651ad75 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -285,7 +285,6 @@ def __init__(self, *args, **kwargs): if self.squeeze not in (None, True, False): raise ValueError("unknown squeeze value") - # Process iosys keywords defaults = { 'inputs': self.fresp.shape[1] if not getattr( self, 'input_index', None) else self.input_labels, @@ -421,10 +420,20 @@ def __repr__(self): limited for number of data points. """ - return "FrequencyResponseData({d}, {w}{smooth})".format( + out = "FrequencyResponseData(\n{d},\n{w}{smooth}".format( d=repr(self.fresp), w=repr(self.omega), smooth=(self._ifunc and ", smooth=True") or "") + if config.defaults['control.default_dt'] != self.dt: + out += ",\ndt={dt}".format( + dt='None' if self.dt is None else self.dt) + + if len(labels := self._label_repr()) > 0: + out += ",\n" + labels + + out += ")" + return out + def __neg__(self): """Negate a transfer function.""" diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index b08cd8260..d2675b3c9 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -465,10 +465,12 @@ def test_repr_str(self): [0.1, 1.0, 10.0, 100.0], name='sys0') sys1 = ct.frd( sys0.fresp, sys0.omega, smooth=True, name='sys1') - ref0 = "FrequencyResponseData(" \ - "array([[[1. +0.j , 0.9 +0.1j, 0.1 +2.j , 0.05+3.j ]]])," \ - " array([ 0.1, 1. , 10. , 100. ]))" - ref1 = ref0[:-1] + ", smooth=True)" + ref_common = "FrequencyResponseData(\n" \ + "array([[[1. +0.j , 0.9 +0.1j, 0.1 +2.j , 0.05+3.j ]]]),\n" \ + "array([ 0.1, 1. , 10. , 100. ])," + ref0 = ref_common + "\nname='sys0', outputs=1, inputs=1)" + ref1 = ref_common + " smooth=True," + \ + "\nname='sys1', outputs=1, inputs=1)" sysm = ct.frd( np.matmul(array([[1], [2]]), sys0.fresp), sys0.omega, name='sysm') From b0397e0710807a119167df72ddd5e0212ce1e078 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 7 Dec 2024 14:15:10 -0800 Subject: [PATCH 05/21] add unit tests for consistent systems repr() processing --- control/tests/iosys_test.py | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 54d6d56c8..7e80a64bb 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2339,3 +2339,54 @@ def test_signal_prefixing(fcn): assert sys.output_labels == ['yy[0]'] if sys.nstates: assert sys.state_labels == ['xx[0]', 'xx[1]'] + +@slycotonly +@pytest.mark.parametrize("fcn, spec, expected, missing", [ + (ct.ss, {}, "\nstates=4, outputs=3, inputs=2", r"dt|name"), + (ct.tf, {}, "\noutputs=3, inputs=2", r"dt|name|states"), + (ct.frd, {}, "\noutputs=3, inputs=2", r"dt|states|name"), + (ct.ss, {'dt': 0.1}, ".*\ndt=0.1,\nstates=4, outputs=3, inputs=2", r"name"), + (ct.tf, {'dt': 0.1}, ".*\ndt=0.1,\noutputs=3, inputs=2", r"name|states"), + (ct.frd, {'dt': 0.1}, + ".*\ndt=0.1,\noutputs=3, inputs=2", r"name|states"), + (ct.ss, {'dt': True}, "\ndt=True,\nstates=4, outputs=3, inputs=2", r"name"), + (ct.ss, {'dt': None}, "\ndt=None,\nstates=4, outputs=3, inputs=2", r"name"), + (ct.ss, {'dt': 0}, "\nstates=4, outputs=3, inputs=2", r"dt|name"), + (ct.ss, {'name': 'mysys'}, "\nname='mysys',", r"dt"), + (ct.tf, {'name': 'mysys'}, "\nname='mysys',", r"dt|states"), + (ct.frd, {'name': 'mysys'}, "\nname='mysys',", r"dt|states"), + (ct.ss, {'inputs': ['u1']}, + r"[\n]states=4, outputs=3, inputs=\['u1'\]", r"dt|name"), + (ct.tf, {'inputs': ['u1']}, + r"[\n]outputs=3, inputs=\['u1'\]", r"dt|name"), + (ct.frd, {'inputs': ['u1'], 'name': 'sampled'}, + r"[\n]name='sampled', outputs=3, inputs=\['u1'\]", r"dt"), + (ct.ss, {'outputs': ['y1']}, + r"[\n]states=4, outputs=\['y1'\], inputs=2", r"dt|name"), + (ct.ss, {'name': 'mysys', 'inputs': ['u1']}, + r"[\n]name='mysys', states=4, outputs=3, inputs=\['u1'\]", r"dt"), + (ct.ss, {'name': 'mysys', 'states': [ + 'long_state_1', 'long_state_2', 'long_state_3']}, + r"[\n]name='.*', states=\[.*\],[\n]outputs=3, inputs=2\)", r"dt"), +]) +def test_system_repr(fcn, spec, expected, missing): + spec['outputs'] = spec.get('outputs', 3) + spec['inputs'] = spec.get('inputs', 2) + if fcn is ct.ss: + spec['states'] = spec.get('states', 4) + + sys = ct.rss(**spec) + match fcn: + case ct.frd: + omega = np.logspace(-1, 1) + sys = fcn(sys, omega, name=spec.get('name')) + case ct.tf: + sys = fcn(sys, name=spec.get('name')) + + assert sys.shape == (sys.noutputs, sys.ninputs) + + out = repr(sys) + assert re.search(expected, out) != None + + if missing is not None: + assert re.search(missing, out) is None From 85772f58e1c9902576f816dbd27698290059ab22 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 14 Dec 2024 19:25:17 -0800 Subject: [PATCH 06/21] add iosys_repr and set default __repr__ to short form --- control/frdata.py | 31 +++++++++++++++++++++++++++++-- control/iosys.py | 17 +++++++++++++++++ control/statesp.py | 33 +++++++++++++++++++++++++++++++-- control/tests/frd_test.py | 8 ++++---- control/tests/iosys_test.py | 31 ++++++++++++++++++++++++++----- control/tests/statesp_test.py | 6 +++--- control/tests/xferfcn_test.py | 7 ++++--- control/xferfcn.py | 33 +++++++++++++++++++++++++++++++-- 8 files changed, 145 insertions(+), 21 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index ea651ad75..6e9cf90a2 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -416,10 +416,37 @@ def __str__(self): return '\n'.join(outstr) def __repr__(self): - """Loadable string representation, + return self.iosys_repr(format=self.repr_format) + + def iosys_repr(self, format='loadable'): + """Return representation of a transfer function. + + Parameters + ---------- + format : str + Format to use in creating the representation: + + * 'iosys' : [outputs] + * 'loadable' : FrequencyResponseData(response, omega[, dt[, ...]]) + + Returns + ------- + str + String representing the transfer function. + + Notes + ----- + By default, the representation for a frequency response is set to + 'iosys'. Set config.defaults['iosys.repr_format'] to change for all + I/O systems or set the `repr_format` attribute for a single system. - limited for number of data points. """ + if format == 'iosys': + return super().__repr__() + elif format != 'loadable': + raise ValueError(f"unknown format '{format}'") + + # Loadable format out = "FrequencyResponseData(\n{d},\n{w}{smooth}".format( d=repr(self.fresp), w=repr(self.omega), smooth=(self._ifunc and ", smooth=True") or "") diff --git a/control/iosys.py b/control/iosys.py index db3a5d782..475549cd6 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -31,6 +31,7 @@ 'iosys.indexed_system_name_suffix': '$indexed', 'iosys.converted_system_name_prefix': '', 'iosys.converted_system_name_suffix': '$converted', + 'iosys.repr_format': 'iosys', } @@ -183,6 +184,8 @@ def __init__( # Process timebase: if not given use default, but allow None as value self.dt = _process_dt_keyword(kwargs) + self._repr_format = kwargs.pop('repr_format', None) + # Make sure there were no other keywords if kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) @@ -241,6 +244,20 @@ def __repr__(self): return f'<{self.__class__.__name__}:{self.name}:' + \ f'{list(self.input_labels)}->{list(self.output_labels)}>' + def iosys_repr(self, format=None): + raise NotImplementedError( + f"`iosys_repr` is not implemented for {self.__class__}") + + @property + def repr_format(self): + """Set the string representation format ('iosys' or 'loadable').""" + return self._repr_format if self._repr_format is not None \ + else config.defaults['iosys.repr_format'] + + @repr_format.setter + def repr_format(self, value): + self._repr_format = value + def __str__(self): """String representation of an input/output object""" str = f"<{self.__class__.__name__}>: {self.name}\n" diff --git a/control/statesp.py b/control/statesp.py index 4a268dbd4..62ae75684 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -389,9 +389,38 @@ def __str__(self): string += f"\ndt = {self.dt}\n" return string - # represent to implement a re-loadable version def __repr__(self): - """Print state-space system in loadable form.""" + return self.iosys_repr(format=self.repr_format) + + def iosys_repr(self, format='loadable'): + """Return representation of a state sapce system. + + Parameters + ---------- + format : str + Format to use in creating the representation: + + * 'iosys' : [outputs] + * 'loadable' : StateSpace(A, B, C, D[, dt[, ...]]) + + Returns + ------- + str + String representing the transfer function. + + Notes + ----- + By default, the representation for a state space system is set to + 'iosys'. Set config.defaults['iosys.repr_format'] to change for all + I/O systems or set the `repr_format` attribute for a single system. + + """ + if format == 'iosys': + return super().__repr__() + elif format != 'loadable': + raise ValueError(f"unknown format '{format}'") + + # Loadable format out = "StateSpace(\n{A},\n{B},\n{C},\n{D}".format( A=self.A.__repr__(), B=self.B.__repr__(), C=self.C.__repr__(), D=self.D.__repr__()) diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index d2675b3c9..0af82ba7d 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -474,14 +474,14 @@ def test_repr_str(self): sysm = ct.frd( np.matmul(array([[1], [2]]), sys0.fresp), sys0.omega, name='sysm') - assert repr(sys0) == ref0 - assert repr(sys1) == ref1 + assert sys0.iosys_repr(format='loadable') == ref0 + assert sys1.iosys_repr(format='loadable') == ref1 - sys0r = eval(repr(sys0)) + sys0r = eval(sys0.iosys_repr(format='loadable')) np.testing.assert_array_almost_equal(sys0r.fresp, sys0.fresp) np.testing.assert_array_almost_equal(sys0r.omega, sys0.omega) - sys1r = eval(repr(sys1)) + sys1r = eval(sys1.iosys_repr(format='loadable')) np.testing.assert_array_almost_equal(sys1r.fresp, sys1.fresp) np.testing.assert_array_almost_equal(sys1r.omega, sys1.omega) assert(sys1._ifunc is not None) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 7e80a64bb..aca48d15e 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2369,7 +2369,8 @@ def test_signal_prefixing(fcn): 'long_state_1', 'long_state_2', 'long_state_3']}, r"[\n]name='.*', states=\[.*\],[\n]outputs=3, inputs=2\)", r"dt"), ]) -def test_system_repr(fcn, spec, expected, missing): +@pytest.mark.parametrize("format", ['iosys', 'loadable']) +def test_loadable_system_repr(fcn, spec, expected, missing, format): spec['outputs'] = spec.get('outputs', 3) spec['inputs'] = spec.get('inputs', 2) if fcn is ct.ss: @@ -2382,11 +2383,31 @@ def test_system_repr(fcn, spec, expected, missing): sys = fcn(sys, omega, name=spec.get('name')) case ct.tf: sys = fcn(sys, name=spec.get('name')) - assert sys.shape == (sys.noutputs, sys.ninputs) + # Construct the 'iosys' format + iosys_expected = f"<{sys.__class__.__name__}:{sys.name}:" \ + f"{sys.input_labels}->{sys.output_labels}>" + + # Make sure the default format is OK + out = repr(sys) + if ct.config.defaults['iosys.repr_format'] == 'iosys': + assert out == iosys_expected + else: + assert re.search(expected, out) != None + + # Now set the format to the given type and make sure things look right + sys.repr_format = format out = repr(sys) - assert re.search(expected, out) != None + if format == 'loadable': + assert re.search(expected, out) != None - if missing is not None: - assert re.search(missing, out) is None + if missing is not None: + assert re.search(missing, out) is None + + # Make sure we can change the default format back to 'iosys' + sys.repr_format = None + + # Test 'iosys', either set explicitly or via config.defaults + out = repr(sys) + assert out == iosys_expected diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 1e94c0aff..67bf67b0d 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -748,12 +748,12 @@ def test_repr(self, sys322): "array([[-2., 4.],", " [ 0., 1.]]){dt},", "name='sys322', states=3, outputs=2, inputs=2)"]) - assert repr(sys322) == ref322.format(dt='') + assert sys322.iosys_repr(format='loadable') == ref322.format(dt='') sysd = StateSpace(sys322.A, sys322.B, sys322.C, sys322.D, 0.4) - assert repr(sysd), ref322.format(dt="\ndt=0.4") + assert sysd.iosys_repr(format='loadable'), ref322.format(dt="\ndt=0.4") array = np.array # noqa - sysd2 = eval(repr(sysd)) + sysd2 = eval(sysd.iosys_repr(format='loadable')) np.testing.assert_allclose(sysd.A, sysd2.A) np.testing.assert_allclose(sysd.B, sysd2.B) np.testing.assert_allclose(sysd.C, sysd2.C) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index d0e5d5d15..e0bfb3199 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1115,15 +1115,16 @@ def test_latex_repr(self): "dt=0.5,\n" "outputs=2, inputs=2)"), ]) - def test_repr(self, Hargs, ref): + def test_loadable_repr(self, Hargs, ref): """Test __repr__ printout.""" H = TransferFunction(*Hargs) - assert repr(H) == ref + rep = H.iosys_repr(format='loadable') + assert rep == ref # and reading back array = np.array # noqa - H2 = eval(H.__repr__()) + H2 = eval(rep) for p in range(len(H.num)): for m in range(len(H.num[0])): np.testing.assert_array_almost_equal( diff --git a/control/xferfcn.py b/control/xferfcn.py index 40c3a8094..685acb570 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -488,9 +488,38 @@ def __str__(self, var=None): return outstr - # represent to implement a re-loadable version def __repr__(self): - """Print transfer function in loadable form.""" + return self.iosys_repr(format=self.repr_format) + + def iosys_repr(self, format='loadable'): + """Return representation of a transfer function. + + Parameters + ---------- + format : str + Format to use in creating the representation: + + * 'iosys' : [outputs] + * 'loadable' : TransferFunction(num, den[, dt[, ...]]) + + Returns + ------- + str + String representing the transfer function. + + Notes + ----- + By default, the representation for a transfer function is set to + 'iosys'. Set config.defaults['iosys.repr_format'] to change for all + I/O systems or set the `repr_format` attribute for a single system. + + """ + if format == 'iosys': + return super().__repr__() + elif format != 'loadable': + raise ValueError(f"unknown format '{format}'") + + # Loadable format if self.issiso(): out = "TransferFunction(\n{num},\n{den}".format( num=self.num[0][0].__repr__(), den=self.den[0][0].__repr__()) From 9897ab212c7e15884ce34119cc89c09bb0048d04 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 17 Dec 2024 13:03:00 -0800 Subject: [PATCH 07/21] use NumPy printoptions in LaTeX representations --- control/config.py | 16 ++++++----- control/statesp.py | 20 ++++++++++---- control/tests/config_test.py | 13 +++++++++ control/tests/lti_test.py | 52 ++++++++++++++++++++++++++++++++---- control/xferfcn.py | 9 ++++++- 5 files changed, 93 insertions(+), 17 deletions(-) diff --git a/control/config.py b/control/config.py index c5a59250b..2a7404945 100644 --- a/control/config.py +++ b/control/config.py @@ -266,7 +266,7 @@ def use_legacy_defaults(version): Parameters ---------- version : string - Version number of the defaults desired. Ranges from '0.1' to '0.8.4'. + Version number of the defaults desired. Ranges from '0.1' to '0.10.1'. Examples -------- @@ -279,26 +279,26 @@ def use_legacy_defaults(version): (major, minor, patch) = (None, None, None) # default values # Early release tag format: REL-0.N - match = re.match("REL-0.([12])", version) + match = re.match(r"^REL-0.([12])$", version) if match: (major, minor, patch) = (0, int(match.group(1)), 0) # Early release tag format: control-0.Np - match = re.match("control-0.([3-6])([a-d])", version) + match = re.match(r"^control-0.([3-6])([a-d])$", version) if match: (major, minor, patch) = \ (0, int(match.group(1)), ord(match.group(2)) - ord('a') + 1) # Early release tag format: v0.Np - match = re.match("[vV]?0.([3-6])([a-d])", version) + match = re.match(r"^[vV]?0\.([3-6])([a-d])$", version) if match: (major, minor, patch) = \ (0, int(match.group(1)), ord(match.group(2)) - ord('a') + 1) # Abbreviated version format: vM.N or M.N - match = re.match("([vV]?[0-9]).([0-9])", version) + match = re.match(r"^[vV]?([0-9]*)\.([0-9]*)$", version) if match: (major, minor, patch) = \ (int(match.group(1)), int(match.group(2)), 0) # Standard version format: vM.N.P or M.N.P - match = re.match("[vV]?([0-9]).([0-9]).([0-9])", version) + match = re.match(r"^[vV]?([0-9]*)\.([0-9]*)\.([0-9]*)$", version) if match: (major, minor, patch) = \ (int(match.group(1)), int(match.group(2)), int(match.group(3))) @@ -311,6 +311,10 @@ def use_legacy_defaults(version): # reset_defaults() # start from a clean slate + # Verions 0.10.2 + if major == 0 and minor <= 10 and patch < 2: + set_defaults('iosys', repr_format='loadable') + # Version 0.9.2: if major == 0 and minor < 9 or (minor == 9 and patch < 2): from math import inf diff --git a/control/statesp.py b/control/statesp.py index 62ae75684..9f7b5e831 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -14,6 +14,7 @@ """ import math +import sys from collections.abc import Iterable from copy import deepcopy from warnings import warn @@ -21,8 +22,8 @@ import numpy as np import scipy as sp import scipy.linalg -from numpy import any, asarray, concatenate, cos, delete, empty, exp, eye, \ - isinf, ones, pad, sin, squeeze, zeros +from numpy import any, array, asarray, concatenate, cos, delete, empty, \ + exp, eye, isinf, ones, pad, sin, squeeze, zeros from numpy.linalg import LinAlgError, eigvals, matrix_rank, solve from numpy.random import rand, randn from scipy.signal import StateSpace as signalStateSpace @@ -444,6 +445,10 @@ def _latex_partitioned_stateless(self): ------- s : string with LaTeX representation of model """ + # Apply NumPy formatting + with np.printoptions(threshold=sys.maxsize): + D = eval(repr(self.D)) + lines = [ r'$$', (r'\left(' @@ -451,7 +456,7 @@ def _latex_partitioned_stateless(self): + r'{' + 'rll' * self.ninputs + '}') ] - for Di in asarray(self.D): + for Di in asarray(D): lines.append('&'.join(_f2s(Dij) for Dij in Di) + '\\\\') @@ -476,6 +481,11 @@ def _latex_partitioned(self): if self.nstates == 0: return self._latex_partitioned_stateless() + # Apply NumPy formatting + with np.printoptions(threshold=sys.maxsize): + A, B, C, D = ( + eval(repr(getattr(self, M))) for M in ['A', 'B', 'C', 'D']) + lines = [ r'$$', (r'\left(' @@ -483,12 +493,12 @@ def _latex_partitioned(self): + r'{' + 'rll' * self.nstates + '|' + 'rll' * self.ninputs + '}') ] - for Ai, Bi in zip(asarray(self.A), asarray(self.B)): + for Ai, Bi in zip(asarray(A), asarray(B)): lines.append('&'.join([_f2s(Aij) for Aij in Ai] + [_f2s(Bij) for Bij in Bi]) + '\\\\') lines.append(r'\hline') - for Ci, Di in zip(asarray(self.C), asarray(self.D)): + for Ci, Di in zip(asarray(C), asarray(D)): lines.append('&'.join([_f2s(Cij) for Cij in Ci] + [_f2s(Dij) for Dij in Di]) + '\\\\') diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 947dc95aa..988699a09 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -319,3 +319,16 @@ def test_system_indexing(self): indexed_system_name_suffix='POST') sys2 = sys[1:, 1:] assert sys2.name == 'PRE' + sys.name + 'POST' + + def test_legacy_repr_format(self): + from ..statesp import StateSpace + from numpy import array + + sys = ct.ss([[1]], [[1]], [[1]], [[0]]) + with pytest.raises(SyntaxError, match="invalid syntax"): + new = eval(repr(sys)) # iosys is default + + ct.use_legacy_defaults('0.10.1') # loadable is default + new = eval(repr(sys)) + for attr in ['A', 'B', 'C', 'D']: + assert getattr(sys, attr) == getattr(sys, attr) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 5359ceea3..b39fabe93 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -1,15 +1,19 @@ """lti_test.py""" +import re + import numpy as np import pytest -from .conftest import editsdefaults import control as ct -from control import c2d, tf, ss, tf2ss, NonlinearIOSystem -from control.lti import LTI, evalfr, damp, dcgain, zeros, poles, bandwidth -from control import common_timebase, isctime, isdtime, issiso -from control.tests.conftest import slycotonly +from control import NonlinearIOSystem, c2d, common_timebase, isctime, \ + isdtime, issiso, ss, tf, tf2ss from control.exception import slycot_check +from control.lti import LTI, bandwidth, damp, dcgain, evalfr, poles, zeros +from control.tests.conftest import slycotonly + +from .conftest import editsdefaults + class TestLTI: @pytest.mark.parametrize("fun, args", [ @@ -368,3 +372,41 @@ def test_scalar_algebra(op, fcn): scaled = getattr(sys, op)(2) np.testing.assert_almost_equal(getattr(sys(1j), op)(2), scaled(1j)) + + +@pytest.mark.parametrize( + "fcn, args, kwargs, suppress, " + + "repr_expected, str_expected, latex_expected", [ + (ct.ss, (-1e-12, 1, 2, 3), {}, False, + r"StateSpace\([\s]*array\(\[\[-1.e-12\]\]\).*", + None, # standard Numpy formatting + r"10\^\{-12\}"), + (ct.ss, (-1e-12, 1, 3, 3), {}, True, + r"StateSpace\([\s]*array\(\[\[-0\.\]\]\).*", + None, # standard Numpy formatting + r"-0"), + (ct.tf, ([1, 1e-12, 1], [1, 2, 1]), {}, False, + r"\[1\.e\+00, 1\.e-12, 1.e\+00\]", + r"s\^2 \+ 1e-12 s \+ 1", + r"1 \\times 10\^\{-12\}"), + (ct.tf, ([1, 1e-12, 1], [1, 2, 1]), {}, True, + r"\[1\., 0., 1.\]", + r"s\^2 \+ 1", + r"\{s\^2 \+ 1\}"), +]) +@pytest.mark.usefixtures("editsdefaults") +def test_printoptions( + fcn, args, kwargs, suppress, + repr_expected, str_expected, latex_expected): + sys = fcn(*args, **kwargs) + + with np.printoptions(suppress=suppress): + # Test loadable representation + assert re.search(repr_expected, sys.iosys_repr('loadable')) is not None + + # Test string representation + if str_expected is not None: + assert re.search(str_expected, str(sys)) is not None + + # Test LaTeX representation + assert re.search(latex_expected, sys._repr_latex_()) is not None diff --git a/control/xferfcn.py b/control/xferfcn.py index 685acb570..24ac3b749 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -13,6 +13,7 @@ """ +import sys from collections.abc import Iterable from copy import deepcopy from itertools import chain, product @@ -1349,9 +1350,12 @@ def _c2d_matched(sysC, Ts, **kwargs): # Borrowed from poly1d library def _tf_polynomial_to_string(coeffs, var='s'): """Convert a transfer function polynomial to a string.""" - thestr = "0" + # Apply NumPy formatting + with np.printoptions(threshold=sys.maxsize): + coeffs = eval(repr(coeffs)) + # Compute the number of coefficients N = len(coeffs) - 1 @@ -1396,6 +1400,9 @@ def _tf_polynomial_to_string(coeffs, var='s'): def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'): """Convert a factorized polynomial to a string.""" + # Apply NumPy formatting + with np.printoptions(threshold=sys.maxsize): + roots = eval(repr(roots)) if roots.size == 0: return _float2str(gain) From 376c28e461477f74bcef47edcc5be5abe4219440 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 20 Dec 2024 11:29:21 -0800 Subject: [PATCH 08/21] refactor I/O system repr() processing via iosys_repr and _repr_ --- control/config.py | 2 +- control/frdata.py | 32 +-------- control/iosys.py | 73 ++++++++++++++++--- control/statesp.py | 37 ++-------- control/tests/frd_test.py | 8 +-- control/tests/iosys_test.py | 130 +++++++++++++++++----------------- control/tests/lti_test.py | 2 +- control/tests/namedio_test.py | 6 +- control/tests/statesp_test.py | 24 +++---- control/tests/xferfcn_test.py | 11 +-- control/xferfcn.py | 42 ++--------- 11 files changed, 169 insertions(+), 198 deletions(-) diff --git a/control/config.py b/control/config.py index 2a7404945..d7333c682 100644 --- a/control/config.py +++ b/control/config.py @@ -313,7 +313,7 @@ def use_legacy_defaults(version): # Verions 0.10.2 if major == 0 and minor <= 10 and patch < 2: - set_defaults('iosys', repr_format='loadable') + set_defaults('iosys', repr_format='eval') # Version 0.9.2: if major == 0 and minor < 9 or (minor == 9 and patch < 2): diff --git a/control/frdata.py b/control/frdata.py index 6e9cf90a2..489197e75 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -415,37 +415,7 @@ def __str__(self): return '\n'.join(outstr) - def __repr__(self): - return self.iosys_repr(format=self.repr_format) - - def iosys_repr(self, format='loadable'): - """Return representation of a transfer function. - - Parameters - ---------- - format : str - Format to use in creating the representation: - - * 'iosys' : [outputs] - * 'loadable' : FrequencyResponseData(response, omega[, dt[, ...]]) - - Returns - ------- - str - String representing the transfer function. - - Notes - ----- - By default, the representation for a frequency response is set to - 'iosys'. Set config.defaults['iosys.repr_format'] to change for all - I/O systems or set the `repr_format` attribute for a single system. - - """ - if format == 'iosys': - return super().__repr__() - elif format != 'loadable': - raise ValueError(f"unknown format '{format}'") - + def _repr_eval_(self): # Loadable format out = "FrequencyResponseData(\n{d},\n{w}{smooth}".format( d=repr(self.fresp), w=repr(self.omega), diff --git a/control/iosys.py b/control/iosys.py index 475549cd6..94ffe2016 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -16,7 +16,7 @@ from .exception import ControlIndexError __all__ = ['InputOutputSystem', 'NamedSignal', 'issiso', 'timebase', - 'common_timebase', 'isdtime', 'isctime'] + 'common_timebase', 'isdtime', 'isctime', 'iosys_repr'] # Define module default parameter values _iosys_defaults = { @@ -31,7 +31,7 @@ 'iosys.indexed_system_name_suffix': '$indexed', 'iosys.converted_system_name_prefix': '', 'iosys.converted_system_name_suffix': '$converted', - 'iosys.repr_format': 'iosys', + 'iosys.repr_format': 'info', } @@ -163,6 +163,8 @@ class InputOutputSystem(object): Set the prefix for output signals. Default = 'y'. state_prefix : string, optional Set the prefix for state signals. Default = 'x'. + repr_format : str + String representation format. See :func:`control.iosys_repr`. """ # Allow NDarray * IOSystem to give IOSystem._rmul_() priority @@ -241,16 +243,30 @@ def _generic_name_check(self): nstates = None def __repr__(self): - return f'<{self.__class__.__name__}:{self.name}:' + \ - f'{list(self.input_labels)}->{list(self.output_labels)}>' + return iosys_repr(self, format=self.repr_format) - def iosys_repr(self, format=None): - raise NotImplementedError( - f"`iosys_repr` is not implemented for {self.__class__}") + def _repr_info_(self): + return f'<{self.__class__.__name__} {self.name}: ' + \ + f'{list(self.input_labels)} -> {list(self.output_labels)}>' @property def repr_format(self): - """Set the string representation format ('iosys' or 'loadable').""" + """String representation format. + + Format used in creating the representation for the system: + + * 'info' : [outputs] + * 'eval' : system specific, loadable representation + * 'latex' : latex representation of the object + + The default representation for an input/output is set to 'info'. + This value can be changed for an individual system by setting the + `repr_format` parameter when the system is created or by setting + the `repr_format` property after system creation. Set + config.defaults['iosys.repr_format'] to change for all I/O systems + or use the `repr_format` parameter/attribute for a single system. + + """ return self._repr_format if self._repr_format is not None \ else config.defaults['iosys.repr_format'] @@ -740,6 +756,47 @@ def isctime(sys=None, dt=None, strict=False): return sys.isctime(strict) +def iosys_repr(sys, format=None): + """Return representation of an I/O system. + + Parameters + ---------- + sys : InputOutputSystem + System for which the representation is generated. + format : str + Format to use in creating the representation: + + * 'info' : [outputs] + * 'eval' : system specific, loadable representation + * 'latex' : latex representation of the object + + Returns + ------- + str + String representing the input/output system. + + Notes + ----- + By default, the representation for an input/output is set to 'info'. + Set config.defaults['iosys.repr_format'] to change for all I/O systems + or use the `repr_format` parameter for a single system. + + Jupyter will automatically use the 'latex' representation for I/O + systems, when available. + + """ + format = config.defaults['iosys.repr_format'] if format is None else format + match format: + case 'info': + return sys._repr_info_() + case 'eval': + return sys._repr_eval_() + case 'latex': + return sys._repr_latex_() + case _: + raise ValueError(f"format '{format}' unknown") + + # Utility function to parse iosys keywords def _process_iosys_keywords( keywords={}, defaults={}, static=False, end=False): diff --git a/control/statesp.py b/control/statesp.py index 9f7b5e831..88012ec4d 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -34,7 +34,7 @@ from .frdata import FrequencyResponseData from .iosys import InputOutputSystem, NamedSignal, _process_dt_keyword, \ _process_iosys_keywords, _process_signal_list, _process_subsys_index, \ - common_timebase, isdtime, issiso + common_timebase, iosys_repr, isdtime, issiso from .lti import LTI, _process_frequency_response from .nlsys import InterconnectedSystem, NonlinearIOSystem import control @@ -390,37 +390,7 @@ def __str__(self): string += f"\ndt = {self.dt}\n" return string - def __repr__(self): - return self.iosys_repr(format=self.repr_format) - - def iosys_repr(self, format='loadable'): - """Return representation of a state sapce system. - - Parameters - ---------- - format : str - Format to use in creating the representation: - - * 'iosys' : [outputs] - * 'loadable' : StateSpace(A, B, C, D[, dt[, ...]]) - - Returns - ------- - str - String representing the transfer function. - - Notes - ----- - By default, the representation for a state space system is set to - 'iosys'. Set config.defaults['iosys.repr_format'] to change for all - I/O systems or set the `repr_format` attribute for a single system. - - """ - if format == 'iosys': - return super().__repr__() - elif format != 'loadable': - raise ValueError(f"unknown format '{format}'") - + def _repr_eval_(self): # Loadable format out = "StateSpace(\n{A},\n{B},\n{C},\n{D}".format( A=self.A.__repr__(), B=self.B.__repr__(), @@ -450,6 +420,7 @@ def _latex_partitioned_stateless(self): D = eval(repr(self.D)) lines = [ + self._repr_info_(), r'$$', (r'\left(' + r'\begin{array}' @@ -487,6 +458,7 @@ def _latex_partitioned(self): eval(repr(getattr(self, M))) for M in ['A', 'B', 'C', 'D']) lines = [ + self._repr_info_(), r'$$', (r'\left(' + r'\begin{array}' @@ -521,6 +493,7 @@ def _latex_separate(self): s : string with LaTeX representation of model """ lines = [ + self._repr_info_(), r'$$', r'\begin{array}{ll}', ] diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 0af82ba7d..c9f5ca7f8 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -474,14 +474,14 @@ def test_repr_str(self): sysm = ct.frd( np.matmul(array([[1], [2]]), sys0.fresp), sys0.omega, name='sysm') - assert sys0.iosys_repr(format='loadable') == ref0 - assert sys1.iosys_repr(format='loadable') == ref1 + assert ct.iosys_repr(sys0, format='eval') == ref0 + assert ct.iosys_repr(sys1, format='eval') == ref1 - sys0r = eval(sys0.iosys_repr(format='loadable')) + sys0r = eval(ct.iosys_repr(sys0, format='eval')) np.testing.assert_array_almost_equal(sys0r.fresp, sys0.fresp) np.testing.assert_array_almost_equal(sys0r.omega, sys0.omega) - sys1r = eval(sys1.iosys_repr(format='loadable')) + sys1r = eval(ct.iosys_repr(sys1, format='eval')) np.testing.assert_array_almost_equal(sys1r.fresp, sys1.fresp) np.testing.assert_array_almost_equal(sys1r.omega, sys1.omega) assert(sys1._ifunc is not None) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index aca48d15e..45582f569 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2287,58 +2287,6 @@ def test_signal_indexing(): with pytest.raises(IndexError, match=r"signal name\(s\) not valid"): resp.outputs['y[0]', 'u[0]'] -@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd, ct.nlsys, fs.flatsys]) -def test_relabeling(fcn): - sys = ct.rss(1, 1, 1, name="sys") - - # Rename the inputs, outputs, (states,) system - match fcn: - case ct.tf: - sys = fcn(sys, inputs='u', outputs='y', name='new') - case ct.frd: - sys = fcn(sys, [0.1, 1, 10], inputs='u', outputs='y', name='new') - case _: - sys = fcn(sys, inputs='u', outputs='y', states='x', name='new') - - assert sys.input_labels == ['u'] - assert sys.output_labels == ['y'] - if sys.nstates: - assert sys.state_labels == ['x'] - assert sys.name == 'new' - - -@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd, ct.nlsys, fs.flatsys]) -def test_signal_prefixing(fcn): - sys = ct.rss(2, 1, 1) - - # Recreate the system in different forms, with non-standard prefixes - match fcn: - case ct.ss: - sys = ct.ss( - sys.A, sys.B, sys.C, sys.D, state_prefix='xx', - input_prefix='uu', output_prefix='yy') - case ct.tf: - sys = ct.tf(sys) - sys = fcn(sys.num, sys.den, input_prefix='uu', output_prefix='yy') - case ct.frd: - freq = [0.1, 1, 10] - data = [sys(w * 1j) for w in freq] - sys = fcn(data, freq, input_prefix='uu', output_prefix='yy') - case ct.nlsys: - sys = ct.nlsys(sys) - sys = fcn( - sys.updfcn, sys.outfcn, inputs=1, outputs=1, states=2, - state_prefix='xx', input_prefix='uu', output_prefix='yy') - case fs.flatsys: - sys = fs.flatsys(sys) - sys = fcn( - sys.forward, sys.reverse, inputs=1, outputs=1, states=2, - state_prefix='xx', input_prefix='uu', output_prefix='yy') - - assert sys.input_labels == ['uu[0]'] - assert sys.output_labels == ['yy[0]'] - if sys.nstates: - assert sys.state_labels == ['xx[0]', 'xx[1]'] @slycotonly @pytest.mark.parametrize("fcn, spec, expected, missing", [ @@ -2369,8 +2317,8 @@ def test_signal_prefixing(fcn): 'long_state_1', 'long_state_2', 'long_state_3']}, r"[\n]name='.*', states=\[.*\],[\n]outputs=3, inputs=2\)", r"dt"), ]) -@pytest.mark.parametrize("format", ['iosys', 'loadable']) -def test_loadable_system_repr(fcn, spec, expected, missing, format): +@pytest.mark.parametrize("format", ['info', 'eval']) +def test_iosys_repr(fcn, spec, expected, missing, format): spec['outputs'] = spec.get('outputs', 3) spec['inputs'] = spec.get('inputs', 2) if fcn is ct.ss: @@ -2385,29 +2333,83 @@ def test_loadable_system_repr(fcn, spec, expected, missing, format): sys = fcn(sys, name=spec.get('name')) assert sys.shape == (sys.noutputs, sys.ninputs) - # Construct the 'iosys' format - iosys_expected = f"<{sys.__class__.__name__}:{sys.name}:" \ - f"{sys.input_labels}->{sys.output_labels}>" + # Construct the 'info' format + info_expected = f"<{sys.__class__.__name__} {sys.name}: " \ + f"{sys.input_labels} -> {sys.output_labels}>" # Make sure the default format is OK out = repr(sys) - if ct.config.defaults['iosys.repr_format'] == 'iosys': - assert out == iosys_expected + if ct.config.defaults['iosys.repr_format'] == 'info': + assert out == info_expected else: assert re.search(expected, out) != None - + # Now set the format to the given type and make sure things look right sys.repr_format = format out = repr(sys) - if format == 'loadable': + if format == 'eval': assert re.search(expected, out) != None if missing is not None: assert re.search(missing, out) is None - # Make sure we can change the default format back to 'iosys' + # Make sure we can change the default format back to 'info' sys.repr_format = None - # Test 'iosys', either set explicitly or via config.defaults + # Test 'info', either set explicitly or via config.defaults out = repr(sys) - assert out == iosys_expected + assert out == info_expected + + +@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd, ct.nlsys, fs.flatsys]) +def test_relabeling(fcn): + sys = ct.rss(1, 1, 1, name="sys") + + # Rename the inputs, outputs, (states,) system + match fcn: + case ct.tf: + sys = fcn(sys, inputs='u', outputs='y', name='new') + case ct.frd: + sys = fcn(sys, [0.1, 1, 10], inputs='u', outputs='y', name='new') + case _: + sys = fcn(sys, inputs='u', outputs='y', states='x', name='new') + + assert sys.input_labels == ['u'] + assert sys.output_labels == ['y'] + if sys.nstates: + assert sys.state_labels == ['x'] + assert sys.name == 'new' + + +@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd, ct.nlsys, fs.flatsys]) +def test_signal_prefixing(fcn): + sys = ct.rss(2, 1, 1) + + # Recreate the system in different forms, with non-standard prefixes + match fcn: + case ct.ss: + sys = ct.ss( + sys.A, sys.B, sys.C, sys.D, state_prefix='xx', + input_prefix='uu', output_prefix='yy') + case ct.tf: + sys = ct.tf(sys) + sys = fcn(sys.num, sys.den, input_prefix='uu', output_prefix='yy') + case ct.frd: + freq = [0.1, 1, 10] + data = [sys(w * 1j) for w in freq] + sys = fcn(data, freq, input_prefix='uu', output_prefix='yy') + case ct.nlsys: + sys = ct.nlsys(sys) + sys = fcn( + sys.updfcn, sys.outfcn, inputs=1, outputs=1, states=2, + state_prefix='xx', input_prefix='uu', output_prefix='yy') + case fs.flatsys: + sys = fs.flatsys(sys) + sys = fcn( + sys.forward, sys.reverse, inputs=1, outputs=1, states=2, + state_prefix='xx', input_prefix='uu', output_prefix='yy') + + assert sys.input_labels == ['uu[0]'] + assert sys.output_labels == ['yy[0]'] + if sys.nstates: + assert sys.state_labels == ['xx[0]', 'xx[1]'] diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index b39fabe93..debc4b941 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -402,7 +402,7 @@ def test_printoptions( with np.printoptions(suppress=suppress): # Test loadable representation - assert re.search(repr_expected, sys.iosys_repr('loadable')) is not None + assert re.search(repr_expected, ct.iosys_repr(sys, 'eval')) is not None # Test string representation if str_expected is not None: diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index f702e704b..79b332620 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -35,7 +35,7 @@ def test_named_ss(): assert sys.output_labels == ['y[0]', 'y[1]'] assert sys.state_labels == ['x[0]', 'x[1]'] assert ct.InputOutputSystem.__repr__(sys) == \ - "['y[0]', 'y[1]']>" + " ['y[0]', 'y[1]']>" # Pass the names as arguments sys = ct.ss( @@ -47,7 +47,7 @@ def test_named_ss(): assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2'] assert ct.InputOutputSystem.__repr__(sys) == \ - "['y1', 'y2']>" + " ['y1', 'y2']>" # Do the same with rss sys = ct.rss(['x1', 'x2', 'x3'], ['y1', 'y2'], 'u1', name='random') @@ -57,7 +57,7 @@ def test_named_ss(): assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2', 'x3'] assert ct.InputOutputSystem.__repr__(sys) == \ - "['y1', 'y2']>" + " ['y1', 'y2']>" # List of classes that are expected diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 67bf67b0d..45bc430ed 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -748,12 +748,12 @@ def test_repr(self, sys322): "array([[-2., 4.],", " [ 0., 1.]]){dt},", "name='sys322', states=3, outputs=2, inputs=2)"]) - assert sys322.iosys_repr(format='loadable') == ref322.format(dt='') + assert ct.iosys_repr(sys322, format='eval') == ref322.format(dt='') sysd = StateSpace(sys322.A, sys322.B, sys322.C, sys322.D, 0.4) - assert sysd.iosys_repr(format='loadable'), ref322.format(dt="\ndt=0.4") + assert ct.iosys_repr(sysd, format='eval'), ref322.format(dt="\ndt=0.4") array = np.array # noqa - sysd2 = eval(sysd.iosys_repr(format='loadable')) + sysd2 = eval(ct.iosys_repr(sysd, format='eval')) np.testing.assert_allclose(sysd.A, sysd2.A) np.testing.assert_allclose(sysd.B, sysd2.B) np.testing.assert_allclose(sysd.C, sysd2.C) @@ -1067,23 +1067,23 @@ def test_statespace_defaults(self): [[1.2345, -2e-200], [-1, 0]]) LTX_G1_REF = { - 'p3_p' : '$$\n\\left(\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$', + 'p3_p' : " ['y[0]']>\n$$\n\\left(\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", - 'p5_p' : '$$\n\\left(\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$', + 'p5_p' : " ['y[0]']>\n$$\n\\left(\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", - 'p3_s' : '$$\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$', + 'p3_s' : " ['y[0]']>\n$$\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", - 'p5_s' : '$$\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$', + 'p5_s' : " ['y[0]']>\n$$\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", } LTX_G2_REF = { - 'p3_p' : '$$\n\\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$', + 'p3_p' : " ['y[0]', 'y[1]']>\n$$\n\\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", - 'p5_p' : '$$\n\\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$', + 'p5_p' : " ['y[0]', 'y[1]']>\n$$\n\\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", - 'p3_s' : '$$\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$', + 'p3_s' : " ['y[0]', 'y[1]']>\n$$\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", - 'p5_s' : '$$\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$', + 'p5_s' : " ['y[0]', 'y[1]']>\n$$\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", } refkey_n = {None: 'p3', '.3g': 'p3', '.5g': 'p5'} @@ -1115,7 +1115,7 @@ def test_latex_repr(gmats, ref, dt, dtref, repr_type, num_format, editsdefaults) if repr_type is not None: set_defaults('statesp', latex_repr_type=repr_type) - g = StateSpace(*(gmats+(dt,))) + g = StateSpace(*(gmats+(dt,)), name='sys') refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type]) dt_latex = dtref.format(dt=dt, fmt=defaults['statesp.latex_num_format']) ref_latex = ref[refkey][:-3] + dt_latex + ref[refkey][-3:] diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index e0bfb3199..e5ead0c5e 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1064,16 +1064,17 @@ def test_size_mismatch(self): def test_latex_repr(self): """Test latex printout for TransferFunction""" Hc = TransferFunction([1e-5, 2e5, 3e-4], - [1.2e34, 2.3e-4, 2.3e-45]) + [1.2e34, 2.3e-4, 2.3e-45], name='sys') Hd = TransferFunction([1e-5, 2e5, 3e-4], [1.2e34, 2.3e-4, 2.3e-45], - .1) + .1, name='sys') # TODO: make the multiplication sign configurable expmul = r'\times' for var, H, suffix in zip(['s', 'z'], [Hc, Hd], - ['', r'\quad dt = 0.1']): - ref = (r'$$\frac{' + ['', r'~,~dt = 0.1']): + ref = (r" ['y[0]']>" + r'$$\frac{' r'1 ' + expmul + ' 10^{-5} ' + var + '^2 ' r'+ 2 ' + expmul + ' 10^{5} ' + var + ' + 0.0003' r'}{' @@ -1119,7 +1120,7 @@ def test_loadable_repr(self, Hargs, ref): """Test __repr__ printout.""" H = TransferFunction(*Hargs) - rep = H.iosys_repr(format='loadable') + rep = ct.iosys_repr(H, format='eval') assert rep == ref # and reading back diff --git a/control/xferfcn.py b/control/xferfcn.py index 24ac3b749..a98d8bbd6 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -489,41 +489,12 @@ def __str__(self, var=None): return outstr - def __repr__(self): - return self.iosys_repr(format=self.repr_format) - - def iosys_repr(self, format='loadable'): - """Return representation of a transfer function. - - Parameters - ---------- - format : str - Format to use in creating the representation: - - * 'iosys' : [outputs] - * 'loadable' : TransferFunction(num, den[, dt[, ...]]) - - Returns - ------- - str - String representing the transfer function. - - Notes - ----- - By default, the representation for a transfer function is set to - 'iosys'. Set config.defaults['iosys.repr_format'] to change for all - I/O systems or set the `repr_format` attribute for a single system. - - """ - if format == 'iosys': - return super().__repr__() - elif format != 'loadable': - raise ValueError(f"unknown format '{format}'") - + def _repr_eval_(self): # Loadable format if self.issiso(): out = "TransferFunction(\n{num},\n{den}".format( - num=self.num[0][0].__repr__(), den=self.den[0][0].__repr__()) + num=self.num_array[0, 0].__repr__(), + den=self.den_array[0, 0].__repr__()) else: out = "TransferFunction(\n[" for entry in [self.num_array, self.den_array]: @@ -553,13 +524,10 @@ def iosys_repr(self, format='loadable'): def _repr_latex_(self, var=None): """LaTeX representation of transfer function, for Jupyter notebook.""" - mimo = not self.issiso() - if var is None: var = 's' if self.isctime() else 'z' - - out = ['$$'] + out = [self._repr_info_(), '$$'] if mimo: out.append(r"\begin{bmatrix}") @@ -595,7 +563,7 @@ def _repr_latex_(self, var=None): # See if this is a discrete time system with specific sampling time if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: - out += [r"\quad dt = ", str(self.dt)] + out += [r"~,~dt = ", str(self.dt)] out.append("$$") From 42eaff9d7ceabbabdf4844206497c43772f6619d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 20 Dec 2024 12:23:21 -0800 Subject: [PATCH 09/21] small docstring and comment updates --- control/statesp.py | 8 -------- control/xferfcn.py | 1 - 2 files changed, 9 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 88012ec4d..f9d3ec11c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1595,14 +1595,6 @@ def ss(*args, **kwargs): name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. - remove_useless_states : bool, optional - If `True`, remove states that have no effect on the input/output - dynamics. If not specified, the value is read from - `config.defaults['statesp.remove_useless_states']` (default = False). - method : str, optional - Set the method used for converting a transfer function to a state - space system. Current methods are 'slycot' and 'scipy'. If set to - None (default), try 'slycot' first and then 'scipy' (SISO only). Returns ------- diff --git a/control/xferfcn.py b/control/xferfcn.py index a98d8bbd6..1c794c52c 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -195,7 +195,6 @@ def __init__(self, *args, **kwargs): % type(args[0])) num = args[0].num den = args[0].den - # TODO: copy over signal names else: raise TypeError("Needs 1, 2 or 3 arguments; received %i." From 964c3aa914962f8e769d99a72c62be6b0dad9b0b Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 21 Dec 2024 23:14:59 -0800 Subject: [PATCH 10/21] expanded __str__ for IC systems (subsys list + connection map) --- control/nlsys.py | 67 ++++++++++++++++++++++++++++++++++- control/statesp.py | 4 +-- control/tests/nlsys_test.py | 48 +++++++++++++++++++++++++ control/tests/statesp_test.py | 2 +- 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index beb2566e7..03ef873f7 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -26,7 +26,7 @@ from . import config from .iosys import InputOutputSystem, _parse_spec, _process_iosys_keywords, \ - _process_signal_list, common_timebase, isctime, isdtime + _process_signal_list, common_timebase, iosys_repr, isctime, isdtime from .timeresp import _check_convert_array, _process_time_response, \ TimeResponseData, TimeResponseList @@ -778,6 +778,71 @@ def outfcn(t, x, u, params): index + "; combining with previous entries") self.output_map[index + j, ylist_index] += gain + def __str__(self): + import textwrap + out = super().__str__() + + out += f"\n\nSubsystems ({len(self.syslist)}):\n" + for sys in self.syslist: + out += "\n".join(textwrap.wrap( + iosys_repr(sys, format='info'), width=78, + initial_indent=" * ", subsequent_indent=" ")) + "\n" + + # Build a list of input, output, and inpout signals + input_list, output_list, inpout_list = [], [], [] + for sys in self.syslist: + input_list += [sys.name + "." + lbl for lbl in sys.input_labels] + output_list += [sys.name + "." + lbl for lbl in sys.output_labels] + inpout_list = input_list + output_list + + # Define a utility function to generate the signal + def cxn_string(signal, gain, first): + if gain == 1: + return (" + " if not first else "") + f"{signal}" + elif gain == -1: + return (" - " if not first else "-") + f"{signal}" + elif gain > 0: + return (" + " if not first else "") + f"{gain} * {signal}" + elif gain < 0: + return (" - " if not first else "-") + \ + f"{abs(gain)} * {signal}" + + out += f"\nConnections:\n" + for i in range(len(input_list)): + first = True + cxn = f"{input_list[i]} <- " + if np.any(self.connect_map[i]): + for j in range(len(output_list)): + if self.connect_map[i, j]: + cxn += cxn_string( + output_list[j], self.connect_map[i,j], first) + first = False + if np.any(self.input_map[i]): + for j in range(len(self.input_labels)): + if self.input_map[i, j]: + cxn += cxn_string( + self.input_labels[j], self.input_map[i, j], first) + first = False + out += "\n".join(textwrap.wrap( + cxn, width=78, initial_indent=" * ", + subsequent_indent=" ")) + "\n" + + out += f"\nOutputs:\n" + for i in range(len(self.output_labels)): + first = True + cxn = f"{self.output_labels[i]} <- " + if np.any(self.output_map[i]): + for j in range(len(inpout_list)): + if self.output_map[i, j]: + cxn += cxn_string( + output_list[j], self.output_map[i, j], first) + first = False + out += "\n".join(textwrap.wrap( + cxn, width=78, initial_indent=" * ", + subsequent_indent=" ")) + "\n" + + return out[:-1] + def _update_params(self, params, warning=False): for sys in self.syslist: local = sys.params.copy() # start with system parameters diff --git a/control/statesp.py b/control/statesp.py index f9d3ec11c..7324af814 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -381,8 +381,8 @@ def _remove_useless_states(self): def __str__(self): """Return string representation of the state space system.""" string = f"{InputOutputSystem.__str__(self)}\n\n" - string += "\n".join([ - "{} = {}\n".format(Mvar, + string += "\n\n".join([ + "{} = {}".format(Mvar, "\n ".join(str(M).splitlines())) for Mvar, M in zip(["A", "B", "C", "D"], [self.A, self.B, self.C, self.D])]) diff --git a/control/tests/nlsys_test.py b/control/tests/nlsys_test.py index 926ca4364..6d8016072 100644 --- a/control/tests/nlsys_test.py +++ b/control/tests/nlsys_test.py @@ -199,3 +199,51 @@ def test_ss2io(): with pytest.raises(ValueError, match=r"new .* doesn't match"): kwargs = {attr: getattr(sys, 'n' + attr) - 1} nlsys = ct.nlsys(sys, **kwargs) + + +def test_ICsystem_str(): + sys1 = ct.rss(2, 2, 3, name='sys1', strictly_proper=True) + sys2 = ct.rss(2, 3, 2, name='sys2', strictly_proper=True) + + with pytest.warns(UserWarning, match="Unused") as record: + sys = ct.interconnect( + [sys1, sys2], inputs=['r1', 'r2'], outputs=['y1', 'y2'], + connections=[ + ['sys1.u[0]', '-sys2.y[0]', 'sys2.y[1]'], + ['sys1.u[1]', 'sys2.y[0]', '-sys2.y[1]'], + ['sys2.u[0]', 'sys2.y[0]', (0, 0, -1)], + ['sys2.u[1]', (1, 1, -2), (0, 1, -2)], + ], + inplist=['sys1.u[0]', 'sys1.u[1]'], + outlist=['sys2.y[0]', 'sys2.y[1]']) + assert len(record) == 2 + assert str(record[0].message).startswith("Unused input") + assert str(record[1].message).startswith("Unused output") + + ref = \ + r": sys\[[\d]+\]" + "\n" + \ + r"Inputs \(2\): \['r1', 'r2'\]" + "\n" + \ + r"Outputs \(2\): \['y1', 'y2'\]" + "\n" + \ + r"States \(4\): \['sys1_x\[0\].*'sys2_x\[1\]'\]" + "\n" + \ + "\n" + \ + r"A = \[\[.*\]\]" + "\n\n" + \ + r"B = \[\[.*\]\]" + "\n\n" + \ + r"C = \[\[.*\]\]" + "\n\n" + \ + r"D = \[\[.*\]\]" + "\n" + \ + "\n" + \ + r"Subsystems \(2\):" + "\n" + \ + r" \* \['y\[0\]', 'y\[1\]']>" + "\n" + \ + r" \* \[.*\]>" + "\n" + \ + "\n" + \ + r"Connections:" + "\n" + \ + r" \* sys1.u\[0\] <- -sys2.y\[0\] \+ sys2.y\[1\] \+ r1" + "\n" + \ + r" \* sys1.u\[1\] <- sys2.y\[0\] - sys2.y\[1\] \+ r2" + "\n" + \ + r" \* sys1.u\[2\] <-" + "\n" + \ + r" \* sys2.u\[0\] <- -sys1.y\[0\] \+ sys2.y\[0\]" + "\n" + \ + r" \* sys2.u\[1\] <- -2.0 \* sys1.y\[1\] - 2.0 \* sys2.y\[1\]" + \ + "\n\n" + \ + r"Outputs:" + "\n" + \ + r" \* y1 <- sys2.y\[0\]" + "\n" + \ + r" \* y2 <- sys2.y\[1\]" + + assert re.match(ref, str(sys), re.DOTALL) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 45bc430ed..b4a0ebb56 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -779,7 +779,7 @@ def test_str(self, sys322): " [ 1. 4. 3.]]\n" "\n" "D = [[-2. 4.]\n" - " [ 0. 1.]]\n") + " [ 0. 1.]]") assert str(tsys) == tref tsysdtunspec = StateSpace( tsys.A, tsys.B, tsys.C, tsys.D, True, name=tsys.name) From 9ca6833cf4130b51934e4a2a6f2f0d1d7125630e Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 22 Dec 2024 10:18:36 -0800 Subject: [PATCH 11/21] add Parameters to NonlinearIOSystem.__str__() + avoid double empty lines --- control/frdata.py | 2 +- control/iosys.py | 10 +++++----- control/nlsys.py | 11 ++++++++--- control/statesp.py | 5 ----- control/tests/nlsys_test.py | 5 +++-- control/tests/xferfcn_test.py | 27 ++++++--------------------- 6 files changed, 23 insertions(+), 37 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 489197e75..147163970 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -399,7 +399,7 @@ def __str__(self): """String representation of the transfer function.""" mimo = self.ninputs > 1 or self.noutputs > 1 - outstr = [f"{InputOutputSystem.__str__(self)}"] + outstr = [f"{InputOutputSystem.__str__(self)}", ""] for i in range(self.ninputs): for j in range(self.noutputs): diff --git a/control/iosys.py b/control/iosys.py index 94ffe2016..71daa43f9 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -276,12 +276,12 @@ def repr_format(self, value): def __str__(self): """String representation of an input/output object""" - str = f"<{self.__class__.__name__}>: {self.name}\n" - str += f"Inputs ({self.ninputs}): {self.input_labels}\n" - str += f"Outputs ({self.noutputs}): {self.output_labels}\n" + out = f"<{self.__class__.__name__}>: {self.name}" + out += f"\nInputs ({self.ninputs}): {self.input_labels}" + out += f"\nOutputs ({self.noutputs}): {self.output_labels}" if self.nstates is not None: - str += f"States ({self.nstates}): {self.state_labels}" - return str + out += f"\nStates ({self.nstates}): {self.state_labels}" + return out def _label_repr(self, show_count=True): out, count = "", 0 diff --git a/control/nlsys.py b/control/nlsys.py index 03ef873f7..5a0913342 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -154,9 +154,13 @@ def __init__(self, updfcn, outfcn=None, params=None, **kwargs): self._current_params = {} if params is None else params.copy() def __str__(self): - return f"{InputOutputSystem.__str__(self)}\n\n" + \ + out = f"{InputOutputSystem.__str__(self)}" + if len(self.params) > 1: + out += f"\nParameters: {[p for p in self.params.keys()]}" + out += "\n\n" + \ f"Update: {self.updfcn}\n" + \ f"Output: {self.outfcn}" + return out # Return the value of a static nonlinear system def __call__(sys, u, params=None, squeeze=None): @@ -1368,7 +1372,7 @@ def nlsys(updfcn, outfcn=None, **kwargs): Examples -------- >>> def kincar_update(t, x, u, params): - ... l = params.get('l', 1) # wheelbase + ... l = params['l'] # wheelbase ... return np.array([ ... np.cos(x[2]) * u[0], # x velocity ... np.sin(x[2]) * u[0], # y velocity @@ -1379,7 +1383,8 @@ def nlsys(updfcn, outfcn=None, **kwargs): ... return x[0:2] # x, y position >>> >>> kincar = ct.nlsys( - ... kincar_update, kincar_output, states=3, inputs=2, outputs=2) + ... kincar_update, kincar_output, states=3, inputs=2, outputs=2, + ... params={'l': 1}) >>> >>> timepts = np.linspace(0, 10) >>> response = ct.input_output_response( diff --git a/control/statesp.py b/control/statesp.py index 7324af814..208965314 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1596,11 +1596,6 @@ def ss(*args, **kwargs): System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. - Returns - ------- - out: StateSpace - Linear input/output system. - Raises ------ ValueError diff --git a/control/tests/nlsys_test.py b/control/tests/nlsys_test.py index 6d8016072..0410e0a92 100644 --- a/control/tests/nlsys_test.py +++ b/control/tests/nlsys_test.py @@ -19,7 +19,7 @@ # Basic test of nlsys() def test_nlsys_basic(): def kincar_update(t, x, u, params): - l = params.get('l', 1) # wheelbase + l = params['l'] # wheelbase return np.array([ np.cos(x[2]) * u[0], # x velocity np.sin(x[2]) * u[0], # y velocity @@ -33,10 +33,11 @@ def kincar_output(t, x, u, params): kincar_update, kincar_output, states=['x', 'y', 'theta'], inputs=2, input_prefix='U', - outputs=2) + outputs=2, params={'l': 1}) assert kincar.input_labels == ['U[0]', 'U[1]'] assert kincar.output_labels == ['y[0]', 'y[1]'] assert kincar.state_labels == ['x', 'y', 'theta'] + assert kincar.params == {'l': 1} # Test nonlinear initial, step, and forced response diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index e5ead0c5e..4062e09ae 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -886,9 +886,9 @@ def test_printing(self): @pytest.mark.parametrize( "args, output", - [(([0], [1]), "\n0\n-\n1\n"), - (([1.0001], [-1.1111]), "\n 1\n------\n-1.111\n"), - (([0, 1], [0, 1.]), "\n1\n-\n1\n"), + [(([0], [1]), "0\n-\n1\n"), + (([1.0001], [-1.1111]), " 1\n------\n-1.111\n"), + (([0, 1], [0, 1.]), "1\n-\n1\n"), ]) def test_printing_polynomial_const(self, args, output): """Test _tf_polynomial_to_string for constant systems""" @@ -897,9 +897,9 @@ def test_printing_polynomial_const(self, args, output): @pytest.mark.parametrize( "args, outputfmt", [(([1, 0], [2, 1]), - "\n {var}\n-------\n2 {var} + 1\n{dtstring}"), + " {var}\n-------\n2 {var} + 1\n{dtstring}"), (([2, 0, -1], [1, 0, 0, 1.2]), - "\n2 {var}^2 - 1\n---------\n{var}^3 + 1.2\n{dtstring}")]) + "2 {var}^2 - 1\n---------\n{var}^3 + 1.2\n{dtstring}")]) @pytest.mark.parametrize("var, dt, dtstring", [("s", None, ''), ("z", True, ''), @@ -919,57 +919,46 @@ def test_printing_mimo(self): @pytest.mark.parametrize( "zeros, poles, gain, output", [([0], [-1], 1, - '\n' ' s\n' '-----\n' 's + 1\n'), ([-1], [-1], 1, - '\n' 's + 1\n' '-----\n' 's + 1\n'), ([-1], [1], 1, - '\n' 's + 1\n' '-----\n' 's - 1\n'), ([1], [-1], 1, - '\n' 's - 1\n' '-----\n' 's + 1\n'), ([-1], [-1], 2, - '\n' '2 (s + 1)\n' '---------\n' ' s + 1\n'), ([-1], [-1], 0, - '\n' '0\n' '-\n' '1\n'), ([-1], [1j, -1j], 1, - '\n' ' s + 1\n' '-----------------\n' '(s - 1j) (s + 1j)\n'), ([4j, -4j], [2j, -2j], 2, - '\n' '2 (s - 4j) (s + 4j)\n' '-------------------\n' ' (s - 2j) (s + 2j)\n'), ([1j, -1j], [-1, -4], 2, - '\n' '2 (s - 1j) (s + 1j)\n' '-------------------\n' ' (s + 1) (s + 4)\n'), ([1], [-1 + 1j, -1 - 1j], 1, - '\n' ' s - 1\n' '-------------------------\n' '(s + (1-1j)) (s + (1+1j))\n'), ([1], [1 + 1j, 1 - 1j], 1, - '\n' ' s - 1\n' '-------------------------\n' '(s - (1+1j)) (s - (1-1j))\n'), @@ -983,17 +972,14 @@ def test_printing_zpk(self, zeros, poles, gain, output): @pytest.mark.parametrize( "zeros, poles, gain, format, output", [([1], [1 + 1j, 1 - 1j], 1, ".2f", - '\n' ' 1.00\n' '-------------------------------------\n' '(s + (1.00-1.41j)) (s + (1.00+1.41j))\n'), ([1], [1 + 1j, 1 - 1j], 1, ".3f", - '\n' ' 1.000\n' '-----------------------------------------\n' '(s + (1.000-1.414j)) (s + (1.000+1.414j))\n'), ([1], [1 + 1j, 1 - 1j], 1, ".6g", - '\n' ' 1\n' '-------------------------------------\n' '(s + (1-1.41421j)) (s + (1+1.41421j))\n') @@ -1012,8 +998,7 @@ def test_printing_zpk_format(self, zeros, poles, gain, format, output): "num, den, output", [([[[11], [21]], [[12], [22]]], [[[1, -3, 2], [1, 1, -6]], [[1, 0, 1], [1, -1, -20]]], - ('\n' - 'Input 1 to output 1:\n' + ('Input 1 to output 1:\n' ' 11\n' '---------------\n' '(s - 2) (s - 1)\n' From ea7d3ed9c001160dcf8f1671e4ace313b024be9e Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 28 Dec 2024 13:57:26 -0800 Subject: [PATCH 12/21] Update split_tf and combine_tf docstrings + combine_tf kwargs --- control/bdalg.py | 80 ++++++++++++++++++++---------------- control/lti.py | 4 +- control/tests/kwargs_test.py | 2 + 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/control/bdalg.py b/control/bdalg.py index 2dbd5c8e9..79c1e712c 100644 --- a/control/bdalg.py +++ b/control/bdalg.py @@ -164,7 +164,7 @@ def parallel(sys1, *sysn, **kwargs): or `y`). See :class:`InputOutputSystem` for more information. states : str, or list of str, optional List of names for system states. If not given, state names will be - of of the form `x[i]` for interconnections of linear systems or + of the form `x[i]` for interconnections of linear systems or '.' for interconnected nonlinear systems. name : string, optional System name (used for specifying signals). If unspecified, a generic @@ -511,7 +511,7 @@ def connect(sys, Q, inputv, outputv): return Ytrim * sys * Utrim -def combine_tf(tf_array): +def combine_tf(tf_array, **kwargs): """Combine array-like of transfer functions into MIMO transfer function. Parameters @@ -527,6 +527,16 @@ def combine_tf(tf_array): TransferFunction Transfer matrix represented as a single MIMO TransferFunction object. + Other Parameters + ---------------- + inputs, outputs : str, or list of str, optional + List of strings that name the individual signals. If not given, + signal names will be of the form `s[i]` (where `s` is one of `u`, + or `y`). See :class:`InputOutputSystem` for more information. + name : string, optional + System name (used for specifying signals). If unspecified, a generic + name is generated with a unique integer id. + Raises ------ ValueError @@ -541,26 +551,22 @@ def combine_tf(tf_array): -------- Combine two transfer functions - >>> s = control.TransferFunction.s - >>> control.combine_tf([ - ... [1 / (s + 1)], - ... [s / (s + 2)], - ... ]) - TransferFunction([[array([1])], [array([1, 0])]], - [[array([1, 1])], [array([1, 2])]]) + >>> s = ct.tf('s') + >>> ct.combine_tf( + ... [[1 / (s + 1)], + ... [s / (s + 2)]], + ... name='G' + ... ) + ['y[0]', 'y[1]']> Combine NumPy arrays with transfer functions - >>> control.combine_tf([ - ... [np.eye(2), np.zeros((2, 1))], - ... [np.zeros((1, 2)), control.TransferFunction([1], [1, 0])], - ... ]) - TransferFunction([[array([1.]), array([0.]), array([0.])], - [array([0.]), array([1.]), array([0.])], - [array([0.]), array([0.]), array([1])]], - [[array([1.]), array([1.]), array([1.])], - [array([1.]), array([1.]), array([1.])], - [array([1.]), array([1.]), array([1, 0])]]) + >>> ct.combine_tf( + ... [[np.eye(2), np.zeros((2, 1))], + ... [np.zeros((1, 2)), ct.tf([1], [1, 0])]], + ... name='G' + ... ) + ['y[0]', 'y[1]', 'y[2]']> """ # Find common timebase or raise error dt_list = [] @@ -616,10 +622,14 @@ def combine_tf(tf_array): "Mismatched number transfer function inputs in row " f"{row_index} of denominator." ) - return tf.TransferFunction(num, den, dt=dt) + return tf.TransferFunction(num, den, dt=dt, **kwargs) + def split_tf(transfer_function): - """Split MIMO transfer function into NumPy array of SISO tranfer functions. + """Split MIMO transfer function into NumPy array of SISO transfer functions. + + System and signal names for the array of SISO transfer functions are + copied from the MIMO system. Parameters ---------- @@ -635,21 +645,18 @@ def split_tf(transfer_function): -------- Split a MIMO transfer function - >>> G = control.TransferFunction( - ... [ - ... [[87.8], [-86.4]], - ... [[108.2], [-109.6]], - ... ], - ... [ - ... [[1, 1], [1, 1]], - ... [[1, 1], [1, 1]], - ... ], + >>> G = ct.tf( + ... [ [[87.8], [-86.4]], + ... [[108.2], [-109.6]] ], + ... [ [[1, 1], [1, 1]], + ... [[1, 1], [1, 1]], ], + ... name='G' ... ) - >>> control.split_tf(G) - array([[TransferFunction(array([87.8]), array([1, 1])), - TransferFunction(array([-86.4]), array([1, 1]))], - [TransferFunction(array([108.2]), array([1, 1])), - TransferFunction(array([-109.6]), array([1, 1]))]], dtype=object) + >>> ct.split_tf(G) + array([[ ['y[0]']>, + ['y[0]']>], + [ ['y[1]']>, + ['y[1]']>]], dtype=object) """ tf_split_lst = [] for i_out in range(transfer_function.noutputs): @@ -660,6 +667,9 @@ def split_tf(transfer_function): transfer_function.num_array[i_out, i_in], transfer_function.den_array[i_out, i_in], dt=transfer_function.dt, + inputs=transfer_function.input_labels[i_in], + outputs=transfer_function.output_labels[i_out], + name=transfer_function.name ) ) tf_split_lst.append(row) diff --git a/control/lti.py b/control/lti.py index b7139f608..cb785ca5f 100644 --- a/control/lti.py +++ b/control/lti.py @@ -615,14 +615,14 @@ def bandwidth(sys, dbdrop=-3): ------- >>> G = ct.tf([1], [1, 1]) >>> ct.bandwidth(G) - 0.9976 + np.float64(0.9976283451102316) >>> G1 = ct.tf(0.1, [1, 0.1]) >>> wn2 = 1 >>> zeta2 = 0.001 >>> G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2]) >>> ct.bandwidth(G1*G2) - 0.1018 + np.float64(0.10184838823897456) """ if not isinstance(sys, LTI): diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 95450da08..d73df0bbd 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -96,6 +96,7 @@ def test_kwarg_search(module, prefix): @pytest.mark.parametrize( "function, nsssys, ntfsys, moreargs, kwargs", [(control.append, 2, 0, (), {}), + (control.combine_tf, 0, 0, ([[1, 0], [0, 1]], ), {}), (control.dlqe, 1, 0, ([[1]], [[1]]), {}), (control.dlqr, 1, 0, ([[1, 0], [0, 1]], [[1]]), {}), (control.drss, 0, 0, (2, 1, 1), {}), @@ -245,6 +246,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): 'bode': test_response_plot_kwargs, 'bode_plot': test_response_plot_kwargs, 'LTI.bode_plot': test_response_plot_kwargs, # alias for bode_plot and tested via bode_plot + 'combine_tf': test_unrecognized_kwargs, 'create_estimator_iosystem': stochsys_test.test_estimator_errors, 'create_statefbk_iosystem': statefbk_test.TestStatefbk.test_statefbk_errors, 'describing_function_plot': test_matplotlib_kwargs, From 143454067f43b62e765203492d936fd1ef11f172 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 30 Dec 2024 23:03:27 -0800 Subject: [PATCH 13/21] add repr_gallery + update formatting for style and consistency --- control/frdata.py | 18 ++--- control/iosys.py | 71 ++++++++++++++----- control/statesp.py | 100 ++++++++++---------------- control/xferfcn.py | 50 ++++++------- examples/repr_gallery.py | 149 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 115 deletions(-) create mode 100644 examples/repr_gallery.py diff --git a/control/frdata.py b/control/frdata.py index 147163970..cb6925661 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -399,16 +399,19 @@ def __str__(self): """String representation of the transfer function.""" mimo = self.ninputs > 1 or self.noutputs > 1 - outstr = [f"{InputOutputSystem.__str__(self)}", ""] + outstr = [f"{InputOutputSystem.__str__(self)}"] + nl = "\n " if mimo else "\n" + sp = " " if mimo else "" for i in range(self.ninputs): for j in range(self.noutputs): if mimo: - outstr.append("Input %i to output %i:" % (i + 1, j + 1)) - outstr.append('Freq [rad/s] Response') - outstr.append('------------ ---------------------') + outstr.append( + "\nInput %i to output %i:" % (i + 1, j + 1)) + outstr.append(nl + 'Freq [rad/s] Response') + outstr.append(sp + '------------ ---------------------') outstr.extend( - ['%12.3f %10.4g%+10.4gj' % (w, re, im) + [sp + '%12.3f %10.4g%+10.4gj' % (w, re, im) for w, re, im in zip(self.omega, real(self.fresp[j, i, :]), imag(self.fresp[j, i, :]))]) @@ -421,10 +424,7 @@ def _repr_eval_(self): d=repr(self.fresp), w=repr(self.omega), smooth=(self._ifunc and ", smooth=True") or "") - if config.defaults['control.default_dt'] != self.dt: - out += ",\ndt={dt}".format( - dt='None' if self.dt is None else self.dt) - + out += self._dt_repr() if len(labels := self._label_repr()) > 0: out += ",\n" + labels diff --git a/control/iosys.py b/control/iosys.py index 71daa43f9..e2a7619cd 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -242,12 +242,51 @@ def _generic_name_check(self): #: :meta hide-value: nstates = None + # + # System representation + # + + def __str__(self): + """String representation of an input/output object""" + out = f"<{self.__class__.__name__}>: {self.name}" + out += f"\nInputs ({self.ninputs}): {self.input_labels}" + out += f"\nOutputs ({self.noutputs}): {self.output_labels}" + if self.nstates is not None: + out += f"\nStates ({self.nstates}): {self.state_labels}" + out += self._dt_repr("\n") + return out + def __repr__(self): return iosys_repr(self, format=self.repr_format) - def _repr_info_(self): - return f'<{self.__class__.__name__} {self.name}: ' + \ - f'{list(self.input_labels)} -> {list(self.output_labels)}>' + def _repr_info_(self, html=False): + out = f"<{self.__class__.__name__} {self.name}: " + \ + f"{list(self.input_labels)} -> {list(self.output_labels)}" + out += self._dt_repr(", ") + ">" + + if html: + # Replace symbols that might be interpreted by HTML processing + escape_chars = { + '$': r'\$', + '<': '<', + '>': '>', + } + return "".join([c if c not in escape_chars else + escape_chars[c] for c in out]) + else: + return out + + def _repr_eval_(self): + # Defaults to _repr_info_; override in subclasses + return self._repr_info_() + + def _repr_latex_(self): + # Defaults to using __repr__; override in subclasses + return None + + def _repr_html_(self): + # Defaults to using __repr__; override in subclasses + return None @property def repr_format(self): @@ -257,7 +296,7 @@ def repr_format(self): * 'info' : [outputs] * 'eval' : system specific, loadable representation - * 'latex' : latex representation of the object + * 'latex' : HTML/LaTeX representation of the object The default representation for an input/output is set to 'info'. This value can be changed for an individual system by setting the @@ -274,15 +313,6 @@ def repr_format(self): def repr_format(self, value): self._repr_format = value - def __str__(self): - """String representation of an input/output object""" - out = f"<{self.__class__.__name__}>: {self.name}" - out += f"\nInputs ({self.ninputs}): {self.input_labels}" - out += f"\nOutputs ({self.noutputs}): {self.output_labels}" - if self.nstates is not None: - out += f"\nStates ({self.nstates}): {self.state_labels}" - return out - def _label_repr(self, show_count=True): out, count = "", 0 @@ -306,6 +336,8 @@ def _label_repr(self, show_count=True): spec = f"{sig_name}={sig_labels}" elif show_count: spec = f"{sig_name}={len(sig_labels)}" + else: + spec = "" # Append the specification string to the output, with wrapping if count == 0: @@ -314,13 +346,20 @@ def _label_repr(self, show_count=True): # TODO: check to make sure a single line is enough (minor) out += ",\n" count = len(spec) - else: + elif len(spec) > 0: out += ", " count += len(spec) + 2 out += spec return out + def _dt_repr(self, separator="\n"): + if config.defaults['control.default_dt'] != self.dt: + return "{separator}dt={dt}".format( + separator=separator, dt='None' if self.dt is None else self.dt) + else: + return "" + # Find a list of signals by name, index, or pattern def _find_signals(self, name_list, sigdict): if not isinstance(name_list, (list, tuple)): @@ -768,7 +807,7 @@ def iosys_repr(sys, format=None): * 'info' : [outputs] * 'eval' : system specific, loadable representation - * 'latex' : latex representation of the object + * 'latex' : HTML/LaTeX representation of the object Returns ------- @@ -792,7 +831,7 @@ def iosys_repr(sys, format=None): case 'eval': return sys._repr_eval_() case 'latex': - return sys._repr_latex_() + return sys._repr_html_() case _: raise ValueError(f"format '{format}' unknown") diff --git a/control/statesp.py b/control/statesp.py index 208965314..97f2eadcd 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -132,12 +132,12 @@ class StateSpace(NonlinearIOSystem, LTI): signal offsets. The subsystem is created by truncating the inputs and outputs, but leaving the full set of system states. - StateSpace instances have support for IPython LaTeX output, intended - for pretty-printing in Jupyter notebooks. The LaTeX output can be + StateSpace instances have support for IPython HTML/LaTeX output, intended + for pretty-printing in Jupyter notebooks. The HTML/LaTeX output can be configured using `control.config.defaults['statesp.latex_num_format']` - and `control.config.defaults['statesp.latex_repr_type']`. The LaTeX - output is tailored for MathJax, as used in Jupyter, and may look odd - when typeset by non-MathJax LaTeX systems. + and `control.config.defaults['statesp.latex_repr_type']`. The + HTML/LaTeX output is tailored for MathJax, as used in Jupyter, and + may look odd when typeset by non-MathJax LaTeX systems. `control.config.defaults['statesp.latex_num_format']` is a format string fragment, specifically the part of the format string after `'{:'` @@ -386,8 +386,6 @@ def __str__(self): "\n ".join(str(M).splitlines())) for Mvar, M in zip(["A", "B", "C", "D"], [self.A, self.B, self.C, self.D])]) - if self.isdtime(strict=True): - string += f"\ndt = {self.dt}\n" return string def _repr_eval_(self): @@ -396,16 +394,42 @@ def _repr_eval_(self): A=self.A.__repr__(), B=self.B.__repr__(), C=self.C.__repr__(), D=self.D.__repr__()) - if config.defaults['control.default_dt'] != self.dt: - out += ",\ndt={dt}".format( - dt='None' if self.dt is None else self.dt) - - if len(labels := self._label_repr()) > 0: + out += super()._dt_repr(",\n") + if len(labels := super()._label_repr(show_count=False)) > 0: out += ",\n" + labels out += ")" return out + def _repr_html_(self): + """HTML representation of state-space model. + + Output is controlled by config options statesp.latex_repr_type, + statesp.latex_num_format, and statesp.latex_maxsize. + + The output is primarily intended for Jupyter notebooks, which + use MathJax to render the LaTeX, and the results may look odd + when processed by a 'conventional' LaTeX system. + + Returns + ------- + s : string + HTML/LaTeX representation of model, or None if either matrix + dimension is greater than statesp.latex_maxsize. + + """ + syssize = self.nstates + max(self.noutputs, self.ninputs) + if syssize > config.defaults['statesp.latex_maxsize']: + return None + elif config.defaults['statesp.latex_repr_type'] == 'partitioned': + return super()._repr_info(html=True) + self._latex_partitioned() + elif config.defaults['statesp.latex_repr_type'] == 'separate': + return super()._repr_info(html=True) + self._latex_separate() + else: + raise ValueError( + "Unknown statesp.latex_repr_type '{cfg}'".format( + cfg=config.defaults['statesp.latex_repr_type'])) + def _latex_partitioned_stateless(self): """`Partitioned` matrix LaTeX representation for stateless systems @@ -420,7 +444,6 @@ def _latex_partitioned_stateless(self): D = eval(repr(self.D)) lines = [ - self._repr_info_(), r'$$', (r'\left(' + r'\begin{array}' @@ -433,8 +456,7 @@ def _latex_partitioned_stateless(self): lines.extend([ r'\end{array}' - r'\right)' - + self._latex_dt(), + r'\right)', r'$$']) return '\n'.join(lines) @@ -458,7 +480,6 @@ def _latex_partitioned(self): eval(repr(getattr(self, M))) for M in ['A', 'B', 'C', 'D']) lines = [ - self._repr_info_(), r'$$', (r'\left(' + r'\begin{array}' @@ -477,8 +498,7 @@ def _latex_partitioned(self): lines.extend([ r'\end{array}' - + r'\right)' - + self._latex_dt(), + + r'\right)', r'$$']) return '\n'.join(lines) @@ -493,7 +513,6 @@ def _latex_separate(self): s : string with LaTeX representation of model """ lines = [ - self._repr_info_(), r'$$', r'\begin{array}{ll}', ] @@ -522,52 +541,11 @@ def fmt_matrix(matrix, name): lines.extend(fmt_matrix(self.D, 'D')) lines.extend([ - r'\end{array}' - + self._latex_dt(), + r'\end{array}', r'$$']) return '\n'.join(lines) - def _latex_dt(self): - if self.isdtime(strict=True): - if self.dt is True: - return r"~,~dt=~\mathrm{True}" - else: - fmt = config.defaults['statesp.latex_num_format'] - return f"~,~dt={self.dt:{fmt}}" - return "" - - def _repr_latex_(self): - """LaTeX representation of state-space model - - Output is controlled by config options statesp.latex_repr_type, - statesp.latex_num_format, and statesp.latex_maxsize. - - The output is primarily intended for Jupyter notebooks, which - use MathJax to render the LaTeX, and the results may look odd - when processed by a 'conventional' LaTeX system. - - - Returns - ------- - - s : string with LaTeX representation of model, or None if - either matrix dimension is greater than - statesp.latex_maxsize - - """ - syssize = self.nstates + max(self.noutputs, self.ninputs) - if syssize > config.defaults['statesp.latex_maxsize']: - return None - elif config.defaults['statesp.latex_repr_type'] == 'partitioned': - return self._latex_partitioned() - elif config.defaults['statesp.latex_repr_type'] == 'separate': - return self._latex_separate() - else: - raise ValueError( - "Unknown statesp.latex_repr_type '{cfg}'".format( - cfg=config.defaults['statesp.latex_repr_type'])) - # Negation of a system def __neg__(self): """Negate a state space system.""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 1c794c52c..ef30d921f 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -210,11 +210,8 @@ def __init__(self, *args, **kwargs): # get initialized when defaults are not fully initialized yet. # Use 'poly' in these cases. - self.display_format = kwargs.pop( - 'display_format', - config.defaults.get('xferfcn.display_format', 'poly')) - - if self.display_format not in ('poly', 'zpk'): + self.display_format = kwargs.pop('display_format', None) + if self.display_format not in (None, 'poly', 'zpk'): raise ValueError("display_format must be 'poly' or 'zpk'," " got '%s'" % self.display_format) @@ -442,30 +439,34 @@ def __str__(self, var=None): Based on the display_format property, the output will be formatted as either polynomials or in zpk form. """ + display_format = config.defaults['xferfcn.display_format'] if \ + self.display_format is None else self.display_format mimo = not self.issiso() if var is None: var = 's' if self.isctime() else 'z' - outstr = f"{InputOutputSystem.__str__(self)}\n" + outstr = f"{InputOutputSystem.__str__(self)}" for ni in range(self.ninputs): for no in range(self.noutputs): + outstr += "\n" if mimo: - outstr += "\nInput %i to output %i:" % (ni + 1, no + 1) + outstr += "\nInput %i to output %i:\n" % (ni + 1, no + 1) # Convert the numerator and denominator polynomials to strings. - if self.display_format == 'poly': + if display_format == 'poly': numstr = _tf_polynomial_to_string( self.num_array[no, ni], var=var) denstr = _tf_polynomial_to_string( self.den_array[no, ni], var=var) - elif self.display_format == 'zpk': + elif display_format == 'zpk': num = self.num_array[no, ni] if num.size == 1 and num.item() == 0: # Catch a special case that SciPy doesn't handle z, p, k = tf2zpk([1.], self.den_array[no, ni]) k = 0 else: - z, p, k = tf2zpk(self.num[no][ni], self.den_array[no, ni]) + z, p, k = tf2zpk( + self.num[no][ni], self.den_array[no, ni]) numstr = _tf_factorized_polynomial_to_string( z, gain=k, var=var) denstr = _tf_factorized_polynomial_to_string(p, var=var) @@ -480,11 +481,7 @@ def __str__(self, var=None): if len(denstr) < dashcount: denstr = ' ' * ((dashcount - len(denstr)) // 2) + denstr - outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n" - - # If this is a strict discrete time system, print the sampling time - if type(self.dt) != bool and self.isdtime(strict=True): - outstr += "\ndt = " + str(self.dt) + "\n" + outstr += "\n " + numstr + "\n " + dashes + "\n " + denstr return outstr @@ -511,22 +508,21 @@ def _repr_eval_(self): out += "]," if i < self.noutputs - 1 else "]" out += "],\n[" if entry is self.num_array else "]" - if config.defaults['control.default_dt'] != self.dt: - out += ",\ndt={dt}".format( - dt='None' if self.dt is None else self.dt) - - if len(labels := self._label_repr()) > 0: + out += super()._dt_repr(separator=",\n") + if len(labels := self._label_repr(show_count=False)) > 0: out += ",\n" + labels out += ")" return out - def _repr_latex_(self, var=None): - """LaTeX representation of transfer function, for Jupyter notebook.""" + def _repr_html_(self, var=None): + """HTML/LaTeX representation of xferfcn, for Jupyter notebook.""" + display_format = config.defaults['xferfcn.display_format'] if \ + self.display_format is None else self.display_format mimo = not self.issiso() if var is None: var = 's' if self.isctime() else 'z' - out = [self._repr_info_(), '$$'] + out = ['$$'] if mimo: out.append(r"\begin{bmatrix}") @@ -534,12 +530,12 @@ def _repr_latex_(self, var=None): for no in range(self.noutputs): for ni in range(self.ninputs): # Convert the numerator and denominator polynomials to strings. - if self.display_format == 'poly': + if display_format == 'poly': numstr = _tf_polynomial_to_string( self.num_array[no, ni], var=var) denstr = _tf_polynomial_to_string( self.den_array[no, ni], var=var) - elif self.display_format == 'zpk': + elif display_format == 'zpk': z, p, k = tf2zpk( self.num_array[no, ni], self.den_array[no, ni]) numstr = _tf_factorized_polynomial_to_string( @@ -560,10 +556,6 @@ def _repr_latex_(self, var=None): if mimo: out.append(r" \end{bmatrix}") - # See if this is a discrete time system with specific sampling time - if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: - out += [r"~,~dt = ", str(self.dt)] - out.append("$$") return ''.join(out) diff --git a/examples/repr_gallery.py b/examples/repr_gallery.py new file mode 100644 index 000000000..fc301c447 --- /dev/null +++ b/examples/repr_gallery.py @@ -0,0 +1,149 @@ +# repr-galler.py - different system representations for comparing versions +# RMM, 30 Dec 2024 +# +# This file creates different types of systems and generates a variety +# of representations (__repr__, __str__) for those systems that can be +# used to compare different versions of python-control. It is mainly +# intended for uses by developers to make sure there are no unexpected +# changes in representation formats, but also has some interest +# examples of different choices in system representation. + +import numpy as np + +import control as ct +import control.flatsys as fs + +# +# Create systems of different types +# +syslist = [] + +# State space (continuous and discrete time) +sys_ss = ct.ss([[0, 1], [-4, -5]], [0, 1], [-1, 1], 0, name='sys_ss') +sys_dss = sys_ss.sample(0.1, name='sys_dss') +sys_ss0 = ct.ss([], [], [], np.eye(2), name='stateless') +syslist += [sys_ss, sys_dss, sys_ss0] + +# Transfer function (continuous and discrete time) +sys_tf = ct.tf(sys_ss) +sys_dtf = ct.tf(sys_dss, name='sys_dss_poly', display_format='poly') +sys_gtf = ct.tf([1], [1, 0]) +syslist += [sys_tf, sys_dtf, sys_gtf] + +# MIMO transfer function (continous time only) +sys_mtf = ct.tf( + [[sys_tf.num[0][0].tolist(), [0]], [[1, 0], [1, 0] ]], + [[sys_tf.den[0][0].tolist(), [1]], [[1], [1, 2, 1]]], + name='sys_mtf_zpk', display_format='zpk') +syslist += [sys_mtf] + +# Frequency response data (FRD) system (continous and discrete time) +sys_frd = ct.frd(sys_tf, np.logspace(-1, 1, 5)) +sys_dfrd = ct.frd(sys_dtf, np.logspace(-1, 1, 5)) +sys_mfrd = ct.frd(sys_mtf, np.logspace(-1, 1, 5)) +syslist += [sys_frd, sys_dfrd, sys_mfrd] + +# Nonlinear system (with linear dynamics), continuous time +def nl_update(t, x, u, params): + return sys_ss.A @ x + sys_ss.B @ u + +def nl_output(t, x, u, params): + return sys_ss.C @ x + sys_ss.D @ u + +sys_nl = ct.nlsys( + nl_update, nl_output, name='sys_nl', + states=sys_ss.nstates, inputs=sys_ss.ninputs, outputs=sys_ss.noutputs) + +# Nonlinear system (with linear dynamics), discrete time +def dnl_update(t, x, u, params): + return sys_ss.A @ x + sys_ss.B @ u + +def dnl_output(t, x, u, params): + return sys_ss.C @ x + sys_ss.D @ u + +sys_dnl = ct.nlsys( + dnl_update, dnl_output, dt=0.1, name='sys_dnl', + states=sys_ss.nstates, inputs=sys_ss.ninputs, outputs=sys_ss.noutputs) + +syslist += [sys_nl, sys_dnl] + +# Interconnected system +proc = ct.ss([[0, 1], [-4, -5]], np.eye(2), [[-1, 1], [1, 0]], 0, name='proc') +ctrl = ct.ss([], [], [], [[-2, 0], [0, -3]], name='ctrl') + +proc_nl = ct.nlsys(proc, name='proc_nl') +ctrl_nl = ct.nlsys(ctrl, name='ctrl_nl') +sys_ic = ct.interconnect( + [proc_nl, ctrl_nl], name='sys_ic', + connections=[['proc_nl.u', 'ctrl_nl.y'], ['ctrl_nl.u', '-proc_nl.y']], + inplist=['ctrl_nl.u'], inputs=['r[0]', 'r[1]'], + outlist=['proc_nl.y'], outputs=proc_nl.output_labels) +syslist += [sys_ic] + +# Linear interconnected system +sys_lic = ct.interconnect( + [proc, ctrl], name='sys_ic', + connections=[['proc.u', 'ctrl.y'], ['ctrl.u', '-proc.y']], + inplist=['ctrl.u'], inputs=['r[0]', 'r[1]'], + outlist=['proc.y'], outputs=proc.output_labels) +syslist += [sys_lic] + +# Differentially flat system (with implicit dynamics), continuous time (only) +def fs_forward(x, u): + return np.array([x[0], x[1], -4 * x[0] - 5 * x[1] + u[0]]) + +def fs_reverse(zflag): + return ( + np.array([zflag[0][0], zflag[0][1]]), + np.array([4 * zflag[0][0] + 5 * zflag[0][1] + zflag[0][2]])) + +sys_fs = fs.flatsys( + fs_forward, fs_reverse, name='sys_fs', + states=sys_nl.nstates, inputs=sys_nl.ninputs, outputs=sys_nl.noutputs) + +# Differentially flat system (with nonlinear dynamics), continuous time (only) +sys_fsnl = fs.flatsys( + fs_forward, fs_reverse, nl_update, nl_output, name='sys_fsnl', + states=sys_nl.nstates, inputs=sys_nl.ninputs, outputs=sys_nl.noutputs) + +syslist += [sys_fs, sys_fsnl] + +# Utility function to display outputs +def display_representations( + description, fcn, class_list=(ct.InputOutputSystem, )): + print("=" * 78) + print(" " * round((78 - len(description)) / 2) + f"{description}") + print("=" * 78 + "\n") + for sys in syslist: + if isinstance(sys, tuple(class_list)): + print(str := f"{type(sys).__name__}: {sys.name}, dt={sys.dt}:") + print("-" * len(str)) + print(fcn(sys)) + print("----\n") + + +# Default formats +display_representations("Default repr", repr) +display_representations("Default str (print)", str) + +# 'info' format (if it exists and hasn't already been displayed) +if getattr(ct.InputOutputSystem, '_repr_info_', None) and \ + ct.config.defaults.get('iosys.repr_format', None) and \ + ct.config.defaults['iosys.repr_format'] != 'info': + ct.set_defaults('iosys', repr_format='info') + display_representations("repr_format='info'", repr) +ct.reset_defaults() + +# 'eval' format (if it exists and hasn't already been displayed) +if getattr(ct.InputOutputSystem, '_repr_eval_', None) and \ + ct.config.defaults.get('iosys.repr_format', None) and \ + ct.config.defaults['iosys.repr_format'] != 'eval': + ct.set_defaults('iosys', repr_format='eval') + display_representations("repr_format='eval'", repr) +ct.reset_defaults() + +ct.set_defaults('xferfcn', display_format='zpk') +display_representations( + "xferfcn.display_format=zpk, str (print)", str, + class_list=[ct.TransferFunction]) +ct.reset_defaults() From b08321268accf6daf50cee22b23934e2bb1b8b51 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 31 Dec 2024 08:44:16 -0800 Subject: [PATCH 14/21] add context manager functionality to config.defaults --- control/config.py | 22 ++++++++++++++++++++++ control/tests/config_test.py | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/control/config.py b/control/config.py index d7333c682..0ffa949a4 100644 --- a/control/config.py +++ b/control/config.py @@ -73,6 +73,28 @@ def _check_deprecation(self, key): else: return key + # + # Context manager functionality + # + + def __call__(self, mapping): + self.saved_mapping = dict() + self.temp_mapping = mapping.copy() + return self + + def __enter__(self): + for key, val in self.temp_mapping.items(): + if not key in self: + raise ValueError(f"unknown parameter '{key}'") + self.saved_mapping[key] = self[key] + self[key] = val + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for key, val in self.saved_mapping.items(): + self[key] = val + del self.saved_mapping, self.temp_mapping + return None defaults = DefaultDict(_control_defaults) diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 988699a09..bfcdf2318 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -332,3 +332,20 @@ def test_legacy_repr_format(self): new = eval(repr(sys)) for attr in ['A', 'B', 'C', 'D']: assert getattr(sys, attr) == getattr(sys, attr) + + +def test_config_context_manager(): + # Make sure we can temporarily set the value of a parameter + default_val = ct.config.defaults['statesp.latex_repr_type'] + with ct.config.defaults({'statesp.latex_repr_type': 'new value'}): + assert ct.config.defaults['statesp.latex_repr_type'] != default_val + assert ct.config.defaults['statesp.latex_repr_type'] == 'new value' + assert ct.config.defaults['statesp.latex_repr_type'] == default_val + + # OK to call the context manager and not do anything with it + ct.config.defaults({'statesp.latex_repr_type': 'new value'}) + assert ct.config.defaults['statesp.latex_repr_type'] == default_val + + with pytest.raises(ValueError, match="unknown parameter 'unknown'"): + with ct.config.defaults({'unknown': 'new value'}): + pass From 8402605699eeb0c6ba4a8d805de491e7053f03e5 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 31 Dec 2024 09:56:31 -0800 Subject: [PATCH 15/21] update latex processing + repr_gallery.ipynb --- control/statesp.py | 16 +- control/xferfcn.py | 4 +- examples/repr_gallery.ipynb | 1220 +++++++++++++++++++++++++++++++++++ 3 files changed, 1230 insertions(+), 10 deletions(-) create mode 100644 examples/repr_gallery.ipynb diff --git a/control/statesp.py b/control/statesp.py index 97f2eadcd..282969c38 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -422,9 +422,9 @@ def _repr_html_(self): if syssize > config.defaults['statesp.latex_maxsize']: return None elif config.defaults['statesp.latex_repr_type'] == 'partitioned': - return super()._repr_info(html=True) + self._latex_partitioned() + return super()._repr_info_(html=True) + self._latex_partitioned() elif config.defaults['statesp.latex_repr_type'] == 'separate': - return super()._repr_info(html=True) + self._latex_separate() + return super()._repr_info_(html=True) + self._latex_separate() else: raise ValueError( "Unknown statesp.latex_repr_type '{cfg}'".format( @@ -445,7 +445,7 @@ def _latex_partitioned_stateless(self): lines = [ r'$$', - (r'\left(' + (r'\left[' + r'\begin{array}' + r'{' + 'rll' * self.ninputs + '}') ] @@ -456,7 +456,7 @@ def _latex_partitioned_stateless(self): lines.extend([ r'\end{array}' - r'\right)', + r'\right]', r'$$']) return '\n'.join(lines) @@ -481,7 +481,7 @@ def _latex_partitioned(self): lines = [ r'$$', - (r'\left(' + (r'\left[' + r'\begin{array}' + r'{' + 'rll' * self.nstates + '|' + 'rll' * self.ninputs + '}') ] @@ -498,7 +498,7 @@ def _latex_partitioned(self): lines.extend([ r'\end{array}' - + r'\right)', + + r'\right]', r'$$']) return '\n'.join(lines) @@ -519,7 +519,7 @@ def _latex_separate(self): def fmt_matrix(matrix, name): matlines = [name - + r' = \left(\begin{array}{' + + r' = \left[\begin{array}{' + 'rll' * matrix.shape[1] + '}'] for row in asarray(matrix): @@ -527,7 +527,7 @@ def fmt_matrix(matrix, name): + '\\\\') matlines.extend([ r'\end{array}' - r'\right)']) + r'\right]']) return matlines if self.nstates > 0: diff --git a/control/xferfcn.py b/control/xferfcn.py index ef30d921f..9d447cfc0 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -522,7 +522,7 @@ def _repr_html_(self, var=None): mimo = not self.issiso() if var is None: var = 's' if self.isctime() else 'z' - out = ['$$'] + out = [super()._repr_info_(html=True), '$$'] if mimo: out.append(r"\begin{bmatrix}") @@ -545,7 +545,7 @@ def _repr_html_(self, var=None): numstr = _tf_string_to_latex(numstr, var=var) denstr = _tf_string_to_latex(denstr, var=var) - out += [r"\frac{", numstr, "}{", denstr, "}"] + out += [r"\dfrac{", numstr, "}{", denstr, "}"] if mimo and ni < self.ninputs - 1: out.append("&") diff --git a/examples/repr_gallery.ipynb b/examples/repr_gallery.ipynb new file mode 100644 index 000000000..e12c16574 --- /dev/null +++ b/examples/repr_gallery.ipynb @@ -0,0 +1,1220 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "639f45ae-0ee8-426e-9d52-a7b9bb95d45a", + "metadata": {}, + "source": [ + "# System Representation Gallery\n", + "\n", + "This Jupyter notebook creates different types of systems and generates a variety of representations (`__repr__`, `__str__`) for those systems that can be used to compare different versions of python-control. It is mainly intended for uses by developers to make sure there are no unexpected changes in representation formats, but also has some interest examples of different choices in system representation." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c4b80abe-59e4-4d76-a81c-6979a583e82d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.10.1.dev324+g2fd3802a.d20241218'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "\n", + "import control as ct\n", + "import control.flatsys as fs\n", + "\n", + "ct.__version__" + ] + }, + { + "cell_type": "markdown", + "id": "035ebae9-7a4b-4079-8111-31f6c493c77c", + "metadata": {}, + "source": [ + "## Text representations\n", + "\n", + "The code below shows what the output in various formats will look like in a terminal window." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eab8cc0b-3e8a-4df8-acbd-258f006f44bb", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==============================================================================\n", + " Default repr\n", + "==============================================================================\n", + "\n", + "StateSpace: sys_ss, dt=0:\n", + "-------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "StateSpace: sys_dss, dt=0.1:\n", + "----------------------------\n", + " ['y[0]'], dt=0.1>\n", + "----\n", + "\n", + "StateSpace: stateless, dt=None:\n", + "-------------------------------\n", + " ['y[0]', 'y[1]'], dt=None>\n", + "----\n", + "\n", + "TransferFunction: sys_ss$converted, dt=0:\n", + "-----------------------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "TransferFunction: sys_dss_poly, dt=0.1:\n", + "---------------------------------------\n", + " ['y[0]'], dt=0.1>\n", + "----\n", + "\n", + "TransferFunction: sys[3], dt=0:\n", + "-------------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "TransferFunction: sys_mtf_zpk, dt=0:\n", + "------------------------------------\n", + " ['y[0]', 'y[1]']>\n", + "----\n", + "\n", + "FrequencyResponseData: sys_ss$converted$sampled, dt=0:\n", + "------------------------------------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "FrequencyResponseData: sys_dss_poly$sampled, dt=0.1:\n", + "----------------------------------------------------\n", + " ['y[0]'], dt=0.1>\n", + "----\n", + "\n", + "FrequencyResponseData: sys_mtf_zpk$sampled, dt=0:\n", + "-------------------------------------------------\n", + " ['y[0]', 'y[1]']>\n", + "----\n", + "\n", + "NonlinearIOSystem: sys_nl, dt=0:\n", + "--------------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "NonlinearIOSystem: sys_dnl, dt=0.1:\n", + "-----------------------------------\n", + " ['y[0]'], dt=0.1>\n", + "----\n", + "\n", + "InterconnectedSystem: sys_ic, dt=0:\n", + "-----------------------------------\n", + " ['y[0]', 'y[1]']>\n", + "----\n", + "\n", + "LinearICSystem: sys_ic, dt=0:\n", + "-----------------------------\n", + " ['y[0]', 'y[1]']>\n", + "----\n", + "\n", + "FlatSystem: sys_fs, dt=0:\n", + "-------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "FlatSystem: sys_fsnl, dt=0:\n", + "---------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "==============================================================================\n", + " Default str (print)\n", + "==============================================================================\n", + "\n", + "StateSpace: sys_ss, dt=0:\n", + "-------------------------\n", + ": sys_ss\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "States (2): ['x[0]', 'x[1]']\n", + "\n", + "A = [[ 0. 1.]\n", + " [-4. -5.]]\n", + "\n", + "B = [[0.]\n", + " [1.]]\n", + "\n", + "C = [[-1. 1.]]\n", + "\n", + "D = [[0.]]\n", + "----\n", + "\n", + "StateSpace: sys_dss, dt=0.1:\n", + "----------------------------\n", + ": sys_dss\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "States (2): ['x[0]', 'x[1]']\n", + "dt=0.1\n", + "\n", + "A = [[ 0.98300988 0.07817246]\n", + " [-0.31268983 0.59214759]]\n", + "\n", + "B = [[0.00424753]\n", + " [0.07817246]]\n", + "\n", + "C = [[-1. 1.]]\n", + "\n", + "D = [[0.]]\n", + "----\n", + "\n", + "StateSpace: stateless, dt=None:\n", + "-------------------------------\n", + ": stateless\n", + "Inputs (2): ['u[0]', 'u[1]']\n", + "Outputs (2): ['y[0]', 'y[1]']\n", + "States (0): []\n", + "dt=None\n", + "\n", + "A = []\n", + "\n", + "B = []\n", + "\n", + "C = []\n", + "\n", + "D = [[1. 0.]\n", + " [0. 1.]]\n", + "----\n", + "\n", + "TransferFunction: sys_ss$converted, dt=0:\n", + "-----------------------------------------\n", + ": sys_ss$converted\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "\n", + " s - 1\n", + " -------------\n", + " s^2 + 5 s + 4\n", + "----\n", + "\n", + "TransferFunction: sys_dss_poly, dt=0.1:\n", + "---------------------------------------\n", + ": sys_dss_poly\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "dt=0.1\n", + "\n", + " 0.07392 z - 0.08177\n", + " ----------------------\n", + " z^2 - 1.575 z + 0.6065\n", + "----\n", + "\n", + "TransferFunction: sys[3], dt=0:\n", + "-------------------------------\n", + ": sys[3]\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "\n", + " 1\n", + " -\n", + " s\n", + "----\n", + "\n", + "TransferFunction: sys_mtf_zpk, dt=0:\n", + "------------------------------------\n", + ": sys_mtf_zpk\n", + "Inputs (2): ['u[0]', 'u[1]']\n", + "Outputs (2): ['y[0]', 'y[1]']\n", + "\n", + "Input 1 to output 1:\n", + "\n", + " s - 1\n", + " ---------------\n", + " (s + 1) (s + 4)\n", + "\n", + "Input 1 to output 2:\n", + "\n", + " s\n", + " -\n", + " 1\n", + "\n", + "Input 2 to output 1:\n", + "\n", + " 0\n", + " -\n", + " 1\n", + "\n", + "Input 2 to output 2:\n", + "\n", + " s\n", + " ---------------\n", + " (s + 1) (s + 1)\n", + "----\n", + "\n", + "FrequencyResponseData: sys_ss$converted$sampled, dt=0:\n", + "------------------------------------------------------\n", + ": sys_ss$converted$sampled\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "\n", + "Freq [rad/s] Response\n", + "------------ ---------------------\n", + " 0.100 -0.2437 +0.0556j\n", + " 0.316 -0.192 +0.1589j\n", + " 1.000 0.05882 +0.2353j\n", + " 3.162 0.1958 -0.01106j\n", + " 10.000 0.05087 -0.07767j\n", + "----\n", + "\n", + "FrequencyResponseData: sys_dss_poly$sampled, dt=0.1:\n", + "----------------------------------------------------\n", + ": sys_dss_poly$sampled\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "dt=0.1\n", + "\n", + "Freq [rad/s] Response\n", + "------------ ---------------------\n", + " 0.100 -0.2434 +0.05673j\n", + " 0.316 -0.1894 +0.1617j\n", + " 1.000 0.07044 +0.2311j\n", + " 3.162 0.1904 -0.04416j\n", + " 10.000 0.002865 -0.09596j\n", + "----\n", + "\n", + "FrequencyResponseData: sys_mtf_zpk$sampled, dt=0:\n", + "-------------------------------------------------\n", + ": sys_mtf_zpk$sampled\n", + "Inputs (2): ['u[0]', 'u[1]']\n", + "Outputs (2): ['y[0]', 'y[1]']\n", + "\n", + "Input 1 to output 1:\n", + "\n", + " Freq [rad/s] Response\n", + " ------------ ---------------------\n", + " 0.100 -0.2437 +0.0556j\n", + " 0.316 -0.192 +0.1589j\n", + " 1.000 0.05882 +0.2353j\n", + " 3.162 0.1958 -0.01106j\n", + " 10.000 0.05087 -0.07767j\n", + "\n", + "Input 1 to output 2:\n", + "\n", + " Freq [rad/s] Response\n", + " ------------ ---------------------\n", + " 0.100 0 +0.1j\n", + " 0.316 0 +0.3162j\n", + " 1.000 0 +1j\n", + " 3.162 0 +3.162j\n", + " 10.000 0 +10j\n", + "\n", + "Input 2 to output 1:\n", + "\n", + " Freq [rad/s] Response\n", + " ------------ ---------------------\n", + " 0.100 0 +0j\n", + " 0.316 0 +0j\n", + " 1.000 0 +0j\n", + " 3.162 0 +0j\n", + " 10.000 0 +0j\n", + "\n", + "Input 2 to output 2:\n", + "\n", + " Freq [rad/s] Response\n", + " ------------ ---------------------\n", + " 0.100 0.01961 +0.09705j\n", + " 0.316 0.1653 +0.2352j\n", + " 1.000 0.5 +0j\n", + " 3.162 0.1653 -0.2352j\n", + " 10.000 0.01961 -0.09705j\n", + "----\n", + "\n", + "NonlinearIOSystem: sys_nl, dt=0:\n", + "--------------------------------\n", + ": sys_nl\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "States (2): ['x[0]', 'x[1]']\n", + "\n", + "Update: \n", + "Output: \n", + "----\n", + "\n", + "NonlinearIOSystem: sys_dnl, dt=0.1:\n", + "-----------------------------------\n", + ": sys_dnl\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "States (2): ['x[0]', 'x[1]']\n", + "dt=0.1\n", + "\n", + "Update: \n", + "Output: \n", + "----\n", + "\n", + "InterconnectedSystem: sys_ic, dt=0:\n", + "-----------------------------------\n", + ": sys_ic\n", + "Inputs (2): ['r[0]', 'r[1]']\n", + "Outputs (2): ['y[0]', 'y[1]']\n", + "States (2): ['proc_nl_x[0]', 'proc_nl_x[1]']\n", + "\n", + "Update: .updfcn at 0x12c6562a0>\n", + "Output: .outfcn at 0x12c656340>\n", + "\n", + "Subsystems (2):\n", + " * ['y[0]', 'y[1]']>\n", + " * ['y[0]', 'y[1]']>\n", + "\n", + "Connections:\n", + " * proc_nl.u[0] <- ctrl_nl.y[0]\n", + " * proc_nl.u[1] <- ctrl_nl.y[1]\n", + " * ctrl_nl.u[0] <- -proc_nl.y[0] + r[0]\n", + " * ctrl_nl.u[1] <- -proc_nl.y[1] + r[1]\n", + "\n", + "Outputs:\n", + " * y[0] <- proc_nl.y[0]\n", + " * y[1] <- proc_nl.y[1]\n", + "----\n", + "\n", + "LinearICSystem: sys_ic, dt=0:\n", + "-----------------------------\n", + ": sys_ic\n", + "Inputs (2): ['r[0]', 'r[1]']\n", + "Outputs (2): ['y[0]', 'y[1]']\n", + "States (2): ['proc_x[0]', 'proc_x[1]']\n", + "\n", + "A = [[-2. 3.]\n", + " [-1. -5.]]\n", + "\n", + "B = [[-2. 0.]\n", + " [ 0. -3.]]\n", + "\n", + "C = [[-1. 1.]\n", + " [ 1. 0.]]\n", + "\n", + "D = [[0. 0.]\n", + " [0. 0.]]\n", + "\n", + "Subsystems (2):\n", + " * ['y[0]', 'y[1]']>\n", + " * ['y[0]', 'y[1]'], dt=None>\n", + "\n", + "Connections:\n", + " * proc.u[0] <- ctrl.y[0]\n", + " * proc.u[1] <- ctrl.y[1]\n", + " * ctrl.u[0] <- -proc.y[0] + r[0]\n", + " * ctrl.u[1] <- -proc.y[1] + r[1]\n", + "\n", + "Outputs:\n", + " * y[0] <- proc.y[0]\n", + " * y[1] <- proc.y[1]\n", + "----\n", + "\n", + "FlatSystem: sys_fs, dt=0:\n", + "-------------------------\n", + ": sys_fs\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "States (2): ['x[0]', 'x[1]']\n", + "\n", + "Update: ['y[0]']>>\n", + "Output: ['y[0]']>>\n", + "\n", + "Forward: \n", + "Reverse: \n", + "----\n", + "\n", + "FlatSystem: sys_fsnl, dt=0:\n", + "---------------------------\n", + ": sys_fsnl\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "States (2): ['x[0]', 'x[1]']\n", + "\n", + "Update: \n", + "Output: \n", + "\n", + "Forward: \n", + "Reverse: \n", + "----\n", + "\n", + "==============================================================================\n", + " repr_format='eval'\n", + "==============================================================================\n", + "\n", + "StateSpace: sys_ss, dt=0:\n", + "-------------------------\n", + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss')\n", + "----\n", + "\n", + "StateSpace: sys_dss, dt=0.1:\n", + "----------------------------\n", + "StateSpace(\n", + "array([[ 0.98300988, 0.07817246],\n", + " [-0.31268983, 0.59214759]]),\n", + "array([[0.00424753],\n", + " [0.07817246]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "dt=0.1,\n", + "name='sys_dss')\n", + "----\n", + "\n", + "StateSpace: stateless, dt=None:\n", + "-------------------------------\n", + "StateSpace(\n", + "array([], shape=(0, 0), dtype=float64),\n", + "array([], shape=(0, 2), dtype=float64),\n", + "array([], shape=(2, 0), dtype=float64),\n", + "array([[1., 0.],\n", + " [0., 1.]]),\n", + "dt=None,\n", + "name='stateless')\n", + "----\n", + "\n", + "TransferFunction: sys_ss$converted, dt=0:\n", + "-----------------------------------------\n", + "TransferFunction(\n", + "array([ 1., -1.]),\n", + "array([1., 5., 4.]),\n", + "name='sys_ss$converted')\n", + "----\n", + "\n", + "TransferFunction: sys_dss_poly, dt=0.1:\n", + "---------------------------------------\n", + "TransferFunction(\n", + "array([ 0.07392493, -0.08176823]),\n", + "array([ 1. , -1.57515746, 0.60653066]),\n", + "dt=0.1,\n", + "name='sys_dss_poly')\n", + "----\n", + "\n", + "TransferFunction: sys[3], dt=0:\n", + "-------------------------------\n", + "TransferFunction(\n", + "array([1]),\n", + "array([1, 0]))\n", + "----\n", + "\n", + "TransferFunction: sys_mtf_zpk, dt=0:\n", + "------------------------------------\n", + "TransferFunction(\n", + "[[array([ 1., -1.]), array([0.])],\n", + " [array([1, 0]), array([1, 0])]],\n", + "[[array([1., 5., 4.]), array([1.])],\n", + " [array([1]), array([1, 2, 1])]],\n", + "name='sys_mtf_zpk')\n", + "----\n", + "\n", + "FrequencyResponseData: sys_ss$converted$sampled, dt=0:\n", + "------------------------------------------------------\n", + "FrequencyResponseData(\n", + "array([[[-0.24365959+0.05559644j, -0.19198193+0.1589174j ,\n", + " 0.05882353+0.23529412j, 0.1958042 -0.01105691j,\n", + " 0.0508706 -0.07767156j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", + "name='sys_ss$converted$sampled', outputs=1, inputs=1)\n", + "----\n", + "\n", + "FrequencyResponseData: sys_dss_poly$sampled, dt=0.1:\n", + "----------------------------------------------------\n", + "FrequencyResponseData(\n", + "array([[[-0.24337799+0.05673083j, -0.18944184+0.16166381j,\n", + " 0.07043649+0.23113479j, 0.19038528-0.04416494j,\n", + " 0.00286505-0.09595906j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ])\n", + "dt=0.1,\n", + "name='sys_dss_poly$sampled', outputs=1, inputs=1)\n", + "----\n", + "\n", + "FrequencyResponseData: sys_mtf_zpk$sampled, dt=0:\n", + "-------------------------------------------------\n", + "FrequencyResponseData(\n", + "array([[[-0.24365959 +0.05559644j, -0.19198193 +0.1589174j ,\n", + " 0.05882353 +0.23529412j, 0.1958042 -0.01105691j,\n", + " 0.0508706 -0.07767156j],\n", + " [ 0. +0.j , 0. +0.j ,\n", + " 0. +0.j , 0. +0.j ,\n", + " 0. +0.j ]],\n", + "\n", + " [[ 0. +0.1j , 0. +0.31622777j,\n", + " 0. +1.j , 0. +3.16227766j,\n", + " 0. +10.j ],\n", + " [ 0.01960592 +0.09704931j, 0.16528926 +0.23521074j,\n", + " 0.5 +0.j , 0.16528926 -0.23521074j,\n", + " 0.01960592 -0.09704931j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", + "name='sys_mtf_zpk$sampled', outputs=2, inputs=2)\n", + "----\n", + "\n", + "NonlinearIOSystem: sys_nl, dt=0:\n", + "--------------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "NonlinearIOSystem: sys_dnl, dt=0.1:\n", + "-----------------------------------\n", + " ['y[0]'], dt=0.1>\n", + "----\n", + "\n", + "InterconnectedSystem: sys_ic, dt=0:\n", + "-----------------------------------\n", + " ['y[0]', 'y[1]']>\n", + "----\n", + "\n", + "LinearICSystem: sys_ic, dt=0:\n", + "-----------------------------\n", + "StateSpace(\n", + "array([[-2., 3.],\n", + " [-1., -5.]]),\n", + "array([[-2., 0.],\n", + " [ 0., -3.]]),\n", + "array([[-1., 1.],\n", + " [ 1., 0.]]),\n", + "array([[0., 0.],\n", + " [0., 0.]]),\n", + "name='sys_ic', states=['proc_x[0]', 'proc_x[1]'], inputs=['r[0]', 'r[1]'])\n", + "----\n", + "\n", + "FlatSystem: sys_fs, dt=0:\n", + "-------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "FlatSystem: sys_fsnl, dt=0:\n", + "---------------------------\n", + " ['y[0]']>\n", + "----\n", + "\n", + "==============================================================================\n", + " xferfcn.display_format=zpk, str (print)\n", + "==============================================================================\n", + "\n", + "TransferFunction: sys_ss$converted, dt=0:\n", + "-----------------------------------------\n", + ": sys_ss$converted\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "\n", + " s - 1\n", + " ---------------\n", + " (s + 1) (s + 4)\n", + "----\n", + "\n", + "TransferFunction: sys_dss_poly, dt=0.1:\n", + "---------------------------------------\n", + ": sys_dss_poly\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "dt=0.1\n", + "\n", + " 0.07392 z - 0.08177\n", + " ----------------------\n", + " z^2 - 1.575 z + 0.6065\n", + "----\n", + "\n", + "TransferFunction: sys[3], dt=0:\n", + "-------------------------------\n", + ": sys[3]\n", + "Inputs (1): ['u[0]']\n", + "Outputs (1): ['y[0]']\n", + "\n", + " 1\n", + " -\n", + " s\n", + "----\n", + "\n", + "TransferFunction: sys_mtf_zpk, dt=0:\n", + "------------------------------------\n", + ": sys_mtf_zpk\n", + "Inputs (2): ['u[0]', 'u[1]']\n", + "Outputs (2): ['y[0]', 'y[1]']\n", + "\n", + "Input 1 to output 1:\n", + "\n", + " s - 1\n", + " ---------------\n", + " (s + 1) (s + 4)\n", + "\n", + "Input 1 to output 2:\n", + "\n", + " s\n", + " -\n", + " 1\n", + "\n", + "Input 2 to output 1:\n", + "\n", + " 0\n", + " -\n", + " 1\n", + "\n", + "Input 2 to output 2:\n", + "\n", + " s\n", + " ---------------\n", + " (s + 1) (s + 1)\n", + "----\n", + "\n" + ] + } + ], + "source": [ + "# Grab system definitions (and generate various representations as text)\n", + "from repr_gallery import *" + ] + }, + { + "cell_type": "markdown", + "id": "19f146a3-c036-4ff6-8425-c201fba14ec7", + "metadata": {}, + "source": [ + "## Jupyter notebook (HTML/LaTeX) representations\n", + "\n", + "The following representations are generated using the `_html_repr_` method in selected types of systems. Only those systems that have unique displays are shown." + ] + }, + { + "cell_type": "markdown", + "id": "16ff8d11-e793-456a-bf27-ae4cc0dd1e3b", + "metadata": {}, + "source": [ + "### Continuous time state space systems" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c1ca661d-10f3-45be-8619-c3e143bb4b4c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>$$\n", + "\\left[\\begin{array}{rllrll|rll}\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "-4\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\hline\n", + "-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + " ['y[0]']>" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_ss" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b10b4db3-a8c0-4a2c-a19d-a09fef3dc25f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>$$\n", + "\\begin{array}{ll}\n", + "A = \\left[\\begin{array}{rllrll}\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "-4\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "&\n", + "B = \\left[\\begin{array}{rll}\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "\\\\\n", + "C = \\left[\\begin{array}{rllrll}\n", + "-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "&\n", + "D = \\left[\\begin{array}{rll}\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "\\end{array}\n", + "$$" + ], + "text/plain": [ + " ['y[0]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with ct.config.defaults({'statesp.latex_repr_type': 'separate'}):\n", + " display(sys_ss)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0537f6fe-a155-4c49-be7c-413608a03887", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>$$\n", + "\\begin{array}{ll}\n", + "A = \\left[\\begin{array}{rllrll}\n", + " 0.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}& 1.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + " -4.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}& -5.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "&\n", + "B = \\left[\\begin{array}{rll}\n", + " 0.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + " 1.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "\\\\\n", + "C = \\left[\\begin{array}{rllrll}\n", + " -1.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}& 1.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "&\n", + "D = \\left[\\begin{array}{rll}\n", + " 0.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "\\end{array}\n", + "$$" + ], + "text/plain": [ + " ['y[0]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with ct.config.defaults({\n", + " 'statesp.latex_repr_type': 'separate',\n", + " 'statesp.latex_num_format': '8.4f'}):\n", + " display(sys_ss)" + ] + }, + { + "cell_type": "markdown", + "id": "fa75f040-633d-401c-ba96-e688713d5a2d", + "metadata": {}, + "source": [ + "### Stateless state space system" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "5a71e38c-9880-4af7-82e0-49f074653e94", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>$$\n", + "\\left[\\begin{array}{rllrll}\n", + "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + " ['y[0]', 'y[1]'], dt=None>" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_ss0" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "7ddbd638-9338-4204-99bc-792f35e14874", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>$$\n", + "\\begin{array}{ll}\n", + "D = \\left[\\begin{array}{rllrll}\n", + "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "\\end{array}\n", + "$$" + ], + "text/plain": [ + " ['y[0]', 'y[1]'], dt=None>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with ct.config.defaults({'statesp.latex_repr_type': 'separate'}):\n", + " display(sys_ss0)" + ] + }, + { + "cell_type": "markdown", + "id": "06c5d470-0768-4628-b2ea-d2387525ed80", + "metadata": {}, + "source": [ + "### Discrete time state space system" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e7b9b438-28e3-453e-9860-06ff75b7af10", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace sys_dss: ['u[0]'] -> ['y[0]'], dt=0.1>$$\n", + "\\left[\\begin{array}{rllrll|rll}\n", + "0.&\\hspace{-1em}983&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}0782&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}00425&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "-0.&\\hspace{-1em}313&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}592&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}0782&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\hline\n", + "-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + " ['y[0]'], dt=0.1>" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_dss" + ] + }, + { + "cell_type": "markdown", + "id": "7719e725-9d38-4f2a-a142-0ebc090e74e4", + "metadata": {}, + "source": [ + "### \"Stateless\" state space system" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "938fd795-f402-4491-b2c9-eb42c458e1e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>$$\n", + "\\left[\\begin{array}{rllrll}\n", + "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + " ['y[0]', 'y[1]'], dt=None>" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_ss0" + ] + }, + { + "cell_type": "markdown", + "id": "c620f1a1-40ff-4320-9f62-21bff9ab308e", + "metadata": {}, + "source": [ + "### Continuous time transfer function" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e364e6eb-0cfa-486a-8b95-ff9c6c41a091", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<TransferFunction sys_ss\\$converted: ['u[0]'] -> ['y[0]']>$$\\dfrac{s - 1}{s^2 + 5 s + 4}$$" + ], + "text/plain": [ + " ['y[0]']>" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_tf" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "af9959fd-90eb-4287-93ee-416cd13fde50", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<TransferFunction sys_ss\\$converted: ['u[0]'] -> ['y[0]']>$$\\dfrac{s - 1}{(s + 1) (s + 4)}$$" + ], + "text/plain": [ + " ['y[0]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with ct.config.defaults({'xferfcn.display_format': 'zpk'}):\n", + " display(sys_tf)" + ] + }, + { + "cell_type": "markdown", + "id": "7bf40707-f84c-4e19-b310-5ec9811faf42", + "metadata": {}, + "source": [ + "### MIMO transfer function" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b2db2d1c-893b-43a1-aab0-a5f6d059f3f9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/murray/miniconda3/envs/python3.13-slycot/lib/python3.13/site-packages/scipy/signal/_filter_design.py:1112: BadCoefficients: Badly conditioned filter coefficients (numerator): the results may be meaningless\n", + " b, a = normalize(b, a)\n", + "/Users/murray/miniconda3/envs/python3.13-slycot/lib/python3.13/site-packages/scipy/signal/_filter_design.py:1116: RuntimeWarning: invalid value encountered in divide\n", + " b /= b[0]\n" + ] + }, + { + "data": { + "text/html": [ + "<TransferFunction sys_mtf_zpk: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']>$$\\begin{bmatrix}\\dfrac{s - 1}{(s + 1) (s + 4)}&\\dfrac{0}{1}\\\\\\dfrac{s}{1}&\\dfrac{s}{(s + 1) (s + 1)}\\\\ \\end{bmatrix}$$" + ], + "text/plain": [ + " ['y[0]', 'y[1]']>" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_mtf # SciPy generates a warning due to 0 numerator in 1, 2 entry" + ] + }, + { + "cell_type": "markdown", + "id": "ef78a05e-9a63-4e22-afae-66ac8ec457c2", + "metadata": {}, + "source": [ + "### Discrete time transfer function" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "365c9b4a-2af3-42e5-ae5d-f2d7d989104b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<TransferFunction sys_dss_poly: ['u[0]'] -> ['y[0]'], dt=0.1>$$\\dfrac{0.07392 z - 0.08177}{z^2 - 1.575 z + 0.6065}$$" + ], + "text/plain": [ + " ['y[0]'], dt=0.1>" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_dtf" + ] + }, + { + "cell_type": "markdown", + "id": "b49fa8ab-c3af-48d1-b160-790c9f4d3c6e", + "metadata": {}, + "source": [ + "### Linear interconnected system" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "78d21969-4615-4a47-b449-7a08138dc319", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<LinearICSystem sys_ic: ['r[0]', 'r[1]'] -> ['y[0]', 'y[1]']>$$\n", + "\\left[\\begin{array}{rllrll|rllrll}\n", + "-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&3\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-3\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\hline\n", + "-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + " ['y[0]', 'y[1]']>" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_lic" + ] + }, + { + "cell_type": "markdown", + "id": "bee65cd5-d9b5-46af-aee5-26a6a4679939", + "metadata": {}, + "source": [ + "### Non-HTML capable system representations" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e5486349-2bd3-4015-ad17-a5b8e8ec0447", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " ['y[0]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " ['y[0]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " ['y[0]', 'y[1]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " ['y[0]']>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for sys in [sys_frd, sys_nl, sys_ic, sys_fs]:\n", + " display(sys)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 786369055b2f5cfc3677ec335484a4257b50b096 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 31 Dec 2024 11:51:16 -0800 Subject: [PATCH 16/21] update unit tests + tweak formatting for consistency --- control/frdata.py | 2 +- control/iosys.py | 11 +- control/statesp.py | 8 +- control/tests/docstrings_test.py | 2 +- control/tests/frd_test.py | 32 +++--- control/tests/iosys_test.py | 42 +++---- control/tests/lti_test.py | 4 +- control/tests/statesp_test.py | 119 ++++++++++---------- control/tests/xferfcn_test.py | 183 ++++++++++++++++--------------- control/xferfcn.py | 4 +- 10 files changed, 212 insertions(+), 195 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index cb6925661..625444195 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -425,7 +425,7 @@ def _repr_eval_(self): smooth=(self._ifunc and ", smooth=True") or "") out += self._dt_repr() - if len(labels := self._label_repr()) > 0: + if len(labels := self._label_repr(show_count=False)) > 0: out += ",\n" + labels out += ")" diff --git a/control/iosys.py b/control/iosys.py index e2a7619cd..deb9b2ddc 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -253,7 +253,7 @@ def __str__(self): out += f"\nOutputs ({self.noutputs}): {self.output_labels}" if self.nstates is not None: out += f"\nStates ({self.nstates}): {self.state_labels}" - out += self._dt_repr("\n") + out += self._dt_repr(separator="\n", space=" ") return out def __repr__(self): @@ -262,7 +262,7 @@ def __repr__(self): def _repr_info_(self, html=False): out = f"<{self.__class__.__name__} {self.name}: " + \ f"{list(self.input_labels)} -> {list(self.output_labels)}" - out += self._dt_repr(", ") + ">" + out += self._dt_repr(separator=", ", space="") + ">" if html: # Replace symbols that might be interpreted by HTML processing @@ -353,10 +353,11 @@ def _label_repr(self, show_count=True): return out - def _dt_repr(self, separator="\n"): + def _dt_repr(self, separator="\n", space=""): if config.defaults['control.default_dt'] != self.dt: - return "{separator}dt={dt}".format( - separator=separator, dt='None' if self.dt is None else self.dt) + return "{separator}dt{space}={space}{dt}".format( + separator=separator, space=space, + dt='None' if self.dt is None else self.dt) else: return "" diff --git a/control/statesp.py b/control/statesp.py index 282969c38..25c22cf8c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -394,7 +394,7 @@ def _repr_eval_(self): A=self.A.__repr__(), B=self.B.__repr__(), C=self.C.__repr__(), D=self.D.__repr__()) - out += super()._dt_repr(",\n") + out += super()._dt_repr(separator=",\n", space="") if len(labels := super()._label_repr(show_count=False)) > 0: out += ",\n" + labels @@ -422,9 +422,11 @@ def _repr_html_(self): if syssize > config.defaults['statesp.latex_maxsize']: return None elif config.defaults['statesp.latex_repr_type'] == 'partitioned': - return super()._repr_info_(html=True) + self._latex_partitioned() + return super()._repr_info_(html=True) + \ + "\n" + self._latex_partitioned() elif config.defaults['statesp.latex_repr_type'] == 'separate': - return super()._repr_info_(html=True) + self._latex_separate() + return super()._repr_info_(html=True) + \ + "\n" + self._latex_separate() else: raise ValueError( "Unknown statesp.latex_repr_type '{cfg}'".format( diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index 16647895a..0c0a7904f 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -35,7 +35,7 @@ control.lqe: '567bf657538935173f2e50700ba87168', control.lqr: 'a3e0a85f781fc9c0f69a4b7da4f0bd22', control.margin: 'f02b3034f5f1d44ce26f916cc3e51600', - control.parallel: '025c5195a34c57392223374b6244a8c4', + control.parallel: 'bfc470aef75dbb923f9c6fb8bf3c9b43', control.series: '9aede1459667738f05cf4fc46603a4f6', control.ss2tf: '48ff25d22d28e7b396e686dd5eb58831', control.tf2ss: '086a3692659b7321c2af126f79f4bc11', diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index c9f5ca7f8..3741b4e88 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -468,9 +468,8 @@ def test_repr_str(self): ref_common = "FrequencyResponseData(\n" \ "array([[[1. +0.j , 0.9 +0.1j, 0.1 +2.j , 0.05+3.j ]]]),\n" \ "array([ 0.1, 1. , 10. , 100. ])," - ref0 = ref_common + "\nname='sys0', outputs=1, inputs=1)" - ref1 = ref_common + " smooth=True," + \ - "\nname='sys1', outputs=1, inputs=1)" + ref0 = ref_common + "\nname='sys0')" + ref1 = ref_common + " smooth=True," + "\nname='sys1')" sysm = ct.frd( np.matmul(array([[1], [2]]), sys0.fresp), sys0.omega, name='sysm') @@ -505,19 +504,22 @@ def test_repr_str(self): Outputs (1): ['y[0]'] Input 1 to output 1: -Freq [rad/s] Response ------------- --------------------- - 0.100 1 +0j - 1.000 0.9 +0.1j - 10.000 0.1 +2j - 100.000 0.05 +3j + + Freq [rad/s] Response + ------------ --------------------- + 0.100 1 +0j + 1.000 0.9 +0.1j + 10.000 0.1 +2j + 100.000 0.05 +3j + Input 2 to output 1: -Freq [rad/s] Response ------------- --------------------- - 0.100 2 +0j - 1.000 1.8 +0.2j - 10.000 0.2 +4j - 100.000 0.1 +6j""" + + Freq [rad/s] Response + ------------ --------------------- + 0.100 2 +0j + 1.000 1.8 +0.2j + 10.000 0.2 +4j + 100.000 0.1 +6j""" assert str(sysm) == refm def test_unrecognized_keyword(self): diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 45582f569..ce87e63bc 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2290,32 +2290,32 @@ def test_signal_indexing(): @slycotonly @pytest.mark.parametrize("fcn, spec, expected, missing", [ - (ct.ss, {}, "\nstates=4, outputs=3, inputs=2", r"dt|name"), - (ct.tf, {}, "\noutputs=3, inputs=2", r"dt|name|states"), - (ct.frd, {}, "\noutputs=3, inputs=2", r"dt|states|name"), - (ct.ss, {'dt': 0.1}, ".*\ndt=0.1,\nstates=4, outputs=3, inputs=2", r"name"), - (ct.tf, {'dt': 0.1}, ".*\ndt=0.1,\noutputs=3, inputs=2", r"name|states"), + (ct.ss, {}, "", r"states|inputs|outputs|dt|name"), + (ct.tf, {}, "", r"states|inputs|outputs|dt|name"), + (ct.frd, {}, "", r"states|inputs|outputs|dt|name"), + (ct.ss, {'dt': 0.1}, ".*\ndt=0.1", r"states|inputs|outputs|name"), + (ct.tf, {'dt': 0.1}, ".*\ndt=0.1", r"states|inputs|outputs|name"), (ct.frd, {'dt': 0.1}, - ".*\ndt=0.1,\noutputs=3, inputs=2", r"name|states"), - (ct.ss, {'dt': True}, "\ndt=True,\nstates=4, outputs=3, inputs=2", r"name"), - (ct.ss, {'dt': None}, "\ndt=None,\nstates=4, outputs=3, inputs=2", r"name"), - (ct.ss, {'dt': 0}, "\nstates=4, outputs=3, inputs=2", r"dt|name"), - (ct.ss, {'name': 'mysys'}, "\nname='mysys',", r"dt"), - (ct.tf, {'name': 'mysys'}, "\nname='mysys',", r"dt|states"), - (ct.frd, {'name': 'mysys'}, "\nname='mysys',", r"dt|states"), + ".*\ndt=0.1", r"states|inputs|outputs|name"), + (ct.ss, {'dt': True}, "\ndt=True", r"states|inputs|outputs|name"), + (ct.ss, {'dt': None}, "\ndt=None", r"states|inputs|outputs|name"), + (ct.ss, {'dt': 0}, "", r"states|inputs|outputs|dt|name"), + (ct.ss, {'name': 'mysys'}, "\nname='mysys'", r"dt"), + (ct.tf, {'name': 'mysys'}, "\nname='mysys'", r"dt|states"), + (ct.frd, {'name': 'mysys'}, "\nname='mysys'", r"dt|states"), (ct.ss, {'inputs': ['u1']}, - r"[\n]states=4, outputs=3, inputs=\['u1'\]", r"dt|name"), + r"[\n]inputs=\['u1'\]", r"states|outputs|dt|name"), (ct.tf, {'inputs': ['u1']}, - r"[\n]outputs=3, inputs=\['u1'\]", r"dt|name"), + r"[\n]inputs=\['u1'\]", r"outputs|dt|name"), (ct.frd, {'inputs': ['u1'], 'name': 'sampled'}, - r"[\n]name='sampled', outputs=3, inputs=\['u1'\]", r"dt"), + r"[\n]name='sampled', inputs=\['u1'\]", r"outputs|dt"), (ct.ss, {'outputs': ['y1']}, - r"[\n]states=4, outputs=\['y1'\], inputs=2", r"dt|name"), + r"[\n]outputs=\['y1'\]", r"states|inputs|dt|name"), (ct.ss, {'name': 'mysys', 'inputs': ['u1']}, - r"[\n]name='mysys', states=4, outputs=3, inputs=\['u1'\]", r"dt"), + r"[\n]name='mysys', inputs=\['u1'\]", r"states|outputs|dt"), (ct.ss, {'name': 'mysys', 'states': [ 'long_state_1', 'long_state_2', 'long_state_3']}, - r"[\n]name='.*', states=\[.*\],[\n]outputs=3, inputs=2\)", r"dt"), + r"[\n]name='.*', states=\[.*\]\)", r"inputs|outputs|dt"), ]) @pytest.mark.parametrize("format", ['info', 'eval']) def test_iosys_repr(fcn, spec, expected, missing, format): @@ -2335,7 +2335,11 @@ def test_iosys_repr(fcn, spec, expected, missing, format): # Construct the 'info' format info_expected = f"<{sys.__class__.__name__} {sys.name}: " \ - f"{sys.input_labels} -> {sys.output_labels}>" + f"{sys.input_labels} -> {sys.output_labels}" + if sys.dt != 0: + info_expected += f", dt={sys.dt}>" + else: + info_expected += ">" # Make sure the default format is OK out = repr(sys) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index debc4b941..e93138af3 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -408,5 +408,5 @@ def test_printoptions( if str_expected is not None: assert re.search(str_expected, str(sys)) is not None - # Test LaTeX representation - assert re.search(latex_expected, sys._repr_latex_()) is not None + # Test LaTeX/HTML representation + assert re.search(latex_expected, sys._repr_html_()) is not None diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index b4a0ebb56..fff22a5bd 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -735,23 +735,22 @@ def test_lft(self): def test_repr(self, sys322): """Test string representation""" - ref322 = "\n".join( - ["StateSpace(", - "array([[-3., 4., 2.],", - " [-1., -3., 0.],", - " [ 2., 5., 3.]]),", - "array([[ 1., 4.],", - " [-3., -3.],", - " [-2., 1.]]),", - "array([[ 4., 2., -3.],", - " [ 1., 4., 3.]]),", - "array([[-2., 4.],", - " [ 0., 1.]]){dt},", - "name='sys322', states=3, outputs=2, inputs=2)"]) + ref322 = """StateSpace( +array([[-3., 4., 2.], + [-1., -3., 0.], + [ 2., 5., 3.]]), +array([[ 1., 4.], + [-3., -3.], + [-2., 1.]]), +array([[ 4., 2., -3.], + [ 1., 4., 3.]]), +array([[-2., 4.], + [ 0., 1.]]), +name='sys322'{dt})""" assert ct.iosys_repr(sys322, format='eval') == ref322.format(dt='') sysd = StateSpace(sys322.A, sys322.B, sys322.C, sys322.D, 0.4) - assert ct.iosys_repr(sysd, format='eval'), ref322.format(dt="\ndt=0.4") + assert ct.iosys_repr(sysd, format='eval'), ref322.format(dt=",\ndt=0.4") array = np.array # noqa sysd2 = eval(ct.iosys_repr(sysd, format='eval')) np.testing.assert_allclose(sysd.A, sysd2.A) @@ -762,31 +761,31 @@ def test_repr(self, sys322): def test_str(self, sys322): """Test that printing the system works""" tsys = sys322 - tref = (": sys322\n" - "Inputs (2): ['u[0]', 'u[1]']\n" - "Outputs (2): ['y[0]', 'y[1]']\n" - "States (3): ['x[0]', 'x[1]', 'x[2]']\n" - "\n" - "A = [[-3. 4. 2.]\n" - " [-1. -3. 0.]\n" - " [ 2. 5. 3.]]\n" - "\n" - "B = [[ 1. 4.]\n" - " [-3. -3.]\n" - " [-2. 1.]]\n" - "\n" - "C = [[ 4. 2. -3.]\n" - " [ 1. 4. 3.]]\n" - "\n" - "D = [[-2. 4.]\n" - " [ 0. 1.]]") - assert str(tsys) == tref + tref = """: sys322 +Inputs (2): ['u[0]', 'u[1]'] +Outputs (2): ['y[0]', 'y[1]'] +States (3): ['x[0]', 'x[1]', 'x[2]']{dt} + +A = [[-3. 4. 2.] + [-1. -3. 0.] + [ 2. 5. 3.]] + +B = [[ 1. 4.] + [-3. -3.] + [-2. 1.]] + +C = [[ 4. 2. -3.] + [ 1. 4. 3.]] + +D = [[-2. 4.] + [ 0. 1.]]""" + assert str(tsys) == tref.format(dt='') tsysdtunspec = StateSpace( tsys.A, tsys.B, tsys.C, tsys.D, True, name=tsys.name) - assert str(tsysdtunspec) == tref + "\ndt = True\n" + assert str(tsysdtunspec) == tref.format(dt="\ndt = True") sysdt1 = StateSpace( tsys.A, tsys.B, tsys.C, tsys.D, 1., name=tsys.name) - assert str(sysdt1) == tref + "\ndt = {}\n".format(1.) + assert str(sysdt1) == tref.format(dt="\ndt = 1.0") def test_pole_static(self): """Regression: poles() of static gain is empty array.""" @@ -1055,7 +1054,7 @@ def test_statespace_defaults(self): "{} is {} but expected {}".format(k, defaults[k], v) -# test data for test_latex_repr below +# test data for test_html_repr below LTX_G1 = ([[np.pi, 1e100], [-1.23456789, 5e-23]], [[0], [1]], [[987654321, 0.001234]], @@ -1067,23 +1066,23 @@ def test_statespace_defaults(self): [[1.2345, -2e-200], [-1, 0]]) LTX_G1_REF = { - 'p3_p' : " ['y[0]']>\n$$\n\\left(\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", + 'p3_p': "<StateSpace sys: ['u[0]'] -> ['y[0]']{dt}>\n$$\n\\left[\\begin{{array}}{{rllrll|rll}}\n3.&\\hspace{{-1em}}14&\\hspace{{-1em}}\\phantom{{\\cdot}}&1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{100}}&0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n-1.&\\hspace{{-1em}}23&\\hspace{{-1em}}\\phantom{{\\cdot}}&5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-23}}&1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\hline\n9.&\\hspace{{-1em}}88&\\hspace{{-1em}}\\cdot10^{{8}}&0.&\\hspace{{-1em}}00123&\\hspace{{-1em}}\\phantom{{\\cdot}}&5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n$$", - 'p5_p' : " ['y[0]']>\n$$\n\\left(\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", + 'p5_p': "<StateSpace sys: ['u[0]'] -> ['y[0]']{dt}>\n$$\n\\left[\\begin{{array}}{{rllrll|rll}}\n3.&\\hspace{{-1em}}1416&\\hspace{{-1em}}\\phantom{{\\cdot}}&1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{100}}&0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n-1.&\\hspace{{-1em}}2346&\\hspace{{-1em}}\\phantom{{\\cdot}}&5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-23}}&1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\hline\n9.&\\hspace{{-1em}}8765&\\hspace{{-1em}}\\cdot10^{{8}}&0.&\\hspace{{-1em}}001234&\\hspace{{-1em}}\\phantom{{\\cdot}}&5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n$$", - 'p3_s' : " ['y[0]']>\n$$\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", + 'p3_s': "<StateSpace sys: ['u[0]'] -> ['y[0]']{dt}>\n$$\n\\begin{{array}}{{ll}}\nA = \\left[\\begin{{array}}{{rllrll}}\n3.&\\hspace{{-1em}}14&\\hspace{{-1em}}\\phantom{{\\cdot}}&1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{100}}\\\\\n-1.&\\hspace{{-1em}}23&\\hspace{{-1em}}\\phantom{{\\cdot}}&5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-23}}\\\\\n\\end{{array}}\\right]\n&\nB = \\left[\\begin{{array}}{{rll}}\n0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n\\\\\nC = \\left[\\begin{{array}}{{rllrll}}\n9.&\\hspace{{-1em}}88&\\hspace{{-1em}}\\cdot10^{{8}}&0.&\\hspace{{-1em}}00123&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n&\nD = \\left[\\begin{{array}}{{rll}}\n5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n\\end{{array}}\n$$", - 'p5_s' : " ['y[0]']>\n$$\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", + 'p5_s': "<StateSpace sys: ['u[0]'] -> ['y[0]']{dt}>\n$$\n\\begin{{array}}{{ll}}\nA = \\left[\\begin{{array}}{{rllrll}}\n3.&\\hspace{{-1em}}1416&\\hspace{{-1em}}\\phantom{{\\cdot}}&1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{100}}\\\\\n-1.&\\hspace{{-1em}}2346&\\hspace{{-1em}}\\phantom{{\\cdot}}&5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-23}}\\\\\n\\end{{array}}\\right]\n&\nB = \\left[\\begin{{array}}{{rll}}\n0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n\\\\\nC = \\left[\\begin{{array}}{{rllrll}}\n9.&\\hspace{{-1em}}8765&\\hspace{{-1em}}\\cdot10^{{8}}&0.&\\hspace{{-1em}}001234&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n&\nD = \\left[\\begin{{array}}{{rll}}\n5\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n\\end{{array}}\n$$", } LTX_G2_REF = { - 'p3_p' : " ['y[0]', 'y[1]']>\n$$\n\\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", + 'p3_p': "<StateSpace sys: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']{dt}>\n$$\n\\left[\\begin{{array}}{{rllrll}}\n1.&\\hspace{{-1em}}23&\\hspace{{-1em}}\\phantom{{\\cdot}}&-2\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-200}}\\\\\n-1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}&0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n$$", - 'p5_p' : " ['y[0]', 'y[1]']>\n$$\n\\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n$$", + 'p5_p': "<StateSpace sys: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']{dt}>\n$$\n\\left[\\begin{{array}}{{rllrll}}\n1.&\\hspace{{-1em}}2345&\\hspace{{-1em}}\\phantom{{\\cdot}}&-2\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-200}}\\\\\n-1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}&0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n$$", - 'p3_s' : " ['y[0]', 'y[1]']>\n$$\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", + 'p3_s': "<StateSpace sys: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']{dt}>\n$$\n\\begin{{array}}{{ll}}\nD = \\left[\\begin{{array}}{{rllrll}}\n1.&\\hspace{{-1em}}23&\\hspace{{-1em}}\\phantom{{\\cdot}}&-2\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-200}}\\\\\n-1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}&0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n\\end{{array}}\n$$", - 'p5_s' : " ['y[0]', 'y[1]']>\n$$\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n$$", + 'p5_s': "<StateSpace sys: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']{dt}>\n$$\n\\begin{{array}}{{ll}}\nD = \\left[\\begin{{array}}{{rllrll}}\n1.&\\hspace{{-1em}}2345&\\hspace{{-1em}}\\phantom{{\\cdot}}&-2\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\cdot10^{{-200}}\\\\\n-1\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}&0\\phantom{{.}}&\\hspace{{-1em}}&\\hspace{{-1em}}\\phantom{{\\cdot}}\\\\\n\\end{{array}}\\right]\n\\end{{array}}\n$$", } refkey_n = {None: 'p3', '.3g': 'p3', '.5g': 'p5'} @@ -1094,19 +1093,19 @@ def test_statespace_defaults(self): (LTX_G2, LTX_G2_REF)]) @pytest.mark.parametrize("dt, dtref", [(0, ""), - (None, ""), - (True, r"~,~dt=~\mathrm{{True}}"), - (0.1, r"~,~dt={dt:{fmt}}")]) + (None, ", dt=None"), + (True, ", dt=True"), + (0.1, ", dt={dt:{fmt}}")]) @pytest.mark.parametrize("repr_type", [None, "partitioned", "separate"]) @pytest.mark.parametrize("num_format", [None, ".3g", ".5g"]) -def test_latex_repr(gmats, ref, dt, dtref, repr_type, num_format, editsdefaults): - """Test `._latex_repr_` with different config values +def test_html_repr(gmats, ref, dt, dtref, repr_type, num_format, editsdefaults): + """Test `._html_repr_` with different config values This is a 'gold image' test, so if you change behaviour, you'll need to regenerate the reference results. Try something like: control.reset_defaults() - print(f'p3_p : {g1._repr_latex_()!r}') + print(f'p3_p : {g1._repr_html_()!r}') """ from control import set_defaults if num_format is not None: @@ -1115,11 +1114,11 @@ def test_latex_repr(gmats, ref, dt, dtref, repr_type, num_format, editsdefaults) if repr_type is not None: set_defaults('statesp', latex_repr_type=repr_type) - g = StateSpace(*(gmats+(dt,)), name='sys') + g = StateSpace(*(gmats + (dt,)), name='sys') refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type]) - dt_latex = dtref.format(dt=dt, fmt=defaults['statesp.latex_num_format']) - ref_latex = ref[refkey][:-3] + dt_latex + ref[refkey][-3:] - assert g._repr_latex_() == ref_latex + dt_html = dtref.format(dt=dt, fmt=defaults['statesp.latex_num_format']) + ref_html = ref[refkey].format(dt=dt_html) + assert g._repr_html_() == ref_html @pytest.mark.parametrize( @@ -1142,8 +1141,8 @@ def test_xferfcn_ndarray_precedence(op, tf, arr): assert isinstance(result, ct.StateSpace) -def test_latex_repr_testsize(editsdefaults): - # _repr_latex_ returns None when size > maxsize +def test_html_repr_testsize(editsdefaults): + # _repr_html_ returns None when size > maxsize from control import set_defaults maxsize = defaults['statesp.latex_maxsize'] @@ -1155,16 +1154,16 @@ def test_latex_repr_testsize(editsdefaults): assert ninputs > 0 g = rss(nstates, ninputs, noutputs) - assert isinstance(g._repr_latex_(), str) + assert isinstance(g._repr_html_(), str) set_defaults('statesp', latex_maxsize=maxsize - 1) - assert g._repr_latex_() is None + assert g._repr_html_() is None set_defaults('statesp', latex_maxsize=-1) - assert g._repr_latex_() is None + assert g._repr_html_() is None gstatic = ss([], [], [], 1) - assert gstatic._repr_latex_() is None + assert gstatic._repr_html_() is None class TestLinfnorm: diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 4062e09ae..92509466d 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -877,18 +877,18 @@ def test_printing(self): """Print SISO""" sys = ss2tf(rss(4, 1, 1)) assert isinstance(str(sys), str) - assert isinstance(sys._repr_latex_(), str) + assert isinstance(sys._repr_html_(), str) # SISO, discrete time sys = sample_system(sys, 1) assert isinstance(str(sys), str) - assert isinstance(sys._repr_latex_(), str) + assert isinstance(sys._repr_html_(), str) @pytest.mark.parametrize( "args, output", - [(([0], [1]), "0\n-\n1\n"), - (([1.0001], [-1.1111]), " 1\n------\n-1.111\n"), - (([0, 1], [0, 1.]), "1\n-\n1\n"), + [(([0], [1]), " 0\n -\n 1"), + (([1.0001], [-1.1111]), " 1\n ------\n -1.111"), + (([0, 1], [0, 1.]), " 1\n -\n 1"), ]) def test_printing_polynomial_const(self, args, output): """Test _tf_polynomial_to_string for constant systems""" @@ -897,71 +897,77 @@ def test_printing_polynomial_const(self, args, output): @pytest.mark.parametrize( "args, outputfmt", [(([1, 0], [2, 1]), - " {var}\n-------\n2 {var} + 1\n{dtstring}"), + " {var}\n -------\n 2 {var} + 1"), (([2, 0, -1], [1, 0, 0, 1.2]), - "2 {var}^2 - 1\n---------\n{var}^3 + 1.2\n{dtstring}")]) + " 2 {var}^2 - 1\n ---------\n {var}^3 + 1.2")]) @pytest.mark.parametrize("var, dt, dtstring", [("s", None, ''), ("z", True, ''), - ("z", 1, '\ndt = 1\n')]) + ("z", 1, 'dt = 1')]) def test_printing_polynomial(self, args, outputfmt, var, dt, dtstring): """Test _tf_polynomial_to_string for all other code branches""" - assert str(TransferFunction(*(args + (dt,)))).partition('\n\n')[2] == \ - outputfmt.format(var=var, dtstring=dtstring) + polystr = str(TransferFunction(*(args + (dt,)))).partition('\n\n') + if dtstring != '': + # Make sure the last line of the header has proper dt + assert polystr[0].split('\n')[3] == dtstring + else: + # Make sure there are only three header lines (sys, in, out) + assert len(polystr[0].split('\n')) == 4 + assert polystr[2] == outputfmt.format(var=var) @slycotonly def test_printing_mimo(self): """Print MIMO, continuous time""" sys = ss2tf(rss(4, 2, 3)) assert isinstance(str(sys), str) - assert isinstance(sys._repr_latex_(), str) + assert isinstance(sys._repr_html_(), str) @pytest.mark.parametrize( "zeros, poles, gain, output", [([0], [-1], 1, - ' s\n' - '-----\n' - 's + 1\n'), + ' s\n' + ' -----\n' + ' s + 1'), ([-1], [-1], 1, - 's + 1\n' - '-----\n' - 's + 1\n'), + ' s + 1\n' + ' -----\n' + ' s + 1'), ([-1], [1], 1, - 's + 1\n' - '-----\n' - 's - 1\n'), + ' s + 1\n' + ' -----\n' + ' s - 1'), ([1], [-1], 1, - 's - 1\n' - '-----\n' - 's + 1\n'), + ' s - 1\n' + ' -----\n' + ' s + 1'), ([-1], [-1], 2, - '2 (s + 1)\n' - '---------\n' - ' s + 1\n'), + ' 2 (s + 1)\n' + ' ---------\n' + ' s + 1'), ([-1], [-1], 0, - '0\n' - '-\n' - '1\n'), + ' 0\n' + ' -\n' + ' 1'), ([-1], [1j, -1j], 1, - ' s + 1\n' - '-----------------\n' - '(s - 1j) (s + 1j)\n'), + ' s + 1\n' + ' -----------------\n' + ' (s - 1j) (s + 1j)'), ([4j, -4j], [2j, -2j], 2, - '2 (s - 4j) (s + 4j)\n' - '-------------------\n' - ' (s - 2j) (s + 2j)\n'), + ' 2 (s - 4j) (s + 4j)\n' + ' -------------------\n' + ' (s - 2j) (s + 2j)'), ([1j, -1j], [-1, -4], 2, - '2 (s - 1j) (s + 1j)\n' - '-------------------\n' - ' (s + 1) (s + 4)\n'), + ' 2 (s - 1j) (s + 1j)\n' + ' -------------------\n' + ' (s + 1) (s + 4)'), ([1], [-1 + 1j, -1 - 1j], 1, - ' s - 1\n' - '-------------------------\n' - '(s + (1-1j)) (s + (1+1j))\n'), + ' s - 1\n' + ' -------------------------\n' + ' (s + (1-1j)) (s + (1+1j))'), ([1], [1 + 1j, 1 - 1j], 1, - ' s - 1\n' - '-------------------------\n' - '(s - (1+1j)) (s - (1-1j))\n'), + ' s - 1\n' + ' -------------------------\n' + ' (s - (1+1j)) (s - (1-1j))'), ]) def test_printing_zpk(self, zeros, poles, gain, output): """Test _tf_polynomial_to_string for constant systems""" @@ -972,17 +978,17 @@ def test_printing_zpk(self, zeros, poles, gain, output): @pytest.mark.parametrize( "zeros, poles, gain, format, output", [([1], [1 + 1j, 1 - 1j], 1, ".2f", - ' 1.00\n' - '-------------------------------------\n' - '(s + (1.00-1.41j)) (s + (1.00+1.41j))\n'), + ' 1.00\n' + ' -------------------------------------\n' + ' (s + (1.00-1.41j)) (s + (1.00+1.41j))'), ([1], [1 + 1j, 1 - 1j], 1, ".3f", - ' 1.000\n' - '-----------------------------------------\n' - '(s + (1.000-1.414j)) (s + (1.000+1.414j))\n'), + ' 1.000\n' + ' -----------------------------------------\n' + ' (s + (1.000-1.414j)) (s + (1.000+1.414j))'), ([1], [1 + 1j, 1 - 1j], 1, ".6g", - ' 1\n' - '-------------------------------------\n' - '(s + (1-1.41421j)) (s + (1+1.41421j))\n') + ' 1\n' + ' -------------------------------------\n' + ' (s + (1-1.41421j)) (s + (1+1.41421j))') ]) def test_printing_zpk_format(self, zeros, poles, gain, format, output): """Test _tf_polynomial_to_string for constant systems""" @@ -998,25 +1004,30 @@ def test_printing_zpk_format(self, zeros, poles, gain, format, output): "num, den, output", [([[[11], [21]], [[12], [22]]], [[[1, -3, 2], [1, 1, -6]], [[1, 0, 1], [1, -1, -20]]], - ('Input 1 to output 1:\n' - ' 11\n' - '---------------\n' - '(s - 2) (s - 1)\n' - '\n' - 'Input 1 to output 2:\n' - ' 12\n' - '-----------------\n' - '(s - 1j) (s + 1j)\n' - '\n' - 'Input 2 to output 1:\n' - ' 21\n' - '---------------\n' - '(s - 2) (s + 3)\n' - '\n' - 'Input 2 to output 2:\n' - ' 22\n' - '---------------\n' - '(s - 5) (s + 4)\n'))]) + ("""Input 1 to output 1: + + 11 + --------------- + (s - 2) (s - 1) + +Input 1 to output 2: + + 12 + ----------------- + (s - 1j) (s + 1j) + +Input 2 to output 1: + + 21 + --------------- + (s - 2) (s + 3) + +Input 2 to output 2: + + 22 + --------------- + (s - 5) (s + 4)"""))], + ) def test_printing_zpk_mimo(self, num, den, output): """Test _tf_polynomial_to_string for constant systems""" G = tf(num, den, display_format='zpk') @@ -1046,7 +1057,7 @@ def test_size_mismatch(self): with pytest.raises(NotImplementedError): TransferFunction.feedback(sys2, sys1) - def test_latex_repr(self): + def test_html_repr(self): """Test latex printout for TransferFunction""" Hc = TransferFunction([1e-5, 2e5, 3e-4], [1.2e34, 2.3e-4, 2.3e-45], name='sys') @@ -1055,41 +1066,40 @@ def test_latex_repr(self): .1, name='sys') # TODO: make the multiplication sign configurable expmul = r'\times' - for var, H, suffix in zip(['s', 'z'], + for var, H, dtstr in zip(['s', 'z'], [Hc, Hd], - ['', r'~,~dt = 0.1']): - ref = (r" ['y[0]']>" - r'$$\frac{' + ['', ', dt=0.1']): + ref = (r"<TransferFunction sys: ['u[0]'] -> ['y[0]']" + + dtstr + r">" + "\n" + r'$$\dfrac{' r'1 ' + expmul + ' 10^{-5} ' + var + '^2 ' r'+ 2 ' + expmul + ' 10^{5} ' + var + ' + 0.0003' r'}{' r'1.2 ' + expmul + ' 10^{34} ' + var + '^2 ' r'+ 0.00023 ' + var + ' ' r'+ 2.3 ' + expmul + ' 10^{-45}' - r'}' + suffix + '$$') - assert H._repr_latex_() == ref + r'}' + '$$') + assert H._repr_html_() == ref @pytest.mark.parametrize( "Hargs, ref", [(([-1., 4.], [1., 3., 5.]), "TransferFunction(\n" "array([-1., 4.]),\n" - "array([1., 3., 5.]),\n" - "outputs=1, inputs=1)"), + "array([1., 3., 5.]))"), (([2., 3., 0.], [1., -3., 4., 0], 2.0), "TransferFunction(\n" "array([2., 3., 0.]),\n" "array([ 1., -3., 4., 0.]),\n" - "dt=2.0,\n" - "outputs=1, inputs=1)"), + "dt=2.0)"), (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3], [0, 1]]]), "TransferFunction(\n" "[[array([1]), array([2, 3])],\n" " [array([4, 5]), array([6, 7])]],\n" "[[array([6, 7]), array([4, 5])],\n" - " [array([2, 3]), array([1])]],\n" - "outputs=2, inputs=2)"), + " [array([2, 3]), array([1])]])"), (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3], [0, 1]]], 0.5), @@ -1098,8 +1108,7 @@ def test_latex_repr(self): " [array([4, 5]), array([6, 7])]],\n" "[[array([6, 7]), array([4, 5])],\n" " [array([2, 3]), array([1])]],\n" - "dt=0.5,\n" - "outputs=2, inputs=2)"), + "dt=0.5)"), ]) def test_loadable_repr(self, Hargs, ref): """Test __repr__ printout.""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 9d447cfc0..1b3bf8690 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -508,7 +508,7 @@ def _repr_eval_(self): out += "]," if i < self.noutputs - 1 else "]" out += "],\n[" if entry is self.num_array else "]" - out += super()._dt_repr(separator=",\n") + out += super()._dt_repr(separator=",\n", space="") if len(labels := self._label_repr(show_count=False)) > 0: out += ",\n" + labels @@ -522,7 +522,7 @@ def _repr_html_(self, var=None): mimo = not self.issiso() if var is None: var = 's' if self.isctime() else 'z' - out = [super()._repr_info_(html=True), '$$'] + out = [super()._repr_info_(html=True), '\n$$'] if mimo: out.append(r"\begin{bmatrix}") From e0a612f7ddf3a0f13686f95c8697f51503166ea3 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 31 Dec 2024 14:03:26 -0800 Subject: [PATCH 17/21] change default repr to 'eval' + adjust HTML formatting --- control/iosys.py | 2 +- control/nlsys.py | 6 +- control/statesp.py | 35 ++- control/tests/config_test.py | 6 +- control/tests/iosys_test.py | 14 +- control/tests/namedio_test.py | 6 +- control/tests/nlsys_test.py | 12 +- examples/repr_gallery.ipynb | 389 ++++++++++++++++++++-------------- examples/repr_gallery.py | 6 +- 9 files changed, 289 insertions(+), 187 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index deb9b2ddc..51bac2eb0 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -31,7 +31,7 @@ 'iosys.indexed_system_name_suffix': '$indexed', 'iosys.converted_system_name_prefix': '', 'iosys.converted_system_name_suffix': '$converted', - 'iosys.repr_format': 'info', + 'iosys.repr_format': 'eval', } diff --git a/control/nlsys.py b/control/nlsys.py index 5a0913342..9333e6177 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -784,7 +784,7 @@ def outfcn(t, x, u, params): def __str__(self): import textwrap - out = super().__str__() + out = InputOutputSystem.__str__(self) out += f"\n\nSubsystems ({len(self.syslist)}):\n" for sys in self.syslist: @@ -845,7 +845,7 @@ def cxn_string(signal, gain, first): cxn, width=78, initial_indent=" * ", subsequent_indent=" ")) + "\n" - return out[:-1] + return out def _update_params(self, params, warning=False): for sys in self.syslist: @@ -1087,7 +1087,7 @@ def unused_signals(self): def connection_table(self, show_names=False, column_width=32): """Print table of connections inside an interconnected system model. - Intended primarily for :class:`InterconnectedSystems` that have been + Intended primarily for :class:`InterconnectedSystem`'s that have been connected implicitly using signal names. Parameters diff --git a/control/statesp.py b/control/statesp.py index 25c22cf8c..68946e90e 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1483,9 +1483,38 @@ def __init__(self, io_sys, ss_sys=None, connection_type=None): outputs=io_sys.output_labels, states=io_sys.state_labels, params=io_sys.params, remove_useless_states=False) - # Use StateSpace.__call__ to evaluate at a given complex value - def __call__(self, *args, **kwargs): - return StateSpace.__call__(self, *args, **kwargs) + # Use StateSpace.__call__ to evaluate at a given complex value + def __call__(self, *args, **kwargs): + return StateSpace.__call__(self, *args, **kwargs) + + def __str__(self): + string = InterconnectedSystem.__str__(self) + "\n" + string += "\n\n".join([ + "{} = {}".format(Mvar, + "\n ".join(str(M).splitlines())) + for Mvar, M in zip(["A", "B", "C", "D"], + [self.A, self.B, self.C, self.D])]) + return string + + # Use InputOutputSystem repr for 'eval' since we can't recreate structure + # (without this, StateSpace._repr_eval_ gets used...) + def _repr_eval_(self): + return InputOutputSystem._repr_eval_(self) + + def _repr_html_(self): + syssize = self.nstates + max(self.noutputs, self.ninputs) + if syssize > config.defaults['statesp.latex_maxsize']: + return None + elif config.defaults['statesp.latex_repr_type'] == 'partitioned': + return InterconnectedSystem._repr_info_(self, html=True) + \ + "\n" + StateSpace._latex_partitioned(self) + elif config.defaults['statesp.latex_repr_type'] == 'separate': + return InterconnectedSystem._repr_info_(self, html=True) + \ + "\n" + StateSpace._latex_separate(self) + else: + raise ValueError( + "Unknown statesp.latex_repr_type '{cfg}'".format( + cfg=config.defaults['statesp.latex_repr_type'])) # The following text needs to be replicated from StateSpace in order for # this entry to show up properly in sphinx doccumentation (not sure why, diff --git a/control/tests/config_test.py b/control/tests/config_test.py index bfcdf2318..600f689da 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -320,15 +320,11 @@ def test_system_indexing(self): sys2 = sys[1:, 1:] assert sys2.name == 'PRE' + sys.name + 'POST' - def test_legacy_repr_format(self): + def test_repr_format(self): from ..statesp import StateSpace from numpy import array sys = ct.ss([[1]], [[1]], [[1]], [[0]]) - with pytest.raises(SyntaxError, match="invalid syntax"): - new = eval(repr(sys)) # iosys is default - - ct.use_legacy_defaults('0.10.1') # loadable is default new = eval(repr(sys)) for attr in ['A', 'B', 'C', 'D']: assert getattr(sys, attr) == getattr(sys, attr) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index ce87e63bc..b288089c9 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2357,12 +2357,18 @@ def test_iosys_repr(fcn, spec, expected, missing, format): if missing is not None: assert re.search(missing, out) is None - # Make sure we can change the default format back to 'info' - sys.repr_format = None + elif format == 'info': + assert out == info_expected + + # Make sure we can change back to the default format + sys.repr_format = None - # Test 'info', either set explicitly or via config.defaults + # Make sure the default format is OK out = repr(sys) - assert out == info_expected + if ct.config.defaults['iosys.repr_format'] == 'info': + assert out == info_expected + elif ct.config.defaults['iosys.repr_format'] == 'eval': + assert re.search(expected, out) != None @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd, ct.nlsys, fs.flatsys]) diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 79b332620..2fc55f7d4 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -34,7 +34,7 @@ def test_named_ss(): assert sys.input_labels == ['u[0]', 'u[1]'] assert sys.output_labels == ['y[0]', 'y[1]'] assert sys.state_labels == ['x[0]', 'x[1]'] - assert ct.InputOutputSystem.__repr__(sys) == \ + assert ct.iosys_repr(sys, format='info') == \ " ['y[0]', 'y[1]']>" # Pass the names as arguments @@ -46,7 +46,7 @@ def test_named_ss(): assert sys.input_labels == ['u1', 'u2'] assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2'] - assert ct.InputOutputSystem.__repr__(sys) == \ + assert ct.iosys_repr(sys, format='info') == \ " ['y1', 'y2']>" # Do the same with rss @@ -56,7 +56,7 @@ def test_named_ss(): assert sys.input_labels == ['u1'] assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2', 'x3'] - assert ct.InputOutputSystem.__repr__(sys) == \ + assert ct.iosys_repr(sys, format='info') == \ " ['y1', 'y2']>" diff --git a/control/tests/nlsys_test.py b/control/tests/nlsys_test.py index 0410e0a92..75f4b0f6d 100644 --- a/control/tests/nlsys_test.py +++ b/control/tests/nlsys_test.py @@ -227,11 +227,6 @@ def test_ICsystem_str(): r"Outputs \(2\): \['y1', 'y2'\]" + "\n" + \ r"States \(4\): \['sys1_x\[0\].*'sys2_x\[1\]'\]" + "\n" + \ "\n" + \ - r"A = \[\[.*\]\]" + "\n\n" + \ - r"B = \[\[.*\]\]" + "\n\n" + \ - r"C = \[\[.*\]\]" + "\n\n" + \ - r"D = \[\[.*\]\]" + "\n" + \ - "\n" + \ r"Subsystems \(2\):" + "\n" + \ r" \* \['y\[0\]', 'y\[1\]']>" + "\n" + \ r" \* \[.*\]>" + "\n" + \ @@ -245,6 +240,11 @@ def test_ICsystem_str(): "\n\n" + \ r"Outputs:" + "\n" + \ r" \* y1 <- sys2.y\[0\]" + "\n" + \ - r" \* y2 <- sys2.y\[1\]" + r" \* y2 <- sys2.y\[1\]" + \ + "\n\n" + \ + r"A = \[\[.*\]\]" + "\n\n" + \ + r"B = \[\[.*\]\]" + "\n\n" + \ + r"C = \[\[.*\]\]" + "\n\n" + \ + r"D = \[\[.*\]\]" assert re.match(ref, str(sys), re.DOTALL) diff --git a/examples/repr_gallery.ipynb b/examples/repr_gallery.ipynb index e12c16574..e6bbcd772 100644 --- a/examples/repr_gallery.ipynb +++ b/examples/repr_gallery.ipynb @@ -58,58 +58,120 @@ "name": "stdout", "output_type": "stream", "text": [ - "==============================================================================\n", - " Default repr\n", - "==============================================================================\n", + "============================================================================\n", + " Default repr\n", + "============================================================================\n", "\n", "StateSpace: sys_ss, dt=0:\n", "-------------------------\n", - " ['y[0]']>\n", + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss')\n", "----\n", "\n", "StateSpace: sys_dss, dt=0.1:\n", "----------------------------\n", - " ['y[0]'], dt=0.1>\n", + "StateSpace(\n", + "array([[ 0.98300988, 0.07817246],\n", + " [-0.31268983, 0.59214759]]),\n", + "array([[0.00424753],\n", + " [0.07817246]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "dt=0.1,\n", + "name='sys_dss')\n", "----\n", "\n", "StateSpace: stateless, dt=None:\n", "-------------------------------\n", - " ['y[0]', 'y[1]'], dt=None>\n", + "StateSpace(\n", + "array([], shape=(0, 0), dtype=float64),\n", + "array([], shape=(0, 2), dtype=float64),\n", + "array([], shape=(2, 0), dtype=float64),\n", + "array([[1., 0.],\n", + " [0., 1.]]),\n", + "dt=None,\n", + "name='stateless')\n", "----\n", "\n", "TransferFunction: sys_ss$converted, dt=0:\n", "-----------------------------------------\n", - " ['y[0]']>\n", + "TransferFunction(\n", + "array([ 1., -1.]),\n", + "array([1., 5., 4.]),\n", + "name='sys_ss$converted')\n", "----\n", "\n", "TransferFunction: sys_dss_poly, dt=0.1:\n", "---------------------------------------\n", - " ['y[0]'], dt=0.1>\n", + "TransferFunction(\n", + "array([ 0.07392493, -0.08176823]),\n", + "array([ 1. , -1.57515746, 0.60653066]),\n", + "dt=0.1,\n", + "name='sys_dss_poly')\n", "----\n", "\n", "TransferFunction: sys[3], dt=0:\n", "-------------------------------\n", - " ['y[0]']>\n", + "TransferFunction(\n", + "array([1]),\n", + "array([1, 0]))\n", "----\n", "\n", "TransferFunction: sys_mtf_zpk, dt=0:\n", "------------------------------------\n", - " ['y[0]', 'y[1]']>\n", + "TransferFunction(\n", + "[[array([ 1., -1.]), array([0.])],\n", + " [array([1, 0]), array([1, 0])]],\n", + "[[array([1., 5., 4.]), array([1.])],\n", + " [array([1]), array([1, 2, 1])]],\n", + "name='sys_mtf_zpk')\n", "----\n", "\n", "FrequencyResponseData: sys_ss$converted$sampled, dt=0:\n", "------------------------------------------------------\n", - " ['y[0]']>\n", + "FrequencyResponseData(\n", + "array([[[-0.24365959+0.05559644j, -0.19198193+0.1589174j ,\n", + " 0.05882353+0.23529412j, 0.1958042 -0.01105691j,\n", + " 0.0508706 -0.07767156j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", + "name='sys_ss$converted$sampled')\n", "----\n", "\n", "FrequencyResponseData: sys_dss_poly$sampled, dt=0.1:\n", "----------------------------------------------------\n", - " ['y[0]'], dt=0.1>\n", + "FrequencyResponseData(\n", + "array([[[-0.24337799+0.05673083j, -0.18944184+0.16166381j,\n", + " 0.07043649+0.23113479j, 0.19038528-0.04416494j,\n", + " 0.00286505-0.09595906j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ])\n", + "dt=0.1,\n", + "name='sys_dss_poly$sampled')\n", "----\n", "\n", "FrequencyResponseData: sys_mtf_zpk$sampled, dt=0:\n", "-------------------------------------------------\n", - " ['y[0]', 'y[1]']>\n", + "FrequencyResponseData(\n", + "array([[[-0.24365959 +0.05559644j, -0.19198193 +0.1589174j ,\n", + " 0.05882353 +0.23529412j, 0.1958042 -0.01105691j,\n", + " 0.0508706 -0.07767156j],\n", + " [ 0. +0.j , 0. +0.j ,\n", + " 0. +0.j , 0. +0.j ,\n", + " 0. +0.j ]],\n", + "\n", + " [[ 0. +0.1j , 0. +0.31622777j,\n", + " 0. +1.j , 0. +3.16227766j,\n", + " 0. +10.j ],\n", + " [ 0.01960592 +0.09704931j, 0.16528926 +0.23521074j,\n", + " 0.5 +0.j , 0.16528926 -0.23521074j,\n", + " 0.01960592 -0.09704931j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", + "name='sys_mtf_zpk$sampled')\n", "----\n", "\n", "NonlinearIOSystem: sys_nl, dt=0:\n", @@ -142,9 +204,9 @@ " ['y[0]']>\n", "----\n", "\n", - "==============================================================================\n", - " Default str (print)\n", - "==============================================================================\n", + "============================================================================\n", + " Default str (print)\n", + "============================================================================\n", "\n", "StateSpace: sys_ss, dt=0:\n", "-------------------------\n", @@ -170,7 +232,7 @@ "Inputs (1): ['u[0]']\n", "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", - "dt=0.1\n", + "dt = 0.1\n", "\n", "A = [[ 0.98300988 0.07817246]\n", " [-0.31268983 0.59214759]]\n", @@ -189,7 +251,7 @@ "Inputs (2): ['u[0]', 'u[1]']\n", "Outputs (2): ['y[0]', 'y[1]']\n", "States (0): []\n", - "dt=None\n", + "dt = None\n", "\n", "A = []\n", "\n", @@ -217,7 +279,7 @@ ": sys_dss_poly\n", "Inputs (1): ['u[0]']\n", "Outputs (1): ['y[0]']\n", - "dt=0.1\n", + "dt = 0.1\n", "\n", " 0.07392 z - 0.08177\n", " ----------------------\n", @@ -286,7 +348,7 @@ ": sys_dss_poly$sampled\n", "Inputs (1): ['u[0]']\n", "Outputs (1): ['y[0]']\n", - "dt=0.1\n", + "dt = 0.1\n", "\n", "Freq [rad/s] Response\n", "------------ ---------------------\n", @@ -351,8 +413,8 @@ "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "----\n", "\n", "NonlinearIOSystem: sys_dnl, dt=0.1:\n", @@ -361,10 +423,10 @@ "Inputs (1): ['u[0]']\n", "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", - "dt=0.1\n", + "dt = 0.1\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "----\n", "\n", "InterconnectedSystem: sys_ic, dt=0:\n", @@ -374,9 +436,6 @@ "Outputs (2): ['y[0]', 'y[1]']\n", "States (2): ['proc_nl_x[0]', 'proc_nl_x[1]']\n", "\n", - "Update: .updfcn at 0x12c6562a0>\n", - "Output: .outfcn at 0x12c656340>\n", - "\n", "Subsystems (2):\n", " * ['y[0]', 'y[1]']>\n", " * ['y[0]', 'y[1]']>\n", @@ -390,6 +449,7 @@ "Outputs:\n", " * y[0] <- proc_nl.y[0]\n", " * y[1] <- proc_nl.y[1]\n", + "\n", "----\n", "\n", "LinearICSystem: sys_ic, dt=0:\n", @@ -399,18 +459,6 @@ "Outputs (2): ['y[0]', 'y[1]']\n", "States (2): ['proc_x[0]', 'proc_x[1]']\n", "\n", - "A = [[-2. 3.]\n", - " [-1. -5.]]\n", - "\n", - "B = [[-2. 0.]\n", - " [ 0. -3.]]\n", - "\n", - "C = [[-1. 1.]\n", - " [ 1. 0.]]\n", - "\n", - "D = [[0. 0.]\n", - " [0. 0.]]\n", - "\n", "Subsystems (2):\n", " * ['y[0]', 'y[1]']>\n", " * ['y[0]', 'y[1]'], dt=None>\n", @@ -424,6 +472,18 @@ "Outputs:\n", " * y[0] <- proc.y[0]\n", " * y[1] <- proc.y[1]\n", + "\n", + "A = [[-2. 3.]\n", + " [-1. -5.]]\n", + "\n", + "B = [[-2. 0.]\n", + " [ 0. -3.]]\n", + "\n", + "C = [[-1. 1.]\n", + " [ 1. 0.]]\n", + "\n", + "D = [[0. 0.]\n", + " [0. 0.]]\n", "----\n", "\n", "FlatSystem: sys_fs, dt=0:\n", @@ -436,8 +496,8 @@ "Update: ['y[0]']>>\n", "Output: ['y[0]']>>\n", "\n", - "Forward: \n", - "Reverse: \n", + "Forward: \n", + "Reverse: \n", "----\n", "\n", "FlatSystem: sys_fsnl, dt=0:\n", @@ -447,127 +507,65 @@ "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "\n", - "Forward: \n", - "Reverse: \n", + "Forward: \n", + "Reverse: \n", "----\n", "\n", - "==============================================================================\n", - " repr_format='eval'\n", - "==============================================================================\n", + "============================================================================\n", + " repr_format='info'\n", + "============================================================================\n", "\n", "StateSpace: sys_ss, dt=0:\n", "-------------------------\n", - "StateSpace(\n", - "array([[ 0., 1.],\n", - " [-4., -5.]]),\n", - "array([[0.],\n", - " [1.]]),\n", - "array([[-1., 1.]]),\n", - "array([[0.]]),\n", - "name='sys_ss')\n", + " ['y[0]']>\n", "----\n", "\n", "StateSpace: sys_dss, dt=0.1:\n", "----------------------------\n", - "StateSpace(\n", - "array([[ 0.98300988, 0.07817246],\n", - " [-0.31268983, 0.59214759]]),\n", - "array([[0.00424753],\n", - " [0.07817246]]),\n", - "array([[-1., 1.]]),\n", - "array([[0.]]),\n", - "dt=0.1,\n", - "name='sys_dss')\n", + " ['y[0]'], dt=0.1>\n", "----\n", "\n", "StateSpace: stateless, dt=None:\n", "-------------------------------\n", - "StateSpace(\n", - "array([], shape=(0, 0), dtype=float64),\n", - "array([], shape=(0, 2), dtype=float64),\n", - "array([], shape=(2, 0), dtype=float64),\n", - "array([[1., 0.],\n", - " [0., 1.]]),\n", - "dt=None,\n", - "name='stateless')\n", + " ['y[0]', 'y[1]'], dt=None>\n", "----\n", "\n", "TransferFunction: sys_ss$converted, dt=0:\n", "-----------------------------------------\n", - "TransferFunction(\n", - "array([ 1., -1.]),\n", - "array([1., 5., 4.]),\n", - "name='sys_ss$converted')\n", + " ['y[0]']>\n", "----\n", "\n", "TransferFunction: sys_dss_poly, dt=0.1:\n", "---------------------------------------\n", - "TransferFunction(\n", - "array([ 0.07392493, -0.08176823]),\n", - "array([ 1. , -1.57515746, 0.60653066]),\n", - "dt=0.1,\n", - "name='sys_dss_poly')\n", + " ['y[0]'], dt=0.1>\n", "----\n", "\n", "TransferFunction: sys[3], dt=0:\n", "-------------------------------\n", - "TransferFunction(\n", - "array([1]),\n", - "array([1, 0]))\n", + " ['y[0]']>\n", "----\n", "\n", "TransferFunction: sys_mtf_zpk, dt=0:\n", "------------------------------------\n", - "TransferFunction(\n", - "[[array([ 1., -1.]), array([0.])],\n", - " [array([1, 0]), array([1, 0])]],\n", - "[[array([1., 5., 4.]), array([1.])],\n", - " [array([1]), array([1, 2, 1])]],\n", - "name='sys_mtf_zpk')\n", + " ['y[0]', 'y[1]']>\n", "----\n", "\n", "FrequencyResponseData: sys_ss$converted$sampled, dt=0:\n", "------------------------------------------------------\n", - "FrequencyResponseData(\n", - "array([[[-0.24365959+0.05559644j, -0.19198193+0.1589174j ,\n", - " 0.05882353+0.23529412j, 0.1958042 -0.01105691j,\n", - " 0.0508706 -0.07767156j]]]),\n", - "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", - "name='sys_ss$converted$sampled', outputs=1, inputs=1)\n", + " ['y[0]']>\n", "----\n", "\n", "FrequencyResponseData: sys_dss_poly$sampled, dt=0.1:\n", "----------------------------------------------------\n", - "FrequencyResponseData(\n", - "array([[[-0.24337799+0.05673083j, -0.18944184+0.16166381j,\n", - " 0.07043649+0.23113479j, 0.19038528-0.04416494j,\n", - " 0.00286505-0.09595906j]]]),\n", - "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ])\n", - "dt=0.1,\n", - "name='sys_dss_poly$sampled', outputs=1, inputs=1)\n", + " ['y[0]'], dt=0.1>\n", "----\n", "\n", "FrequencyResponseData: sys_mtf_zpk$sampled, dt=0:\n", "-------------------------------------------------\n", - "FrequencyResponseData(\n", - "array([[[-0.24365959 +0.05559644j, -0.19198193 +0.1589174j ,\n", - " 0.05882353 +0.23529412j, 0.1958042 -0.01105691j,\n", - " 0.0508706 -0.07767156j],\n", - " [ 0. +0.j , 0. +0.j ,\n", - " 0. +0.j , 0. +0.j ,\n", - " 0. +0.j ]],\n", - "\n", - " [[ 0. +0.1j , 0. +0.31622777j,\n", - " 0. +1.j , 0. +3.16227766j,\n", - " 0. +10.j ],\n", - " [ 0.01960592 +0.09704931j, 0.16528926 +0.23521074j,\n", - " 0.5 +0.j , 0.16528926 -0.23521074j,\n", - " 0.01960592 -0.09704931j]]]),\n", - "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", - "name='sys_mtf_zpk$sampled', outputs=2, inputs=2)\n", + " ['y[0]', 'y[1]']>\n", "----\n", "\n", "NonlinearIOSystem: sys_nl, dt=0:\n", @@ -587,16 +585,7 @@ "\n", "LinearICSystem: sys_ic, dt=0:\n", "-----------------------------\n", - "StateSpace(\n", - "array([[-2., 3.],\n", - " [-1., -5.]]),\n", - "array([[-2., 0.],\n", - " [ 0., -3.]]),\n", - "array([[-1., 1.],\n", - " [ 1., 0.]]),\n", - "array([[0., 0.],\n", - " [0., 0.]]),\n", - "name='sys_ic', states=['proc_x[0]', 'proc_x[1]'], inputs=['r[0]', 'r[1]'])\n", + " ['y[0]', 'y[1]']>\n", "----\n", "\n", "FlatSystem: sys_fs, dt=0:\n", @@ -609,9 +598,9 @@ " ['y[0]']>\n", "----\n", "\n", - "==============================================================================\n", - " xferfcn.display_format=zpk, str (print)\n", - "==============================================================================\n", + "============================================================================\n", + " xferfcn.display_format=zpk, str (print)\n", + "============================================================================\n", "\n", "TransferFunction: sys_ss$converted, dt=0:\n", "-----------------------------------------\n", @@ -629,7 +618,7 @@ ": sys_dss_poly\n", "Inputs (1): ['u[0]']\n", "Outputs (1): ['y[0]']\n", - "dt=0.1\n", + "dt = 0.1\n", "\n", " 0.07392 z - 0.08177\n", " ----------------------\n", @@ -713,7 +702,8 @@ { "data": { "text/html": [ - "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>$$\n", + "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>\n", + "$$\n", "\\left[\\begin{array}{rllrll|rll}\n", "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", "-4\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -723,7 +713,14 @@ "$$" ], "text/plain": [ - " ['y[0]']>" + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss')" ] }, "execution_count": 3, @@ -744,7 +741,8 @@ { "data": { "text/html": [ - "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>$$\n", + "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>\n", + "$$\n", "\\begin{array}{ll}\n", "A = \\left[\\begin{array}{rllrll}\n", "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -767,7 +765,14 @@ "$$" ], "text/plain": [ - " ['y[0]']>" + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss')" ] }, "metadata": {}, @@ -788,7 +793,8 @@ { "data": { "text/html": [ - "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>$$\n", + "<StateSpace sys_ss: ['u[0]'] -> ['y[0]']>\n", + "$$\n", "\\begin{array}{ll}\n", "A = \\left[\\begin{array}{rllrll}\n", " 0.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}& 1.&\\hspace{-1em}0000&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -811,7 +817,14 @@ "$$" ], "text/plain": [ - " ['y[0]']>" + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss')" ] }, "metadata": {}, @@ -842,7 +855,8 @@ { "data": { "text/html": [ - "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>$$\n", + "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>\n", + "$$\n", "\\left[\\begin{array}{rllrll}\n", "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -850,7 +864,14 @@ "$$" ], "text/plain": [ - " ['y[0]', 'y[1]'], dt=None>" + "StateSpace(\n", + "array([], shape=(0, 0), dtype=float64),\n", + "array([], shape=(0, 2), dtype=float64),\n", + "array([], shape=(2, 0), dtype=float64),\n", + "array([[1., 0.],\n", + " [0., 1.]]),\n", + "dt=None,\n", + "name='stateless')" ] }, "execution_count": 6, @@ -871,7 +892,8 @@ { "data": { "text/html": [ - "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>$$\n", + "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>\n", + "$$\n", "\\begin{array}{ll}\n", "D = \\left[\\begin{array}{rllrll}\n", "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -881,7 +903,14 @@ "$$" ], "text/plain": [ - " ['y[0]', 'y[1]'], dt=None>" + "StateSpace(\n", + "array([], shape=(0, 0), dtype=float64),\n", + "array([], shape=(0, 2), dtype=float64),\n", + "array([], shape=(2, 0), dtype=float64),\n", + "array([[1., 0.],\n", + " [0., 1.]]),\n", + "dt=None,\n", + "name='stateless')" ] }, "metadata": {}, @@ -910,7 +939,8 @@ { "data": { "text/html": [ - "<StateSpace sys_dss: ['u[0]'] -> ['y[0]'], dt=0.1>$$\n", + "<StateSpace sys_dss: ['u[0]'] -> ['y[0]'], dt=0.1>\n", + "$$\n", "\\left[\\begin{array}{rllrll|rll}\n", "0.&\\hspace{-1em}983&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}0782&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}00425&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", "-0.&\\hspace{-1em}313&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}592&\\hspace{-1em}\\phantom{\\cdot}&0.&\\hspace{-1em}0782&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -920,7 +950,15 @@ "$$" ], "text/plain": [ - " ['y[0]'], dt=0.1>" + "StateSpace(\n", + "array([[ 0.98300988, 0.07817246],\n", + " [-0.31268983, 0.59214759]]),\n", + "array([[0.00424753],\n", + " [0.07817246]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "dt=0.1,\n", + "name='sys_dss')" ] }, "execution_count": 8, @@ -949,7 +987,8 @@ { "data": { "text/html": [ - "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>$$\n", + "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>\n", + "$$\n", "\\left[\\begin{array}{rllrll}\n", "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -957,7 +996,14 @@ "$$" ], "text/plain": [ - " ['y[0]', 'y[1]'], dt=None>" + "StateSpace(\n", + "array([], shape=(0, 0), dtype=float64),\n", + "array([], shape=(0, 2), dtype=float64),\n", + "array([], shape=(2, 0), dtype=float64),\n", + "array([[1., 0.],\n", + " [0., 1.]]),\n", + "dt=None,\n", + "name='stateless')" ] }, "execution_count": 9, @@ -986,10 +1032,14 @@ { "data": { "text/html": [ - "<TransferFunction sys_ss\\$converted: ['u[0]'] -> ['y[0]']>$$\\dfrac{s - 1}{s^2 + 5 s + 4}$$" + "<TransferFunction sys_ss\\$converted: ['u[0]'] -> ['y[0]']>\n", + "$$\\dfrac{s - 1}{s^2 + 5 s + 4}$$" ], "text/plain": [ - " ['y[0]']>" + "TransferFunction(\n", + "array([ 1., -1.]),\n", + "array([1., 5., 4.]),\n", + "name='sys_ss$converted')" ] }, "execution_count": 10, @@ -1010,10 +1060,14 @@ { "data": { "text/html": [ - "<TransferFunction sys_ss\\$converted: ['u[0]'] -> ['y[0]']>$$\\dfrac{s - 1}{(s + 1) (s + 4)}$$" + "<TransferFunction sys_ss\\$converted: ['u[0]'] -> ['y[0]']>\n", + "$$\\dfrac{s - 1}{(s + 1) (s + 4)}$$" ], "text/plain": [ - " ['y[0]']>" + "TransferFunction(\n", + "array([ 1., -1.]),\n", + "array([1., 5., 4.]),\n", + "name='sys_ss$converted')" ] }, "metadata": {}, @@ -1052,10 +1106,16 @@ { "data": { "text/html": [ - "<TransferFunction sys_mtf_zpk: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']>$$\\begin{bmatrix}\\dfrac{s - 1}{(s + 1) (s + 4)}&\\dfrac{0}{1}\\\\\\dfrac{s}{1}&\\dfrac{s}{(s + 1) (s + 1)}\\\\ \\end{bmatrix}$$" + "<TransferFunction sys_mtf_zpk: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]']>\n", + "$$\\begin{bmatrix}\\dfrac{s - 1}{(s + 1) (s + 4)}&\\dfrac{0}{1}\\\\\\dfrac{s}{1}&\\dfrac{s}{(s + 1) (s + 1)}\\\\ \\end{bmatrix}$$" ], "text/plain": [ - " ['y[0]', 'y[1]']>" + "TransferFunction(\n", + "[[array([ 1., -1.]), array([0.])],\n", + " [array([1, 0]), array([1, 0])]],\n", + "[[array([1., 5., 4.]), array([1.])],\n", + " [array([1]), array([1, 2, 1])]],\n", + "name='sys_mtf_zpk')" ] }, "execution_count": 12, @@ -1084,10 +1144,15 @@ { "data": { "text/html": [ - "<TransferFunction sys_dss_poly: ['u[0]'] -> ['y[0]'], dt=0.1>$$\\dfrac{0.07392 z - 0.08177}{z^2 - 1.575 z + 0.6065}$$" + "<TransferFunction sys_dss_poly: ['u[0]'] -> ['y[0]'], dt=0.1>\n", + "$$\\dfrac{0.07392 z - 0.08177}{z^2 - 1.575 z + 0.6065}$$" ], "text/plain": [ - " ['y[0]'], dt=0.1>" + "TransferFunction(\n", + "array([ 0.07392493, -0.08176823]),\n", + "array([ 1. , -1.57515746, 0.60653066]),\n", + "dt=0.1,\n", + "name='sys_dss_poly')" ] }, "execution_count": 13, @@ -1116,7 +1181,8 @@ { "data": { "text/html": [ - "<LinearICSystem sys_ic: ['r[0]', 'r[1]'] -> ['y[0]', 'y[1]']>$$\n", + "<LinearICSystem sys_ic: ['r[0]', 'r[1]'] -> ['y[0]', 'y[1]']>\n", + "$$\n", "\\left[\\begin{array}{rllrll|rllrll}\n", "-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&3\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", "-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&-3\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -1156,7 +1222,12 @@ { "data": { "text/plain": [ - " ['y[0]']>" + "FrequencyResponseData(\n", + "array([[[-0.24365959+0.05559644j, -0.19198193+0.1589174j ,\n", + " 0.05882353+0.23529412j, 0.1958042 -0.01105691j,\n", + " 0.0508706 -0.07767156j]]]),\n", + "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", + "name='sys_ss$converted$sampled')" ] }, "metadata": {}, diff --git a/examples/repr_gallery.py b/examples/repr_gallery.py index fc301c447..b346d0761 100644 --- a/examples/repr_gallery.py +++ b/examples/repr_gallery.py @@ -111,9 +111,9 @@ def fs_reverse(zflag): # Utility function to display outputs def display_representations( description, fcn, class_list=(ct.InputOutputSystem, )): - print("=" * 78) - print(" " * round((78 - len(description)) / 2) + f"{description}") - print("=" * 78 + "\n") + print("=" * 76) + print(" " * round((76 - len(description)) / 2) + f"{description}") + print("=" * 76 + "\n") for sys in syslist: if isinstance(sys, tuple(class_list)): print(str := f"{type(sys).__name__}: {sys.name}, dt={sys.dt}:") From 5ae33b0ff45b6260d5cee33807775e6634b78f11 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 3 Jan 2025 16:48:06 -0800 Subject: [PATCH 18/21] add iosys.repr_show_count and defualt to True --- control/frdata.py | 2 +- control/iosys.py | 5 +- control/statesp.py | 2 +- control/tests/frd_test.py | 5 +- control/tests/iosys_test.py | 31 ++++----- control/tests/statesp_test.py | 2 +- control/tests/xferfcn_test.py | 12 ++-- control/xferfcn.py | 2 +- examples/repr_gallery.ipynb | 125 +++++++++++++++++++++++----------- examples/repr_gallery.py | 33 +++++---- 10 files changed, 138 insertions(+), 81 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 625444195..cb6925661 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -425,7 +425,7 @@ def _repr_eval_(self): smooth=(self._ifunc and ", smooth=True") or "") out += self._dt_repr() - if len(labels := self._label_repr(show_count=False)) > 0: + if len(labels := self._label_repr()) > 0: out += ",\n" + labels out += ")" diff --git a/control/iosys.py b/control/iosys.py index 51bac2eb0..5c4c88776 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -32,6 +32,7 @@ 'iosys.converted_system_name_prefix': '', 'iosys.converted_system_name_suffix': '$converted', 'iosys.repr_format': 'eval', + 'iosys.repr_show_count': True, } @@ -313,7 +314,9 @@ def repr_format(self): def repr_format(self, value): self._repr_format = value - def _label_repr(self, show_count=True): + def _label_repr(self, show_count=None): + show_count = config._get_param( + 'iosys', 'repr_show_count', show_count, True) out, count = "", 0 # Include the system name if not generic diff --git a/control/statesp.py b/control/statesp.py index 68946e90e..98adc942f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -395,7 +395,7 @@ def _repr_eval_(self): C=self.C.__repr__(), D=self.D.__repr__()) out += super()._dt_repr(separator=",\n", space="") - if len(labels := super()._label_repr(show_count=False)) > 0: + if len(labels := super()._label_repr()) > 0: out += ",\n" + labels out += ")" diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 3741b4e88..c63d9e217 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -468,8 +468,9 @@ def test_repr_str(self): ref_common = "FrequencyResponseData(\n" \ "array([[[1. +0.j , 0.9 +0.1j, 0.1 +2.j , 0.05+3.j ]]]),\n" \ "array([ 0.1, 1. , 10. , 100. ])," - ref0 = ref_common + "\nname='sys0')" - ref1 = ref_common + " smooth=True," + "\nname='sys1')" + ref0 = ref_common + "\nname='sys0', outputs=1, inputs=1)" + ref1 = ref_common + " smooth=True," + \ + "\nname='sys1', outputs=1, inputs=1)" sysm = ct.frd( np.matmul(array([[1], [2]]), sys0.fresp), sys0.omega, name='sysm') diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index b288089c9..1f9327785 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2290,32 +2290,31 @@ def test_signal_indexing(): @slycotonly @pytest.mark.parametrize("fcn, spec, expected, missing", [ - (ct.ss, {}, "", r"states|inputs|outputs|dt|name"), - (ct.tf, {}, "", r"states|inputs|outputs|dt|name"), - (ct.frd, {}, "", r"states|inputs|outputs|dt|name"), - (ct.ss, {'dt': 0.1}, ".*\ndt=0.1", r"states|inputs|outputs|name"), - (ct.tf, {'dt': 0.1}, ".*\ndt=0.1", r"states|inputs|outputs|name"), - (ct.frd, {'dt': 0.1}, - ".*\ndt=0.1", r"states|inputs|outputs|name"), - (ct.ss, {'dt': True}, "\ndt=True", r"states|inputs|outputs|name"), - (ct.ss, {'dt': None}, "\ndt=None", r"states|inputs|outputs|name"), - (ct.ss, {'dt': 0}, "", r"states|inputs|outputs|dt|name"), + (ct.ss, {}, "states=4, outputs=3, inputs=2", r"dt|name"), + (ct.tf, {}, "outputs=3, inputs=2", r"dt|states|name"), + (ct.frd, {}, "outputs=3, inputs=2", r"dt|states|name"), + (ct.ss, {'dt': 0.1}, ".*\ndt=0.1,\nstates=4, outputs=3, inputs=2", r"name"), + (ct.tf, {'dt': 0.1}, ".*\ndt=0.1,\noutputs=3, inputs=2", r"states|name"), + (ct.frd, {'dt': 0.1}, ".*\ndt=0.1,\noutputs=3, inputs=2", r"states|name"), + (ct.ss, {'dt': True}, "\ndt=True,\nstates=4, outputs=3, inputs=2", r"name"), + (ct.ss, {'dt': None}, "\ndt=None,\nstates=4, outputs=3, inputs=2", r"name"), + (ct.ss, {'dt': 0}, "states=4, outputs=3, inputs=2", r"dt|name"), (ct.ss, {'name': 'mysys'}, "\nname='mysys'", r"dt"), (ct.tf, {'name': 'mysys'}, "\nname='mysys'", r"dt|states"), (ct.frd, {'name': 'mysys'}, "\nname='mysys'", r"dt|states"), (ct.ss, {'inputs': ['u1']}, - r"[\n]inputs=\['u1'\]", r"states|outputs|dt|name"), + r"[\n]states=4, outputs=3, inputs=\['u1'\]", r"dt|name"), (ct.tf, {'inputs': ['u1']}, - r"[\n]inputs=\['u1'\]", r"outputs|dt|name"), + r"[\n]outputs=3, inputs=\['u1'\]", r"dt|name"), (ct.frd, {'inputs': ['u1'], 'name': 'sampled'}, - r"[\n]name='sampled', inputs=\['u1'\]", r"outputs|dt"), + r"[\n]name='sampled', outputs=3, inputs=\['u1'\]", r"dt"), (ct.ss, {'outputs': ['y1']}, - r"[\n]outputs=\['y1'\]", r"states|inputs|dt|name"), + r"[\n]states=4, outputs=\['y1'\], inputs=2", r"dt|name"), (ct.ss, {'name': 'mysys', 'inputs': ['u1']}, - r"[\n]name='mysys', inputs=\['u1'\]", r"states|outputs|dt"), + r"[\n]name='mysys', states=4, outputs=3, inputs=\['u1'\]", r"dt"), (ct.ss, {'name': 'mysys', 'states': [ 'long_state_1', 'long_state_2', 'long_state_3']}, - r"[\n]name='.*', states=\[.*\]\)", r"inputs|outputs|dt"), + r"[\n]name='.*', states=\[.*\],\noutputs=3, inputs=2\)", r"dt"), ]) @pytest.mark.parametrize("format", ['info', 'eval']) def test_iosys_repr(fcn, spec, expected, missing, format): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index fff22a5bd..7855cc402 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -746,7 +746,7 @@ def test_repr(self, sys322): [ 1., 4., 3.]]), array([[-2., 4.], [ 0., 1.]]), -name='sys322'{dt})""" +name='sys322'{dt}, states=3, outputs=2, inputs=2)""" assert ct.iosys_repr(sys322, format='eval') == ref322.format(dt='') sysd = StateSpace(sys322.A, sys322.B, sys322.C, sys322.D, 0.4) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 92509466d..c7f92379a 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1087,19 +1087,22 @@ def test_html_repr(self): [(([-1., 4.], [1., 3., 5.]), "TransferFunction(\n" "array([-1., 4.]),\n" - "array([1., 3., 5.]))"), + "array([1., 3., 5.]),\n" + "outputs=1, inputs=1)"), (([2., 3., 0.], [1., -3., 4., 0], 2.0), "TransferFunction(\n" "array([2., 3., 0.]),\n" "array([ 1., -3., 4., 0.]),\n" - "dt=2.0)"), + "dt=2.0,\n" + "outputs=1, inputs=1)"), (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3], [0, 1]]]), "TransferFunction(\n" "[[array([1]), array([2, 3])],\n" " [array([4, 5]), array([6, 7])]],\n" "[[array([6, 7]), array([4, 5])],\n" - " [array([2, 3]), array([1])]])"), + " [array([2, 3]), array([1])]],\n" + "outputs=2, inputs=2)"), (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3], [0, 1]]], 0.5), @@ -1108,7 +1111,8 @@ def test_html_repr(self): " [array([4, 5]), array([6, 7])]],\n" "[[array([6, 7]), array([4, 5])],\n" " [array([2, 3]), array([1])]],\n" - "dt=0.5)"), + "dt=0.5,\n" + "outputs=2, inputs=2)"), ]) def test_loadable_repr(self, Hargs, ref): """Test __repr__ printout.""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 1b3bf8690..dea61f9f3 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -509,7 +509,7 @@ def _repr_eval_(self): out += "],\n[" if entry is self.num_array else "]" out += super()._dt_repr(separator=",\n", space="") - if len(labels := self._label_repr(show_count=False)) > 0: + if len(labels := self._label_repr()) > 0: out += ",\n" + labels out += ")" diff --git a/examples/repr_gallery.ipynb b/examples/repr_gallery.ipynb index e6bbcd772..0981cdf6e 100644 --- a/examples/repr_gallery.ipynb +++ b/examples/repr_gallery.ipynb @@ -50,9 +50,7 @@ "cell_type": "code", "execution_count": 2, "id": "eab8cc0b-3e8a-4df8-acbd-258f006f44bb", - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -71,7 +69,7 @@ " [1.]]),\n", "array([[-1., 1.]]),\n", "array([[0.]]),\n", - "name='sys_ss')\n", + "name='sys_ss', states=2, outputs=1, inputs=1)\n", "----\n", "\n", "StateSpace: sys_dss, dt=0.1:\n", @@ -84,7 +82,7 @@ "array([[-1., 1.]]),\n", "array([[0.]]),\n", "dt=0.1,\n", - "name='sys_dss')\n", + "name='sys_dss', states=2, outputs=1, inputs=1)\n", "----\n", "\n", "StateSpace: stateless, dt=None:\n", @@ -96,7 +94,7 @@ "array([[1., 0.],\n", " [0., 1.]]),\n", "dt=None,\n", - "name='stateless')\n", + "name='stateless', states=0, outputs=2, inputs=['u0', 'u1'])\n", "----\n", "\n", "TransferFunction: sys_ss$converted, dt=0:\n", @@ -104,7 +102,7 @@ "TransferFunction(\n", "array([ 1., -1.]),\n", "array([1., 5., 4.]),\n", - "name='sys_ss$converted')\n", + "name='sys_ss$converted', outputs=1, inputs=1)\n", "----\n", "\n", "TransferFunction: sys_dss_poly, dt=0.1:\n", @@ -113,14 +111,15 @@ "array([ 0.07392493, -0.08176823]),\n", "array([ 1. , -1.57515746, 0.60653066]),\n", "dt=0.1,\n", - "name='sys_dss_poly')\n", + "name='sys_dss_poly', outputs=1, inputs=1)\n", "----\n", "\n", "TransferFunction: sys[3], dt=0:\n", "-------------------------------\n", "TransferFunction(\n", "array([1]),\n", - "array([1, 0]))\n", + "array([1, 0]),\n", + "outputs=1, inputs=1)\n", "----\n", "\n", "TransferFunction: sys_mtf_zpk, dt=0:\n", @@ -130,7 +129,7 @@ " [array([1, 0]), array([1, 0])]],\n", "[[array([1., 5., 4.]), array([1.])],\n", " [array([1]), array([1, 2, 1])]],\n", - "name='sys_mtf_zpk')\n", + "name='sys_mtf_zpk', outputs=2, inputs=2)\n", "----\n", "\n", "FrequencyResponseData: sys_ss$converted$sampled, dt=0:\n", @@ -140,7 +139,7 @@ " 0.05882353+0.23529412j, 0.1958042 -0.01105691j,\n", " 0.0508706 -0.07767156j]]]),\n", "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", - "name='sys_ss$converted$sampled')\n", + "name='sys_ss$converted$sampled', outputs=1, inputs=1)\n", "----\n", "\n", "FrequencyResponseData: sys_dss_poly$sampled, dt=0.1:\n", @@ -151,7 +150,7 @@ " 0.00286505-0.09595906j]]]),\n", "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ])\n", "dt=0.1,\n", - "name='sys_dss_poly$sampled')\n", + "name='sys_dss_poly$sampled', outputs=1, inputs=1)\n", "----\n", "\n", "FrequencyResponseData: sys_mtf_zpk$sampled, dt=0:\n", @@ -171,7 +170,7 @@ " 0.5 +0.j , 0.16528926 -0.23521074j,\n", " 0.01960592 -0.09704931j]]]),\n", "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", - "name='sys_mtf_zpk$sampled')\n", + "name='sys_mtf_zpk$sampled', outputs=2, inputs=2)\n", "----\n", "\n", "NonlinearIOSystem: sys_nl, dt=0:\n", @@ -248,7 +247,7 @@ "StateSpace: stateless, dt=None:\n", "-------------------------------\n", ": stateless\n", - "Inputs (2): ['u[0]', 'u[1]']\n", + "Inputs (2): ['u0', 'u1']\n", "Outputs (2): ['y[0]', 'y[1]']\n", "States (0): []\n", "dt = None\n", @@ -413,8 +412,8 @@ "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "----\n", "\n", "NonlinearIOSystem: sys_dnl, dt=0.1:\n", @@ -425,8 +424,8 @@ "States (2): ['x[0]', 'x[1]']\n", "dt = 0.1\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "----\n", "\n", "InterconnectedSystem: sys_ic, dt=0:\n", @@ -496,8 +495,8 @@ "Update: ['y[0]']>>\n", "Output: ['y[0]']>>\n", "\n", - "Forward: \n", - "Reverse: \n", + "Forward: \n", + "Reverse: \n", "----\n", "\n", "FlatSystem: sys_fsnl, dt=0:\n", @@ -507,11 +506,11 @@ "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "\n", - "Forward: \n", - "Reverse: \n", + "Forward: \n", + "Reverse: \n", "----\n", "\n", "============================================================================\n", @@ -530,7 +529,7 @@ "\n", "StateSpace: stateless, dt=None:\n", "-------------------------------\n", - " ['y[0]', 'y[1]'], dt=None>\n", + " ['y[0]', 'y[1]'], dt=None>\n", "----\n", "\n", "TransferFunction: sys_ss$converted, dt=0:\n", @@ -599,6 +598,52 @@ "----\n", "\n", "============================================================================\n", + " iosys.repr_show_count=False\n", + "============================================================================\n", + "\n", + "StateSpace: sys_ss, dt=0:\n", + "-------------------------\n", + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss')\n", + "----\n", + "\n", + "StateSpace: sys_dss, dt=0.1:\n", + "----------------------------\n", + "StateSpace(\n", + "array([[ 0.98300988, 0.07817246],\n", + " [-0.31268983, 0.59214759]]),\n", + "array([[0.00424753],\n", + " [0.07817246]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "dt=0.1,\n", + "name='sys_dss')\n", + "----\n", + "\n", + "StateSpace: stateless, dt=None:\n", + "-------------------------------\n", + "StateSpace(\n", + "array([], shape=(0, 0), dtype=float64),\n", + "array([], shape=(0, 2), dtype=float64),\n", + "array([], shape=(2, 0), dtype=float64),\n", + "array([[1., 0.],\n", + " [0., 1.]]),\n", + "dt=None,\n", + "name='stateless', inputs=['u0', 'u1'])\n", + "----\n", + "\n", + "LinearICSystem: sys_ic, dt=0:\n", + "-----------------------------\n", + " ['y[0]', 'y[1]']>\n", + "----\n", + "\n", + "============================================================================\n", " xferfcn.display_format=zpk, str (print)\n", "============================================================================\n", "\n", @@ -720,7 +765,7 @@ " [1.]]),\n", "array([[-1., 1.]]),\n", "array([[0.]]),\n", - "name='sys_ss')" + "name='sys_ss', states=2, outputs=1, inputs=1)" ] }, "execution_count": 3, @@ -772,7 +817,7 @@ " [1.]]),\n", "array([[-1., 1.]]),\n", "array([[0.]]),\n", - "name='sys_ss')" + "name='sys_ss', states=2, outputs=1, inputs=1)" ] }, "metadata": {}, @@ -824,7 +869,7 @@ " [1.]]),\n", "array([[-1., 1.]]),\n", "array([[0.]]),\n", - "name='sys_ss')" + "name='sys_ss', states=2, outputs=1, inputs=1)" ] }, "metadata": {}, @@ -855,7 +900,7 @@ { "data": { "text/html": [ - "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>\n", + "<StateSpace stateless: ['u0', 'u1'] -> ['y[0]', 'y[1]'], dt=None>\n", "$$\n", "\\left[\\begin{array}{rllrll}\n", "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -871,7 +916,7 @@ "array([[1., 0.],\n", " [0., 1.]]),\n", "dt=None,\n", - "name='stateless')" + "name='stateless', states=0, outputs=2, inputs=['u0', 'u1'])" ] }, "execution_count": 6, @@ -892,7 +937,7 @@ { "data": { "text/html": [ - "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>\n", + "<StateSpace stateless: ['u0', 'u1'] -> ['y[0]', 'y[1]'], dt=None>\n", "$$\n", "\\begin{array}{ll}\n", "D = \\left[\\begin{array}{rllrll}\n", @@ -910,7 +955,7 @@ "array([[1., 0.],\n", " [0., 1.]]),\n", "dt=None,\n", - "name='stateless')" + "name='stateless', states=0, outputs=2, inputs=['u0', 'u1'])" ] }, "metadata": {}, @@ -958,7 +1003,7 @@ "array([[-1., 1.]]),\n", "array([[0.]]),\n", "dt=0.1,\n", - "name='sys_dss')" + "name='sys_dss', states=2, outputs=1, inputs=1)" ] }, "execution_count": 8, @@ -987,7 +1032,7 @@ { "data": { "text/html": [ - "<StateSpace stateless: ['u[0]', 'u[1]'] -> ['y[0]', 'y[1]'], dt=None>\n", + "<StateSpace stateless: ['u0', 'u1'] -> ['y[0]', 'y[1]'], dt=None>\n", "$$\n", "\\left[\\begin{array}{rllrll}\n", "1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", @@ -1003,7 +1048,7 @@ "array([[1., 0.],\n", " [0., 1.]]),\n", "dt=None,\n", - "name='stateless')" + "name='stateless', states=0, outputs=2, inputs=['u0', 'u1'])" ] }, "execution_count": 9, @@ -1039,7 +1084,7 @@ "TransferFunction(\n", "array([ 1., -1.]),\n", "array([1., 5., 4.]),\n", - "name='sys_ss$converted')" + "name='sys_ss$converted', outputs=1, inputs=1)" ] }, "execution_count": 10, @@ -1067,7 +1112,7 @@ "TransferFunction(\n", "array([ 1., -1.]),\n", "array([1., 5., 4.]),\n", - "name='sys_ss$converted')" + "name='sys_ss$converted', outputs=1, inputs=1)" ] }, "metadata": {}, @@ -1115,7 +1160,7 @@ " [array([1, 0]), array([1, 0])]],\n", "[[array([1., 5., 4.]), array([1.])],\n", " [array([1]), array([1, 2, 1])]],\n", - "name='sys_mtf_zpk')" + "name='sys_mtf_zpk', outputs=2, inputs=2)" ] }, "execution_count": 12, @@ -1152,7 +1197,7 @@ "array([ 0.07392493, -0.08176823]),\n", "array([ 1. , -1.57515746, 0.60653066]),\n", "dt=0.1,\n", - "name='sys_dss_poly')" + "name='sys_dss_poly', outputs=1, inputs=1)" ] }, "execution_count": 13, @@ -1227,7 +1272,7 @@ " 0.05882353+0.23529412j, 0.1958042 -0.01105691j,\n", " 0.0508706 -0.07767156j]]]),\n", "array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ]),\n", - "name='sys_ss$converted$sampled')" + "name='sys_ss$converted$sampled', outputs=1, inputs=1)" ] }, "metadata": {}, diff --git a/examples/repr_gallery.py b/examples/repr_gallery.py index b346d0761..e72c91b9a 100644 --- a/examples/repr_gallery.py +++ b/examples/repr_gallery.py @@ -21,7 +21,7 @@ # State space (continuous and discrete time) sys_ss = ct.ss([[0, 1], [-4, -5]], [0, 1], [-1, 1], 0, name='sys_ss') sys_dss = sys_ss.sample(0.1, name='sys_dss') -sys_ss0 = ct.ss([], [], [], np.eye(2), name='stateless') +sys_ss0 = ct.ss([], [], [], np.eye(2), name='stateless', inputs=['u0', 'u1']) syslist += [sys_ss, sys_dss, sys_ss0] # Transfer function (continuous and discrete time) @@ -121,7 +121,6 @@ def display_representations( print(fcn(sys)) print("----\n") - # Default formats display_representations("Default repr", repr) display_representations("Default str (print)", str) @@ -130,20 +129,26 @@ def display_representations( if getattr(ct.InputOutputSystem, '_repr_info_', None) and \ ct.config.defaults.get('iosys.repr_format', None) and \ ct.config.defaults['iosys.repr_format'] != 'info': - ct.set_defaults('iosys', repr_format='info') - display_representations("repr_format='info'", repr) -ct.reset_defaults() + with ct.config.defaults({'iosys.repr_format': 'info'}): + display_representations("repr_format='info'", repr) # 'eval' format (if it exists and hasn't already been displayed) if getattr(ct.InputOutputSystem, '_repr_eval_', None) and \ ct.config.defaults.get('iosys.repr_format', None) and \ ct.config.defaults['iosys.repr_format'] != 'eval': - ct.set_defaults('iosys', repr_format='eval') - display_representations("repr_format='eval'", repr) -ct.reset_defaults() - -ct.set_defaults('xferfcn', display_format='zpk') -display_representations( - "xferfcn.display_format=zpk, str (print)", str, - class_list=[ct.TransferFunction]) -ct.reset_defaults() + with ct.config.defaults({'iosys.repr_format': 'eval'}): + display_representations("repr_format='eval'", repr) + +# Change the way counts are displayed +with ct.config.defaults( + {'iosys.repr_show_count': + not ct.config.defaults['iosys.repr_show_count']}): + display_representations( + f"iosys.repr_show_count={ct.config.defaults['iosys.repr_show_count']}", + repr, class_list=[ct.StateSpace]) + +# ZPK format for transfer functions +with ct.config.defaults({'xferfcn.display_format': 'zpk'}): + display_representations( + "xferfcn.display_format=zpk, str (print)", str, + class_list=[ct.TransferFunction]) From 32f65d6fa23f55948185e82221fd157705d32ffd Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 4 Jan 2025 13:10:18 -0800 Subject: [PATCH 19/21] add params to sys_nl in repr_gallery; fix combine/split_tf outputs --- control/bdalg.py | 35 +++++++++++++++++++++++++++++------ examples/repr_gallery.ipynb | 21 +++++++++++---------- examples/repr_gallery.py | 4 +++- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/control/bdalg.py b/control/bdalg.py index 79c1e712c..2be239177 100644 --- a/control/bdalg.py +++ b/control/bdalg.py @@ -557,7 +557,12 @@ def combine_tf(tf_array, **kwargs): ... [s / (s + 2)]], ... name='G' ... ) - ['y[0]', 'y[1]']> + TransferFunction( + [[array([1])], + [array([1, 0])]], + [[array([1, 1])], + [array([1, 2])]], + name='G', outputs=2, inputs=1) Combine NumPy arrays with transfer functions @@ -566,7 +571,14 @@ def combine_tf(tf_array, **kwargs): ... [np.zeros((1, 2)), ct.tf([1], [1, 0])]], ... name='G' ... ) - ['y[0]', 'y[1]', 'y[2]']> + TransferFunction( + [[array([1.]), array([0.]), array([0.])], + [array([0.]), array([1.]), array([0.])], + [array([0.]), array([0.]), array([1])]], + [[array([1.]), array([1.]), array([1.])], + [array([1.]), array([1.]), array([1.])], + [array([1.]), array([1.]), array([1, 0])]], + name='G', outputs=3, inputs=3) """ # Find common timebase or raise error dt_list = [] @@ -653,10 +665,21 @@ def split_tf(transfer_function): ... name='G' ... ) >>> ct.split_tf(G) - array([[ ['y[0]']>, - ['y[0]']>], - [ ['y[1]']>, - ['y[1]']>]], dtype=object) + array([[TransferFunction( + array([87.8]), + array([1, 1]), + name='G', outputs=1, inputs=1), TransferFunction( + array([-86.4]), + array([1, 1]), + name='G', outputs=1, inputs=1)], + [TransferFunction( + array([108.2]), + array([1, 1]), + name='G', outputs=1, inputs=1), TransferFunction( + array([-109.6]), + array([1, 1]), + name='G', outputs=1, inputs=1)]], + dtype=object) """ tf_split_lst = [] for i_out in range(transfer_function.noutputs): diff --git a/examples/repr_gallery.ipynb b/examples/repr_gallery.ipynb index 0981cdf6e..4fee270f7 100644 --- a/examples/repr_gallery.ipynb +++ b/examples/repr_gallery.ipynb @@ -411,9 +411,10 @@ "Inputs (1): ['u[0]']\n", "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", + "Parameters: ['a', 'b']\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "----\n", "\n", "NonlinearIOSystem: sys_dnl, dt=0.1:\n", @@ -424,8 +425,8 @@ "States (2): ['x[0]', 'x[1]']\n", "dt = 0.1\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "----\n", "\n", "InterconnectedSystem: sys_ic, dt=0:\n", @@ -495,8 +496,8 @@ "Update: ['y[0]']>>\n", "Output: ['y[0]']>>\n", "\n", - "Forward: \n", - "Reverse: \n", + "Forward: \n", + "Reverse: \n", "----\n", "\n", "FlatSystem: sys_fsnl, dt=0:\n", @@ -506,11 +507,11 @@ "Outputs (1): ['y[0]']\n", "States (2): ['x[0]', 'x[1]']\n", "\n", - "Update: \n", - "Output: \n", + "Update: \n", + "Output: \n", "\n", - "Forward: \n", - "Reverse: \n", + "Forward: \n", + "Reverse: \n", "----\n", "\n", "============================================================================\n", diff --git a/examples/repr_gallery.py b/examples/repr_gallery.py index e72c91b9a..f6cd1f80c 100644 --- a/examples/repr_gallery.py +++ b/examples/repr_gallery.py @@ -50,8 +50,10 @@ def nl_update(t, x, u, params): def nl_output(t, x, u, params): return sys_ss.C @ x + sys_ss.D @ u +nl_params = {'a': 0, 'b': 1} + sys_nl = ct.nlsys( - nl_update, nl_output, name='sys_nl', + nl_update, nl_output, name='sys_nl', params=nl_params, states=sys_ss.nstates, inputs=sys_ss.ninputs, outputs=sys_ss.noutputs) # Nonlinear system (with linear dynamics), discrete time From 41d92d3c97788ba5ad3107999bc3dc85b71cb2f8 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 7 Jan 2025 16:49:36 -0800 Subject: [PATCH 20/21] address @murrayrm and preliminary @slivingston review comments --- control/config.py | 4 ---- control/iosys.py | 3 ++- control/nlsys.py | 2 +- control/tests/nlsys_test.py | 19 ++++++++++++++++++- control/tests/statesp_test.py | 16 +++++++++++++++- examples/repr_gallery.py | 6 +++--- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/control/config.py b/control/config.py index 0ffa949a4..721871ed3 100644 --- a/control/config.py +++ b/control/config.py @@ -333,10 +333,6 @@ def use_legacy_defaults(version): # reset_defaults() # start from a clean slate - # Verions 0.10.2 - if major == 0 and minor <= 10 and patch < 2: - set_defaults('iosys', repr_format='eval') - # Version 0.9.2: if major == 0 and minor < 9 or (minor == 9 and patch < 2): from math import inf diff --git a/control/iosys.py b/control/iosys.py index 5c4c88776..373bc2111 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -267,6 +267,7 @@ def _repr_info_(self, html=False): if html: # Replace symbols that might be interpreted by HTML processing + # TODO: replace -> with right arrow (later) escape_chars = { '$': r'\$', '<': '<', @@ -820,7 +821,7 @@ def iosys_repr(sys, format=None): Notes ----- - By default, the representation for an input/output is set to 'info'. + By default, the representation for an input/output is set to 'eval'. Set config.defaults['iosys.repr_format'] to change for all I/O systems or use the `repr_format` parameter for a single system. diff --git a/control/nlsys.py b/control/nlsys.py index 9333e6177..7683d3382 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -155,7 +155,7 @@ def __init__(self, updfcn, outfcn=None, params=None, **kwargs): def __str__(self): out = f"{InputOutputSystem.__str__(self)}" - if len(self.params) > 1: + if len(self.params) > 0: out += f"\nParameters: {[p for p in self.params.keys()]}" out += "\n\n" + \ f"Update: {self.updfcn}\n" + \ diff --git a/control/tests/nlsys_test.py b/control/tests/nlsys_test.py index 75f4b0f6d..4b1a235c0 100644 --- a/control/tests/nlsys_test.py +++ b/control/tests/nlsys_test.py @@ -19,7 +19,7 @@ # Basic test of nlsys() def test_nlsys_basic(): def kincar_update(t, x, u, params): - l = params['l'] # wheelbase + l = params['l'] # wheelbase return np.array([ np.cos(x[2]) * u[0], # x velocity np.sin(x[2]) * u[0], # y velocity @@ -248,3 +248,20 @@ def test_ICsystem_str(): r"D = \[\[.*\]\]" assert re.match(ref, str(sys), re.DOTALL) + + +# Make sure nlsys str() works as expected +@pytest.mark.parametrize("params, expected", [ + ({}, r"States \(1\): \['x\[0\]'\]" + "\n\n"), + ({'a': 1}, r"States \(1\): \['x\[0\]'\]" + "\n" + + r"Parameters: \['a'\]" + "\n\n"), + ({'a': 1, 'b': 1}, r"States \(1\): \['x\[0\]'\]" + "\n" + + r"Parameters: \['a', 'b'\]" + "\n\n"), +]) +def test_nlsys_params_str(params, expected): + sys = ct.nlsys( + lambda t, x, u, params: -x, inputs=1, outputs=1, states=1, + params=params) + out = str(sys) + + assert re.search(expected, out) is not None diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 7855cc402..a80168649 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -1313,4 +1313,18 @@ def test_convenience_aliases(): assert isinstance(sys.forced_response([0, 1], [1, 1]), (ct.TimeResponseData, ct.TimeResponseList)) assert isinstance(sys.impulse_response(), (ct.TimeResponseData, ct.TimeResponseList)) assert isinstance(sys.step_response(), (ct.TimeResponseData, ct.TimeResponseList)) - assert isinstance(sys.initial_response(X0=1), (ct.TimeResponseData, ct.TimeResponseList)) \ No newline at end of file + assert isinstance(sys.initial_response(X0=1), (ct.TimeResponseData, ct.TimeResponseList)) + +# Test LinearICSystem __call__ +def test_linearic_call(): + import cmath + + sys1 = ct.rss(2, 1, 1, strictly_proper=True, name='sys1') + sys2 = ct.rss(2, 1, 1, strictly_proper=True, name='sys2') + + sys_ic = ct.interconnect( + [sys1, sys2], connections=['sys1.u', 'sys2.y'], + inplist='sys2.u', outlist='sys1.y') + + for s in [0, 1, 1j]: + assert cmath.isclose(sys_ic(s), (sys1 * sys2)(s)) diff --git a/examples/repr_gallery.py b/examples/repr_gallery.py index f6cd1f80c..27755b59e 100644 --- a/examples/repr_gallery.py +++ b/examples/repr_gallery.py @@ -5,7 +5,7 @@ # of representations (__repr__, __str__) for those systems that can be # used to compare different versions of python-control. It is mainly # intended for uses by developers to make sure there are no unexpected -# changes in representation formats, but also has some interest +# changes in representation formats, but also has some interesting # examples of different choices in system representation. import numpy as np @@ -30,14 +30,14 @@ sys_gtf = ct.tf([1], [1, 0]) syslist += [sys_tf, sys_dtf, sys_gtf] -# MIMO transfer function (continous time only) +# MIMO transfer function (continuous time only) sys_mtf = ct.tf( [[sys_tf.num[0][0].tolist(), [0]], [[1, 0], [1, 0] ]], [[sys_tf.den[0][0].tolist(), [1]], [[1], [1, 2, 1]]], name='sys_mtf_zpk', display_format='zpk') syslist += [sys_mtf] -# Frequency response data (FRD) system (continous and discrete time) +# Frequency response data (FRD) system (continuous and discrete time) sys_frd = ct.frd(sys_tf, np.logspace(-1, 1, 5)) sys_dfrd = ct.frd(sys_dtf, np.logspace(-1, 1, 5)) sys_mfrd = ct.frd(sys_mtf, np.logspace(-1, 1, 5)) From 0f0fad0de06348736e0c8db742c946606332daa9 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 12 Jan 2025 17:24:43 -0800 Subject: [PATCH 21/21] address final (?) @slivingston comments --- control/tests/config_test.py | 22 +++++++++++++++++++--- control/tests/iosys_test.py | 2 +- examples/repr_gallery.ipynb | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 600f689da..c214526cd 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -320,14 +320,30 @@ def test_system_indexing(self): sys2 = sys[1:, 1:] assert sys2.name == 'PRE' + sys.name + 'POST' - def test_repr_format(self): + @pytest.mark.parametrize("kwargs", [ + {}, + {'name': 'mysys'}, + {'inputs': 1}, + {'inputs': 'u'}, + {'outputs': 1}, + {'outputs': 'y'}, + {'states': 1}, + {'states': 'x'}, + {'inputs': 1, 'outputs': 'y', 'states': 'x'}, + {'dt': 0.1} + ]) + def test_repr_format(self, kwargs): from ..statesp import StateSpace from numpy import array - sys = ct.ss([[1]], [[1]], [[1]], [[0]]) + sys = ct.ss([[1]], [[1]], [[1]], [[0]], **kwargs) new = eval(repr(sys)) for attr in ['A', 'B', 'C', 'D']: - assert getattr(sys, attr) == getattr(sys, attr) + assert getattr(new, attr) == getattr(sys, attr) + for prop in ['input_labels', 'output_labels', 'state_labels']: + assert getattr(new, attr) == getattr(sys, attr) + if 'name' in kwargs: + assert new.name == sys.name def test_config_context_manager(): diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 1f9327785..78177995d 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2351,7 +2351,7 @@ def test_iosys_repr(fcn, spec, expected, missing, format): sys.repr_format = format out = repr(sys) if format == 'eval': - assert re.search(expected, out) != None + assert re.search(expected, out) is not None if missing is not None: assert re.search(missing, out) is None diff --git a/examples/repr_gallery.ipynb b/examples/repr_gallery.ipynb index 4fee270f7..e0d33c147 100644 --- a/examples/repr_gallery.ipynb +++ b/examples/repr_gallery.ipynb @@ -7,7 +7,7 @@ "source": [ "# System Representation Gallery\n", "\n", - "This Jupyter notebook creates different types of systems and generates a variety of representations (`__repr__`, `__str__`) for those systems that can be used to compare different versions of python-control. It is mainly intended for uses by developers to make sure there are no unexpected changes in representation formats, but also has some interest examples of different choices in system representation." + "This Jupyter notebook creates different types of systems and generates a variety of representations (`__repr__`, `__str__`) for those systems that can be used to compare different versions of python-control. It is mainly intended for uses by developers to make sure there are no unexpected changes in representation formats, but also has some interesting examples of different choices in system representation." ] }, {