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

Display Connection Matrix #963

Open
SoundsSerious opened this issue Jan 21, 2024 · 8 comments
Open

Display Connection Matrix #963

SoundsSerious opened this issue Jan 21, 2024 · 8 comments

Comments

@SoundsSerious
Copy link

SoundsSerious commented Jan 21, 2024

Hi,

I wanted to expand on #925 and display a sparse graph of all the system connections, some of which had the same signal name, which is a case I found the new connection table feature a bit confusing for so I wrote this short function to display the connection matrix directly using matplotlib imshow.

def make_connection_plot(self):
    iinx = {}
    oinx = {}
    sinx = {} 

    # Go through the system list and keep track of counts, offsets
    for sysidx, sys in enumerate(self.syslist):

        for st in range(sys.nstates):
            iinx[ max(iinx.keys())+1 if iinx else 0 + st] = f'{sys.name}.{sys.state_labels[st]}'
        for st in range(sys.noutputs):
            oinx[ max(oinx.keys())+1 if oinx else 0 +st] = f'{sys.name}.{sys.output_labels[st]}'
        for st in range(sys.ninputs):
            sinx[max(sinx.keys())+1 if sinx else 0 +st] = f'{sys.name}.{sys.input_labels[st]}'

    cmm = self.connect_map.copy()
    cmm[cmm==0] = np.nan
    imshow(cmm)
    grid()
    title(f'Connection Map: {self.name}')
    xticks(ticks=list(oinx.keys()),labels=list(oinx.values()),rotation=90)
    yticks(ticks=list(sinx.keys()),labels=list(sinx.values()))
    xlabel('Outputs')
    ylabel('Inputs')
    
    tight_layout()

This code results in this plot, displaying the gain via color as well as the scoped input and output names.
Connectionmap

@sawyerbfuller
Copy link
Contributor

Thank you @SoundsSerious for the submission! I had been stuck on how to best represent connections for explicitly connected systems and the complexity of signal names with sub components and I think you have come up with a simple and most elegant solution. It would be great to have something like this in the library.

A few thoughts.

  1. The color choice of purple and yellow do not carry an obvious association for me for which is positive. It would be good to use an alternative that does. One option is if matplotlib supports a “-“ marker (I know it has a “+”), that would be an obvious choice. Filled vs empty markers is another option.

  2. It would be nice to have also a pure text version that would print out when connection-table is called on an explicitly-connected system. A pure text version of this exactly as-is would probably not make sense because if there are a lot of outputs, the table would quickly get too wide to fit on screen. But a variant that printed out the names of each input reached by a given output probably would fit.

  3. I suggest shortening the function name to “connection-plot”

  4. Would you consider submitting a pull request? (See the developers wiki for instructions if that helps).

@SoundsSerious
Copy link
Author

@sawyerbfuller Ah glad you like it, happy to give back to a framework I've been getting alot of use out of!

I agree the formatting sucks but this is something I put together in about 15 minutes so there is lots of room for improvement.

  1. Looking around for other examples I think this is an interesting format, although it requires seaborn and pandas dependencies, i think matplotlib could make it. https://seaborn.pydata.org/examples/heat_scatter.html Potentially you could use symbols to identify various layers or types of connections, whereas colors might be the gain?

  2. Agreed about the pure text version, especially in the light of simplifying dependencies, however i see matplotlib is already a dependency.

  3. sounds good!

  4. yes absolutely, however I'm quite busy at the moment until I submit a paper in April so maybe we could leave this issue open and see if anyone else has any ideas on 1/2?

@SoundsSerious
Copy link
Author

SoundsSerious commented Jan 21, 2024

I have actually broken this down into two functions one to index the states & IO by global name and one to plot.

system_index function is helping me to set initial state for this fairly large system so perhaps this is part of a bigger quality of life PR.

def make_connection_plot(self):
    iinx,oinx,sinx = system_index(self)

    cmm = self.connect_map.copy()
    cmm[cmm==0] = np.nan
    imshow(cmm)
    grid()
    title(f'Connection Map: {self.name}')
    xticks(ticks=list(oinx.keys()),labels=list(oinx.values()),rotation=90)
    yticks(ticks=list(sinx.keys()),labels=list(sinx.values()))
    xlabel('Outputs')
    ylabel('Inputs')
    
    tight_layout()

def system_index(self):
    iinx = {}
    oinx = {}
    sinx = {} 

    # Go through the system list and keep track of counts, offsets
    for sysidx, sys in enumerate(self.syslist):

        for st in range(sys.nstates):
            sinx[ max(sinx.keys())+1 if sinx else 0 ] = f'{sys.name}.{sys.state_labels[st]}'
        for st in range(sys.noutputs):
            oinx[ max(oinx.keys())+1 if oinx else 0] = f'{sys.name}.{sys.output_labels[st]}'
        for st in range(sys.ninputs):
            iinx[max(iinx.keys())+1 if iinx else 0] = f'{sys.name}.{sys.input_labels[st]}'

    return iinx,oinx,sinx

@sawyerbfuller
Copy link
Contributor

sawyerbfuller commented Jan 22, 2024

@SoundsSerious yes, no need to worry about a matplotlib dependency, that's already part of the library as you noted. Sometimes I like having the option of not spawning a new plot : )

I am not aware of an existing function for InterconnectedSystems that has the system_index functionality, so I agree that would be a useful method. It took me awhile to figure out what you were doing in it, though. The following might make that code more legible:

def system_index(self):
        istate = 0
        state_index = {} 
        for sys in self.syslist:
            for i in range(sys.nstates):
                state_index[istate] = f'{sys.name}.{sys.state_labels[i]}'
                istate += 1
            # and same for inputs and outputs

@SoundsSerious
Copy link
Author

Sometimes I like having the option of not spawning a new plot : )

Ha yes sometimes you can have too many plots! Not sure if you ever tried jupyter qtconsole but it has the ability to do in or out of terminal plots as well as a bunch of neat tricks such as bash commands and navigation.

I like that formulation much more, it is alot clearer whats going on!

@SoundsSerious
Copy link
Author

@sawyerbfuller
I think it would be easy enough to make some tables from the stateindex with https://github.com/matthewdeanmartin/terminaltables

Not sure you want another dependency but alternatively you could make a soft dependency (import function local and fail with a message to install it) or you could just copy the code from one of the IO modules

@sawyerbfuller
Copy link
Contributor

@SoundsSerious I thought about that in #925, e.g. whether to use that one or e.g. https://github.com/petercorke/ansitable. I decided in that case that the table requirements weren't too onerous and could get away with just rolling my own by hand to reduce dependencies. But if we start making lots of tables like this, then it starts to make more sense. What do you think?

@KybernetikJo
Copy link
Contributor

I'm not sure about your needs, but from a dependency perspective, rich would probably be the better maintained project.
https://rich.readthedocs.io/en/stable/tables.html

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

No branches or pull requests

3 participants