diff --git a/control/bdalg.py b/control/bdalg.py index 2dbd5c8e9..2be239177 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,34 @@ 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' + ... ) + TransferFunction( + [[array([1])], + [array([1, 0])]], + [[array([1, 1])], + [array([1, 2])]], + name='G', outputs=2, inputs=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' + ... ) + 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 = [] @@ -616,10 +634,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 +657,29 @@ 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([[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): @@ -660,6 +690,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/config.py b/control/config.py index c5a59250b..721871ed3 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) @@ -266,7 +288,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 +301,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))) diff --git a/control/frdata.py b/control/frdata.py index 64a1e8227..cb6925661 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, @@ -401,30 +400,37 @@ def __str__(self): mimo = self.ninputs > 1 or self.noutputs > 1 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, :]))]) return '\n'.join(outstr) - def __repr__(self): - """Loadable string representation, - - limited for number of data points. - """ - return "FrequencyResponseData({d}, {w}{smooth})".format( + def _repr_eval_(self): + # 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 "") + out += self._dt_repr() + if len(labels := self._label_repr()) > 0: + out += ",\n" + labels + + out += ")" + return out + def __neg__(self): """Negate a transfer function.""" diff --git a/control/iosys.py b/control/iosys.py index 2c1f9cea7..373bc2111 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,6 +31,8 @@ 'iosys.indexed_system_name_suffix': '$indexed', 'iosys.converted_system_name_prefix': '', 'iosys.converted_system_name_suffix': '$converted', + 'iosys.repr_format': 'eval', + 'iosys.repr_show_count': True, } @@ -162,6 +164,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 @@ -183,6 +187,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)) @@ -237,18 +243,127 @@ def _generic_name_check(self): #: :meta hide-value: nstates = None - def __repr__(self): - return f'<{self.__class__.__name__}:{self.name}:' + \ - f'{list(self.input_labels)}->{list(self.output_labels)}>' + # + # System representation + # 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}" + out += self._dt_repr(separator="\n", space=" ") + return out + + def __repr__(self): + return iosys_repr(self, format=self.repr_format) + + 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(separator=", ", space="") + ">" + + if html: + # Replace symbols that might be interpreted by HTML processing + # TODO: replace -> with right arrow (later) + 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): + """String representation format. + + Format used in creating the representation for the system: + + * 'info' : [outputs] + * 'eval' : system specific, loadable representation + * '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 + `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'] + + @repr_format.setter + def repr_format(self, value): + self._repr_format = value + + 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 + 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)}" + else: + spec = "" + + # 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) + elif len(spec) > 0: + out += ", " + count += len(spec) + 2 + out += spec + + return out + + def _dt_repr(self, separator="\n", space=""): + if config.defaults['control.default_dt'] != 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 "" # Find a list of signals by name, index, or pattern def _find_signals(self, name_list, sigdict): @@ -685,6 +800,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' : HTML/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 'eval'. + 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_html_() + 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/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/nlsys.py b/control/nlsys.py index beb2566e7..7683d3382 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 @@ -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) > 0: + 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): @@ -778,6 +782,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 = InputOutputSystem.__str__(self) + + 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 + def _update_params(self, params, warning=False): for sys in self.syslist: local = sys.params.copy() # start with system parameters @@ -1018,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 @@ -1303,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 @@ -1314,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 070be2e15..98adc942f 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 @@ -33,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 @@ -131,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 `'{:'` @@ -380,23 +381,56 @@ 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])]) - if self.isdtime(strict=True): - 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.""" - # TODO: add input/output names (?) - return "StateSpace({A}, {B}, {C}, {D}{dt})".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__(), - 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__()) + + out += super()._dt_repr(separator=",\n", space="") + if len(labels := super()._label_repr()) > 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) + \ + "\n" + self._latex_partitioned() + elif config.defaults['statesp.latex_repr_type'] == 'separate': + return super()._repr_info_(html=True) + \ + "\n" + 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 @@ -407,21 +441,24 @@ 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(' + (r'\left[' + r'\begin{array}' + r'{' + 'rll' * self.ninputs + '}') ] - for Di in asarray(self.D): + for Di in asarray(D): lines.append('&'.join(_f2s(Dij) for Dij in Di) + '\\\\') lines.extend([ r'\end{array}' - r'\right)' - + self._latex_dt(), + r'\right]', r'$$']) return '\n'.join(lines) @@ -439,27 +476,31 @@ 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(' + (r'\left[' + r'\begin{array}' + 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]) + '\\\\') lines.extend([ r'\end{array}' - + r'\right)' - + self._latex_dt(), + + r'\right]', r'$$']) return '\n'.join(lines) @@ -480,7 +521,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): @@ -488,7 +529,7 @@ def fmt_matrix(matrix, name): + '\\\\') matlines.extend([ r'\end{array}' - r'\right)']) + r'\right]']) return matlines if self.nstates > 0: @@ -502,52 +543,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.""" @@ -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 947dc95aa..c214526cd 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -319,3 +319,45 @@ def test_system_indexing(self): indexed_system_name_suffix='POST') sys2 = sys[1:, 1:] assert sys2.name == 'PRE' + sys.name + 'POST' + + @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]], **kwargs) + new = eval(repr(sys)) + for attr in ['A', 'B', 'C', 'D']: + 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(): + # 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 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 b08cd8260..c63d9e217 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -465,21 +465,23 @@ 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') - assert repr(sys0) == ref0 - assert repr(sys1) == ref1 + assert ct.iosys_repr(sys0, format='eval') == ref0 + assert ct.iosys_repr(sys1, format='eval') == ref1 - sys0r = eval(repr(sys0)) + 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(repr(sys1)) + 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) @@ -503,19 +505,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 54d6d56c8..78177995d 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -2287,6 +2287,89 @@ def test_signal_indexing(): with pytest.raises(IndexError, match=r"signal name\(s\) not valid"): resp.outputs['y[0]', 'u[0]'] + +@slycotonly +@pytest.mark.parametrize("fcn, spec, expected, missing", [ + (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]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=\[.*\],\noutputs=3, inputs=2\)", r"dt"), +]) +@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: + 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) + + # Construct the 'info' format + info_expected = f"<{sys.__class__.__name__} {sys.name}: " \ + 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) + 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 == 'eval': + assert re.search(expected, out) is not None + + if missing is not None: + assert re.search(missing, out) is None + + elif format == 'info': + assert out == info_expected + + # Make sure we can change back to the default format + sys.repr_format = None + + # Make sure the default format is OK + out = repr(sys) + 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]) def test_relabeling(fcn): sys = ct.rss(1, 1, 1, name="sys") 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, diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 5359ceea3..e93138af3 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, ct.iosys_repr(sys, 'eval')) is not None + + # Test string representation + if str_expected is not None: + assert re.search(str_expected, str(sys)) is not None + + # Test LaTeX/HTML representation + assert re.search(latex_expected, sys._repr_html_()) is not None diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index f702e704b..2fc55f7d4 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -34,8 +34,8 @@ 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) == \ - "['y[0]', 'y[1]']>" + assert ct.iosys_repr(sys, format='info') == \ + " ['y[0]', 'y[1]']>" # Pass the names as arguments sys = ct.ss( @@ -46,8 +46,8 @@ 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) == \ - "['y1', 'y2']>" + assert ct.iosys_repr(sys, format='info') == \ + " ['y1', 'y2']>" # Do the same with rss sys = ct.rss(['x1', 'x2', 'x3'], ['y1', 'y2'], 'u1', name='random') @@ -56,8 +56,8 @@ 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) == \ - "['y1', 'y2']>" + assert ct.iosys_repr(sys, format='info') == \ + " ['y1', 'y2']>" # List of classes that are expected diff --git a/control/tests/nlsys_test.py b/control/tests/nlsys_test.py index 926ca4364..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.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 @@ -199,3 +200,68 @@ 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"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\]" + \ + "\n\n" + \ + r"A = \[\[.*\]\]" + "\n\n" + \ + r"B = \[\[.*\]\]" + "\n\n" + \ + r"C = \[\[.*\]\]" + "\n\n" + \ + 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 647db4567..a80168649 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -735,19 +735,24 @@ 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})"]) - assert repr(sys322) == ref322.format(dt='') + 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}, 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) - assert repr(sysd), ref322.format(dt=" == 0.4") + assert ct.iosys_repr(sysd, format='eval'), ref322.format(dt=",\ndt=0.4") array = np.array # noqa - sysd2 = eval(repr(sysd)) + 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) @@ -756,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.]]\n") - 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.""" @@ -1049,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]], @@ -1061,23 +1066,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': "<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' : '$$\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' : '$$\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' : '$$\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' : '$$\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' : '$$\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' : '$$\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' : '$$\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'} @@ -1088,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: @@ -1109,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,))) + 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( @@ -1136,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'] @@ -1149,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: @@ -1308,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/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 3f87ef1d2..c7f92379a 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]), "\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 -\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,82 +897,77 @@ 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 -------\n 2 {var} + 1"), (([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")]) @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, - '\n' - ' s\n' - '-----\n' - 's + 1\n'), + ' s\n' + ' -----\n' + ' s + 1'), ([-1], [-1], 1, - '\n' - 's + 1\n' - '-----\n' - 's + 1\n'), + ' s + 1\n' + ' -----\n' + ' s + 1'), ([-1], [1], 1, - '\n' - 's + 1\n' - '-----\n' - 's - 1\n'), + ' s + 1\n' + ' -----\n' + ' s - 1'), ([1], [-1], 1, - '\n' - 's - 1\n' - '-----\n' - 's + 1\n'), + ' s - 1\n' + ' -----\n' + ' s + 1'), ([-1], [-1], 2, - '\n' - '2 (s + 1)\n' - '---------\n' - ' s + 1\n'), + ' 2 (s + 1)\n' + ' ---------\n' + ' s + 1'), ([-1], [-1], 0, - '\n' - '0\n' - '-\n' - '1\n'), + ' 0\n' + ' -\n' + ' 1'), ([-1], [1j, -1j], 1, - '\n' - ' s + 1\n' - '-----------------\n' - '(s - 1j) (s + 1j)\n'), + ' s + 1\n' + ' -----------------\n' + ' (s - 1j) (s + 1j)'), ([4j, -4j], [2j, -2j], 2, - '\n' - '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, - '\n' - '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, - '\n' - ' 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, - '\n' - ' 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""" @@ -983,20 +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", - '\n' - ' 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", - '\n' - ' 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", - '\n' - ' 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""" @@ -1012,26 +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]]], - ('\n' - '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') @@ -1061,59 +1057,73 @@ 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]) + [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'], + for var, H, dtstr in zip(['s', 'z'], [Hc, Hd], - ['', r'\quad dt = 0.1']): - ref = (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(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): + def test_loadable_repr(self, Hargs, ref): """Test __repr__ printout.""" H = TransferFunction(*Hargs) - assert repr(H) == ref + rep = ct.iosys_repr(H, format='eval') + 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 5e391d30b..dea61f9f3 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 @@ -209,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) @@ -441,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) @@ -479,50 +481,48 @@ 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 - # represent to implement a re-loadable version - def __repr__(self): - """Print transfer function in loadable form.""" + def _repr_eval_(self): + # Loadable format if self.issiso(): - return "TransferFunction({num}, {den}{dt})".format( + out = "TransferFunction(\n{num},\n{den}".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 '') + den=self.den_array[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 + out += super()._dt_repr(separator=",\n", space="") + if len(labels := self._label_repr()) > 0: + out += ",\n" + labels - def _repr_latex_(self, var=None): - """LaTeX representation of transfer function, for Jupyter notebook.""" + out += ")" + return out + 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 = ['$$'] + out = [super()._repr_info_(html=True), '\n$$'] if mimo: out.append(r"\begin{bmatrix}") @@ -530,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( @@ -545,7 +545,7 @@ def _repr_latex_(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("&") @@ -556,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"\quad dt = ", str(self.dt)] - out.append("$$") return ''.join(out) @@ -777,7 +773,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) @@ -1313,9 +1309,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 @@ -1360,6 +1359,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) diff --git a/examples/repr_gallery.ipynb b/examples/repr_gallery.ipynb new file mode 100644 index 000000000..e0d33c147 --- /dev/null +++ b/examples/repr_gallery.ipynb @@ -0,0 +1,1337 @@ +{ + "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 interesting 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": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "============================================================================\n", + " Default repr\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', states=2, outputs=1, inputs=1)\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', states=2, outputs=1, inputs=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', states=0, outputs=2, inputs=['u0', 'u1'])\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', outputs=1, inputs=1)\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', outputs=1, inputs=1)\n", + "----\n", + "\n", + "TransferFunction: sys[3], dt=0:\n", + "-------------------------------\n", + "TransferFunction(\n", + "array([1]),\n", + "array([1, 0]),\n", + "outputs=1, inputs=1)\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', outputs=2, inputs=2)\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", + " ['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): ['u0', 'u1']\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", + "Parameters: ['a', 'b']\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", + "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", + "\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", + "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", + "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", + "-------------------------\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='info'\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", + " 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", + "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", + "$$\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": [ + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss', states=2, outputs=1, inputs=1)" + ] + }, + "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", + "$$\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": [ + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss', states=2, outputs=1, inputs=1)" + ] + }, + "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", + "$$\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": [ + "StateSpace(\n", + "array([[ 0., 1.],\n", + " [-4., -5.]]),\n", + "array([[0.],\n", + " [1.]]),\n", + "array([[-1., 1.]]),\n", + "array([[0.]]),\n", + "name='sys_ss', states=2, outputs=1, inputs=1)" + ] + }, + "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: ['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", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + "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', states=0, outputs=2, inputs=['u0', 'u1'])" + ] + }, + "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: ['u0', 'u1'] -> ['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", + "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": [ + "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', states=0, outputs=2, inputs=['u0', 'u1'])" + ] + }, + "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", + "$$\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": [ + "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', states=2, outputs=1, inputs=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: ['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", + "0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n", + "\\end{array}\\right]\n", + "$$" + ], + "text/plain": [ + "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', states=0, outputs=2, inputs=['u0', 'u1'])" + ] + }, + "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]']>\n", + "$$\\dfrac{s - 1}{s^2 + 5 s + 4}$$" + ], + "text/plain": [ + "TransferFunction(\n", + "array([ 1., -1.]),\n", + "array([1., 5., 4.]),\n", + "name='sys_ss$converted', outputs=1, inputs=1)" + ] + }, + "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]']>\n", + "$$\\dfrac{s - 1}{(s + 1) (s + 4)}$$" + ], + "text/plain": [ + "TransferFunction(\n", + "array([ 1., -1.]),\n", + "array([1., 5., 4.]),\n", + "name='sys_ss$converted', outputs=1, inputs=1)" + ] + }, + "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]']>\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": [ + "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', outputs=2, inputs=2)" + ] + }, + "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>\n", + "$$\\dfrac{0.07392 z - 0.08177}{z^2 - 1.575 z + 0.6065}$$" + ], + "text/plain": [ + "TransferFunction(\n", + "array([ 0.07392493, -0.08176823]),\n", + "array([ 1. , -1.57515746, 0.60653066]),\n", + "dt=0.1,\n", + "name='sys_dss_poly', outputs=1, inputs=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", + "$$\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": [ + "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)" + ] + }, + "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 +} diff --git a/examples/repr_gallery.py b/examples/repr_gallery.py new file mode 100644 index 000000000..27755b59e --- /dev/null +++ b/examples/repr_gallery.py @@ -0,0 +1,156 @@ +# 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 interesting +# 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', inputs=['u0', 'u1']) +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 (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 (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)) +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 + +nl_params = {'a': 0, 'b': 1} + +sys_nl = ct.nlsys( + 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 +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("=" * 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}:") + 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': + 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': + 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])