How can I extract the elements referenced by the ends of a ConnectionUsage?

Here is my model (minimized and obfuscated):

package P1 {
    part A {
        out port o;
    }
}

package P2 {
    part B {
        in port i;
        connect P1::A.o to i;
    }
}

I want to use SysIDE Automator to extract the ends of the connection in part B as Python strings, something like (["P1", "A", "o"], ["i"]) would be ideal. The .declared_ends attribute sounds like exactly what I want, but I’m not sure how to get strings “P1”, “A” etc. from it. Here’s my attempt so far:

#!/usr/bin/env python3.13
import syside

m,_ = syside.load_model(["test.sysml"])

p2_matches = [n for n in m.nodes(syside.Package) if n.name == "P2"]
assert len(p2_matches) == 1
p2 = p2_matches[0]

partB = p2["B"]
assert(isinstance(partB, syside.PartUsage))
partB_connections = [n for n in partB.children.elements if isinstance(n, syside.ConnectionUsage)]
assert len(partB_connections) == 1
partB_connection = partB_connections[0]

print(partB_connection.declared_ends.elements)
# prints [P2::B::(anonymous ConnectionUsage)::source, P2::B::(anonymous ConnectionUsage)::target]

A hacky regex works to get the connection source and target names as strings (feeding the ConnectionUsage node into syside.pprint() and parsing the result with a regex) but that’s not a great solution and I was wondering if there is a robust way to do this by walking the syntax tree?

Hi Ricardas!

You were on the right track, but stopped a bit too early! Here is the example script that can get you the ports that are connected. Afterwards, you can do what you want with those ports, not just print their names!

import syside


def main(model: syside.Model):
    # Since the model only has one user document, we will only look into that.
    with model.user_docs[0].lock() as locked:
        # Let's get the part B from package 2
        package2 = locked.root_node.get_member("P2")
        assert isinstance(package2, syside.Package)
        partB = package2.get_member("B")
        assert isinstance(partB, syside.PartUsage)

        # Now let's extract the connection
        partB_connections = [
            n for n in partB.children.elements if isinstance(n, syside.ConnectionUsage)
        ]
        assert len(partB_connections) == 1
        partB_connection = partB_connections[0]

        # Do some magic with the connection
        connection_ends: list[syside.Feature] = []
        for end in partB_connection.declared_ends:
            connection_ends.append(end[1])  # Only take the feature, not the membership
        assert len(connection_ends) == 2

        connection_end_1 = connection_ends[0]
        connection_end_2 = connection_ends[1]
        assert isinstance(connection_end_1, syside.Feature)
        assert isinstance(connection_end_2, syside.Feature)

        # Now, the actual targets of the features can be found as "referenced_feature_target"
        # in the features.
        port_1 = connection_end_1.referenced_feature_target
        port_2 = connection_end_2.referenced_feature_target
        assert isinstance(port_1, syside.PortUsage)
        assert isinstance(port_2, syside.PortUsage)

        # Let's print their names
        print(port_1)
        print(port_2)


if __name__ == "__main__":
    model, diagnostics = syside.try_load_model(["test.sysml"])
    if diagnostics.contains_errors(warnings_as_errors=True):
        print(str(diagnostics))
        exit(1)

    main(model)

Alternatively, instead of using declared_ends, we can also look at source and target features:

        source_feature = partB_connection.get_member("source")
        target_feature = partB_connection.get_member("target")
        assert isinstance(source_feature, syside.Feature)
        assert isinstance(target_feature, syside.Feature)

        # Now, the actual targets of the features can be found as "referenced_feature_target"
        # in the features.
        source_port = source_feature.referenced_feature_target
        target_port = target_feature.referenced_feature_target
        assert isinstance(source_port, syside.PortUsage)
        assert isinstance(target_port, syside.PortUsage)

        # Let's print their names
        print(source_port)
        print(target_port)

I hope this helps you on your SysMLv2 adventures.

3 Likes

Looking at .referenced_feature_target.qualified_name of the source/target feature gave me exactly what I wanted, thanks!

After some testing I’m still having issues in the actual model. Here’s another simplified example:

package P1 {
    part def PD {
        out port o;
    }

    part A defined by PD {}
}

package P2 {
    part B {
        in port i;
        connect P1::A.o to i;
    }
}

In this case the .referenced_feature_target of the connection source points to P1::PD::o, so this approach fails to tell me that out port o of specifically part P1::A is being connected. I’m not very familiar with the underlying concepts of SysMLv2, so correct me if I’m wrong, but my impression is that part A inherits all attributes of part def PD, so it makes sense to talk about port P1::A::o being connected to port P2::B::i

The problem is that A does not have an actual model element for the inherited port that can be linked to.

Therefore you need either to redefine the port in A or track the fact that the feature chain is “inside A” separately.

We plan to work on this as part of the work imlroving the querying/evaluation API, but are not quite there yet.

2 Likes