Skip to content

petri_net_nn.pnml

pnml

PNML (Petri Net Markup Language) import and export.

PNML is the ISO/IEC 15909-2 interchange format for Petri nets. Adding import / export here connects PETRA to the established Petri-net tool ecosystem — CPN Tools, GreatSPN, TINA, Snoopy, ProM, and many others all emit or consume PNML.

This module supports the P/T net subset of PNML 2009:

  • one or more <net> elements (the first net is parsed; others are ignored on import);
  • places, transitions, arcs nested under <page> elements (the parser flattens all pages into a single net, which is the standard interpretation when no page-level semantics are needed);
  • names (via <name><text>...</text></name>);
  • initial markings (<initialMarking><text>N</text></initialMarking>);
  • arc inscriptions for arc weights (<inscription><text>N</text></inscription>);
  • inhibitor arcs via the common <arctype><text>inhibitor</text></arctype> extension recognised by Snoopy, GreatSPN and others.

PETRA-specific extensions that have no standard PNML representation — transition durations, firing rates, guards, arc output values — are dropped on export and ignored on import. The structural net (places, transitions, flow, weights, initial marking, inhibitor arcs) round-trips cleanly.

parse_pnml

parse_pnml(source)

Parse a PNML document into a PETRA PetriNet.

The first <net> element in the document is used; additional nets (PNML allows multiple) are ignored. All pages within that net are flattened — places, transitions, and arcs are collected regardless of which page they live on. Anything PETRA doesn't understand (graphics, tool-specific extensions, colour declarations) is silently dropped.

Source code in petri_net_nn/pnml.py
def parse_pnml(source: str | Path | IO[str] | IO[bytes]) -> PetriNet:
    """Parse a PNML document into a PETRA ``PetriNet``.

    The first ``<net>`` element in the document is used; additional
    nets (PNML allows multiple) are ignored. All pages within that
    net are flattened — places, transitions, and arcs are collected
    regardless of which page they live on. Anything PETRA doesn't
    understand (graphics, tool-specific extensions, colour
    declarations) is silently dropped."""
    root = _load_xml(source)
    if _local(root.tag) != "pnml":
        raise ValueError(
            f"expected PNML root element <pnml>, got <{_local(root.tag)}>"
        )

    net_elem = next((c for c in root if _local(c.tag) == "net"), None)
    if net_elem is None:
        raise ValueError("no <net> element found inside <pnml>")

    net = PetriNet()

    # PNML allows places / transitions / arcs to appear either directly
    # under <net> or under nested <page> elements. We walk recursively
    # and pick up everything we recognise, regardless of nesting depth.
    def walk(element: ET.Element) -> None:
        for child in element:
            tag = _local(child.tag)
            if tag == "place":
                _read_place(child, net)
            elif tag == "transition":
                _read_transition(child, net)
            elif tag == "arc":
                _read_arc(child, net)
            elif tag == "page":
                walk(child)

    walk(net_elem)

    return net

to_pnml

to_pnml(net, *, net_id='net1', name=None)

Serialise a PETRA PetriNet as a PNML 2009 P/T net document.

Includes places, transitions, arcs, arc weights (as PNML inscriptions), initial markings, place / transition names, and inhibitor arcs (via the de-facto <arctype>inhibitor</arctype> extension). PETRA-specific extensions without a standard PNML representation — transition durations, firing rates, guards, arc output values — are dropped. The export is a string of UTF-8 XML, suitable for writing to a .pnml file or piping into any PNML-aware tool.

Source code in petri_net_nn/pnml.py
def to_pnml(net: PetriNet, *, net_id: str = "net1", name: str | None = None) -> str:
    """Serialise a PETRA ``PetriNet`` as a PNML 2009 P/T net document.

    Includes places, transitions, arcs, arc weights (as PNML
    inscriptions), initial markings, place / transition names, and
    inhibitor arcs (via the de-facto ``<arctype>inhibitor</arctype>``
    extension). PETRA-specific extensions without a standard PNML
    representation — transition durations, firing rates, guards,
    arc output values — are dropped. The export is a string of
    UTF-8 XML, suitable for writing to a ``.pnml`` file or piping
    into any PNML-aware tool."""
    pnml = ET.Element(f"{NS_PREFIX}pnml")
    net_elem = ET.SubElement(
        pnml, f"{NS_PREFIX}net", attrib={"id": net_id, "type": PT_NET_TYPE}
    )
    if name is not None:
        _write_label(net_elem, "name", name)

    # PNML conventionally wraps the contents in a single <page>;
    # some tools require it. Keep things on one page since PETRA
    # has no notion of pagination.
    page = ET.SubElement(net_elem, f"{NS_PREFIX}page", attrib={"id": "page1"})

    for place in sorted(net.places):
        place_elem = ET.SubElement(
            page, f"{NS_PREFIX}place", attrib={"id": place}
        )
        label = net.place_labels.get(place)
        if label is not None:
            _write_label(place_elem, "name", label)
        tokens = net.initial_marking.get(place, 0)
        if tokens:
            _write_label(place_elem, "initialMarking", str(tokens))

    for transition in sorted(net.transitions):
        t_elem = ET.SubElement(
            page, f"{NS_PREFIX}transition", attrib={"id": transition}
        )
        label = net.transition_labels.get(transition)
        if label is not None:
            _write_label(t_elem, "name", label)

    arc_counter = 0
    for src, dst in sorted(net.flow):
        arc_counter += 1
        arc_elem = ET.SubElement(
            page,
            f"{NS_PREFIX}arc",
            attrib={"id": f"arc_{arc_counter}", "source": src, "target": dst},
        )
        weight = net.weight(src, dst)
        if weight != 1:
            _write_label(arc_elem, "inscription", str(weight))

    for src, dst in sorted(net.inhibitor_arcs):
        arc_counter += 1
        arc_elem = ET.SubElement(
            page,
            f"{NS_PREFIX}arc",
            attrib={"id": f"arc_{arc_counter}", "source": src, "target": dst},
        )
        # The arctype element is the conventional way to tag an arc
        # as inhibitor. Plain PNML 2009 doesn't standardise this but
        # the Snoopy / GreatSPN / TINA ecosystem recognises it.
        _write_label(arc_elem, "arctype", "inhibitor")

    ET.register_namespace("", PNML_NS)
    return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
        pnml, encoding="unicode"
    )