Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

print a connection table for interconnected systems #925

Merged
merged 12 commits into from
Sep 16, 2023

Conversation

sawyerbfuller
Copy link
Contributor

@sawyerbfuller sawyerbfuller commented Jul 24, 2023

This PR prints out a table of each signal name, where it comes from (source), and where it goes (destination). It is intended primarily for systems that have been connected implicitly, because in that case all of the signal names have been defined. It doesn't really work for systems that have been connected explicitly, because their signal names may not be unique

It was is inspired by a similar table printout in bdsim and the need to have a means to debug interconnections of many systems.

Example:

        >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y')
        >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u')
        >>> L = ct.interconnect([C, P], inputs='e', outputs='y')
        >>> L.connection_table()
        signal    | source                  | destination
        --------------------------------------------------------------
        e         | input                   | system 0
        u         | system 0                | system 1
        y         | system 1                | output

(edit: in the code above, signal_table has been changed to connection_table)
Remarks:

  • it does not list systems by their name by default because the because default system names (e.g. sys[17]) are not very descriptive or helpful when you're connecting systems implicitly with signal names. instead the table just references them by the order they appear in the interconnect list, with an option to print names instead.
  • it would be convenient if instead systems were listed by the name of the variable referencing them, e.g. if the first row looked like e | input | C , but this is not straightforward in Python because it is possible to have many variable names pointing to the same object in memory.
  • Maybe this could be achieved using a change to the library that automatically populates the 'name' field in new systems with the variable name it is first assigned to, but I am not sure if this is possible or Pythonic
  • this PR also includes a couple of docstring cleanups to remove references to the namedio class

@coveralls
Copy link

coveralls commented Jul 24, 2023

Coverage Status

coverage: 94.969% (+0.001%) from 94.968% when pulling 5c23f1a on sawyerbfuller:connections into 42c6fb1 on python-control:main.

@murrayrm
Copy link
Member

This looks very useful for sorting out what is connected to what. A couple of questions and comments:

  • What happens if the list of systems is longer than 32 characters. Is there a way to either truncate the field (perhaps end in "...") or adjust the spacing variable to make sure that the table stays lined up?
  • Is there a way to generate a warning if the system was not generated using common signal names?
  • A more general functionality seems like it would be possible by looking through the connect_map, input_map, and output_map arrays, which describe how everything is connected.

As an experiment, I tried modifying the example above to use a more typical form with a reference input:

P = ct.ss(1,1,1,0, inputs='u', outputs='y')
C = ct.tf(10, [.1, 1], inputs='e', outputs='u')
sumblk = ct.summing_junction(inputs=['r', '-y'], outputs='e')
T = ct.interconnect([C, P, sumblk], inputs='r', outputs='y')
T.signal_table()

which gives

signal    | source                        | destination
--------------------------------------------------------------------------
e         | system 2                      | system 0                      
y         | system 1                      | output, system 2              
r         | input                         | system 2                      
u         | system 0                      | system 1      

That looks right to me, but it it might be nice to see the fact that y goes into system 2 (the summing junction) with a negative gain.

@sawyerbfuller
Copy link
Contributor Author

sawyerbfuller commented Aug 2, 2023

This looks very useful for sorting out what is connected to what. A couple of questions and comments:

* What happens if the list of systems is longer than 32 characters.  Is there a way to either truncate the field (perhaps end in "...") or adjust the `spacing` variable to make sure that the table stays lined up?

Right now it just breaks alignment. I think a good solution might be to add an elipsis as you suggest, and to allow the user to set the spacing parameter. update forthcoming.

* Is there a way to generate a warning if the system was not generated using common signal names?

* A more general functionality seems like it would be possible by looking through the `connect_map`, `input_map`, and `output_map` arrays, which describe how everything is connected.

On further thought, I would propose the following:

  1. change the name of this function to be more generic, such as "connection_table", rather than "signal_table", allowing for a future PR that perhaps produces a different kind of table for explicitly-connected systems.
  2. which would pave the way for a future PR geared toward explicitly-connected systems that perhaps uses those maps. OTOH I think that such interconnections might better be presented in a table that puts the system name first, followed by its input and output systems. Perhaps a flag could be added to to the InterconnectedSystem class, specifying whether it was connected implicitly or explicitly, and choose the table type based on that.
  3. (edit) add in a warning for the time being

As an experiment, I tried modifying the example above to use a more typical form with a reference input:

P = ct.ss(1,1,1,0, inputs='u', outputs='y')
C = ct.tf(10, [.1, 1], inputs='e', outputs='u')
sumblk = ct.summing_junction(inputs=['r', '-y'], outputs='e')
T = ct.interconnect([C, P, sumblk], inputs='r', outputs='y')
T.signal_table()

which gives

signal    | source                        | destination
--------------------------------------------------------------------------
e         | system 2                      | system 0                      
y         | system 1                      | output, system 2              
r         | input                         | system 2                      
u         | system 0                      | system 1      

That looks right to me, but it it might be nice to see the fact that y goes into system 2 (the summing junction) with a negative gain.

Great. I agree this could be useful. Open to it going into a future PR? I am not sure this information is stored anywhere in the interconnectedSystem class: the negative sign appears as one of the elements in summing junction's D matrix.

@sawyerbfuller
Copy link
Contributor Author

sawyerbfuller commented Aug 14, 2023

@murrayrm I incorporated (most of) your suggested updates:

  • function warns if connections were made explicitly
    • to do this, I added a field to InterconnectedSystem.connection_type that indicates how connections were made: 'explicit'-ly, 'implicit'-ly, or not at all (None)
  • table adds an elipsis if any column gets too long; user can now specify column width. Example:
    P1 = ct.ss(1,1,1,0, inputs='u', outputs='y', name='Plant1')
    P2 = ct.tf(10, [.1, 1], inputs='e', outputs='y', name='Plant2')
    P3 = ct.tf(10, [.1, 1], inputs='x', outputs='y', name='Plant3')
    L = ct.interconnect([P1, P2, P3], inputs=['e', 'u', 'x'], outputs='y')
    L.connection_table(show_names=True, column_width=23)

signal    | source               | destination
--------------------------------------------------------
x         | input                | Plant3               
e         | input                | Plant2               
u         | input                | Plant1               
y         | Plant1, Plant2, Pl.. | output               
  • renamed function connection_table to make it more general, allowing for it to print a similar, but probably differently-formatted table for explicit connectiosn in the future
  • I don't know of an obvious way to indicate negative connections. perhaps a future PR?

control/nlsys.py Outdated

Parameters
----------
show_names : bool (optional)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Numpydoc standard notation is bool, optional (comma instead of parens). I'm not sure if Sphinx will parse or not, but probably better to change so that it is consistent with other documentation.

control/nlsys.py Outdated
each system. Default is False because system name is not usually
specified when performing implicit interconnection using
:func:`interconnect`.
column_width : int (optional)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional) → , optional

@@ -1955,7 +2032,7 @@ def interconnect(
signals are given names, then the forms 'sys.sig' or ('sys', 'sig')
are also recognized. Finally, for multivariable systems the signal
index can be given as a list, for example '(subsys_i, [inp_j1, ...,
inp_jn])'; as a slice, for example, 'sys.sig[i:j]'; or as a base
inp_jn])'; or as a slice, for example, 'sys.sig[i:j]'; or as a base
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't this correct before? Now it says 'a; or b; or c' instead of 'a; b; or c'.

control/nlsys.py Outdated
Comment on lines 2582 to 2617

def connection_table(sys, show_names=False, column_width=32):
"""Print table of connections inside an interconnected system model.

Intended primarily for :class:`InterconnectedSystems` that have been
connected implicitly using signal names.

Parameters
----------
sys : :class:`InterconnectedSystem`
Interconnected system object
show_names : bool (optional)
Instead of printing out the system number, print out the name of
each system. Default is False because system name is not usually
specified when performing implicit interconnection using
:func:`interconnect`.
column_width : int (optional)
Character width of printed columns


Examples
--------
>>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P')
>>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C')
>>> L = ct.interconnect([C, P], inputs='e', outputs='y')
>>> L.connection_table(show_names=True) # doctest: +SKIP
signal | source | destination
--------------------------------------------------------------
e | input | C
u | C | P
y | P | output
"""
assert isinstance(sys, InterconnectedSystem), "system must be"\
"an InterconnectedSystem."

sys.connection_table(show_names=show_names, column_width=column_width)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something that we need to fix in this PR, but we should eventually adopt some mechanism for creating duplicate docstrings from a single primary docstring (see, for example, the discussion here).

@@ -1459,7 +1459,7 @@ class LinearICSystem(InterconnectedSystem, StateSpace):
"""

def __init__(self, io_sys, ss_sys=None):
def __init__(self, io_sys, ss_sys=None, connection_type=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to allow the connection type to be overridden? It seems like this should just be inherited from the underlying InterconnectedSystem.

@murrayrm murrayrm merged commit 405bf77 into python-control:main Sep 16, 2023
14 checks passed
@murrayrm murrayrm added this to the 0.10.0 milestone Mar 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants