Skip to content

API Reference

Python framework for Piping and Instrumentation Diagrams (P&IDs) using the DEXPI information model


Core Components

Loaders

Load DEXPI models from various formats.

DexpiXmlLoader

Load DEXPI XML files into pyDEXPI models.

pydexpi.loaders.DexpiXmlSerializer

DexpiXmlSerializer(id_canonicalization: bool = False, export_metadata: DexpiXmlModelMetadata | None = None, indent: int = 2, strict: bool = True)

Bases: AbstractTreeReferenceSerializer[DexpiXmlDeserializationElement, DexpiXmlSerializationElement]

Serializer for the DEXPI XML tree-reference format.

Carriage returns in string data attributes are written as 
 and survive the round trip unchanged. The remaining C0 control characters -- everything below U+0020 except tab, line feed and carriage return -- have no representation in XML 1.0, as a literal or as a character reference, so export raises ValueError naming the attribute and the offending code point rather than writing a file that cannot be read back. JsonSerializer has no such restriction.

Initialize a new XML serializer.

Parameters:

Name Type Description Default
id_canonicalization bool

Whether to perform ID canonicalization for reference attributes during serialization. If True, list attributes will be sorted by ID. Default is False.

False
export_metadata DexpiXmlModelMetadata | None

Metadata for the DEXPI XML model document. If not provided, default metadata with Core, Plant, and Process imports will be used.

None
indent int

Number of spaces to use for indentation in the output XML. Default is 2.

2
strict bool

Whether an object omitting a mandatory DEXPI attribute is discarded on load. Pass False to load such objects anyway, recording each omission as an error - needed for real process files, which routinely omit the mandatory Identifier. See TreeReferenceLoader.__init__. Default is True.

True

validate_xml_schema classmethod

validate_xml_schema(xml_path: Path, xsd_path: Path) -> None

Validate the XML file against the DEXPI XML Schema.

Parameters:

Name Type Description Default
xml_path Path

Path to the XML file to validate.

required
xsd_path Path

Path to the XSD file to validate against.

required

Raises:

Type Description
DocumentInvalid

If the XML file does not conform to the DEXPI XML Schema.

JsonSerializer

Serialize and deserialize DEXPI models to/from JSON format.

pydexpi.loaders.JsonSerializer

JsonSerializer(id_canonicalization: bool = False, indent: int | None = 4, explicit: bool = False, strict: bool = True, declare_version: bool = True, non_finite: NonFiniteSpelling = 'strings')

Bases: AbstractTreeReferenceSerializer[JsonDeserializationElement, JsonSerializationElement]

JSON implementation of the AbstractTreeReferenceSerializer for DEXPI models.

This class provides JSON-specific implementations of the loader and serializer element factories, enabling serialization and deserialization of DEXPI models to/from JSON format.

Initialize a new JSON serializer.

Parameters:

Name Type Description Default
id_canonicalization bool

Whether to perform ID canonicalization for reference attributes during serialization. If True, list attributes will be sorted by ID. Default is False.

False
indent int

The number of spaces to use for indentation in the JSON output. Default is 4.

4
explicit bool

Whether to include null values in the JSON output. Default is False.

False
strict bool

Whether an object omitting a mandatory DEXPI attribute is discarded on load. Pass False to load such objects anyway, recording each omission as an error -- as for DexpiXmlSerializer. A process model loaded leniently and saved as JSON still omits the attribute, so reading it back needs False too. Default is True.

True
declare_version bool

Whether the documents written declare the DEXPI version, "dexpiVersion": "2.0.1". Only pydexpi.hashing turns it off, so that no hash depends on it. Default is True.

True
non_finite ('strings', 'constants')

How a NaN or an infinity the model holds is written. xsd:double admits both, so a DEXPI document may hold one, and JSON has no number for either. "strings", the default, writes the JSON strings "NaN" / "Infinity" / "-Infinity" (non_finite_token), so everything this serializer writes is JSON a strict parser reads; diff values (pydexpi.loaders.value_codec) are spelled the same way. "constants" writes Python's bare NaN / Infinity / -Infinity, which is what pyDEXPI wrote before and no JSON: a strict parser refuses the whole document. Either spelling is read back as the same number, by any JsonSerializer, because the field it is read into is a number: no generated class has a field that takes both a string and a float. pydexpi.hashing keeps its own serializers on "constants", so no digest depends on this default.

"strings"

Raises:

Type Description
ValueError

If non_finite is neither "constants" nor "strings".

PickleSerializer

Serialize and deserialize DEXPI models to/from Pickle format.

pydexpi.loaders.PickleSerializer

Bases: Serializer

Save a DEXPI model as a pickle, and load one these classes wrote.

A pickle is Python-specific persistence, tied to the pyDEXPI classes that wrote it: it is not a format for exchange, and not one for keeping a model across pyDEXPI or DEXPI versions. Nothing converts it, so load refuses a pickle written by other classes rather than return a model that fails later (pydexpi.loaders.pickle_stamp).

Only load a pickle you trust. Unpickling runs code the file names, so a pickle from somewhere else can do anything your Python process can. The check for other classes is no protection against that: it reads what the file says about itself.

load

load(dir_path: Path, filename: str) -> Any

Load a DEXPI model from a pickle file written by these pyDEXPI classes.

Only for a file you trust: unpickling runs code the file names.

Parameters:

Name Type Description Default
dir_path Path

Directory where the pickle file is stored.

required
filename str

Filename of the pickle file.

required

Returns:

Type Description
DexpiModel

Loaded DEXPI model.

Raises:

Type Description
IncompatiblePickleError

If the pickle was written by other classes: its stamp names another DEXPI version, or a class that is gone or whose fields changed; or, for a pickle without a stamp (one an earlier pyDEXPI wrote), an object in it does not hold the fields of its class. Read the model from its DEXPI XML or JSON source instead, and pickle it again.

save

save(model: PyDexpiBaseModel, dir_path: Path, filename: str) -> None

Saves a DEXPI model to pickle.

The file is a plain pickle of model, which pickle.load reads as it is, followed by a stamp naming the DEXPI version, the pyDEXPI version and the classes it holds, which load checks.

Parameters:

Name Type Description Default
model PyDexpiBaseModel

DEXPI model that should be saved.

required
dir_path Path

Directory where the DEXPI model should be saved.

required
filename str

Filename for the saved DEXPI model.

required

load raises this for a pickle written by other pyDEXPI classes (see Pickle Format):

pydexpi.loaders.IncompatiblePickleError

Bases: ValueError

A pickle cannot be read back by these classes: it was written by other ones.

A pickle is tied to the classes that wrote it and is never converted. Read the model from its DEXPI XML or JSON source, which pyDEXPI converts from an earlier DEXPI version, and pickle it again.

GraphLoader

Convert DEXPI models to NetworkX graphs and manipulate graph structures.

pydexpi.loaders.GraphLoader

Bases: AbstractGraphLoader

Loads a DEXPI model instance into a NetworkX MultiDiGraph.

dexpi_to_graph

dexpi_to_graph(dexpi_model: EngineeringModel) -> MultiDiGraph

Parse a pyDEXPI model into a NetworkX graph representation.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The pyDEXPI model to be parsed into a NetworkX graph.

required

Returns:

Type Description
MultiDiGraph

The parsed NetworkX graph representation of the pyDEXPI model.

graph_to_dexpi

graph_to_dexpi(graph: MultiDiGraph) -> EngineeringModel

Parsing the example graph back to pyDEXPI is not supported yet.

FormatConverter

Convert directly between tree-reference-backed formats (DEXPI XML, JSON) without building a pyDEXPI model. Faster than loading and re-saving, but skips pydantic validation and error reporting.

pydexpi.loaders.tree_ref_base.FormatConverter

FormatConverter(serialization_element_factory: Callable[[], OutputSerializationElement], dt_serialization_element_factory: Callable[[], OutputDataTypeSerializationElement] | None = None)

Convert between tree-reference-backed serialization formats directly.

This utility uses the SerializationElement and DeserializationElement interfaces to transform pyDEXPI tree-reference-backed formats without direct parsing to pyDEXPI models.

Attribute order is preserved as found in the source. Unlike TreeReferenceExporter, this converter has no id_canonicalization option and never sorts list-valued attributes, so output ordering follows the input rather than a canonical form.

Since the conversion bypasses pyDEXPI models, it also bypasses pydantic validation and the loader's ErrorRegistry. A DEXPI class name that is unknown to pyDEXPI raises an AttributeError and aborts the conversion instead of being logged and skipped. So does a source element that left part of its structure out (StructureReportingElement): the loader would register that part with its content, and here there is no registry to hold it, so converting on would drop it without a word. It raises a ValueError naming the part.

Attributes:

Name Type Description
serialization_element_factory Callable[[], OutputSerializationElement]

Factory function to create new instances of the target serialization element type.

dt_serialization_element_factory Callable[[], OutputDataTypeSerializationElement]

Factory function to create new instances of the target data type serialization element type.

Initialize the format converter with the given serialization element factories.

Parameters:

Name Type Description Default
serialization_element_factory Callable[[], OutputSerializationElement]

Factory function to create new instances of the target serialization element type.

required
dt_serialization_element_factory Callable[[], OutputDataTypeSerializationElement]

Factory function to create new instances of the target data type serialization element type. If not provided, the serialization_element_factory will be used for data type elements as well.

None
Source code in pydexpi/loaders/tree_ref_base/format_converter.py
def __init__(
    self,
    serialization_element_factory: Callable[[], OutputSerializationElement],
    dt_serialization_element_factory: Callable[[], OutputDataTypeSerializationElement]
    | None = None,
) -> None:
    """Initialize the format converter with the given serialization element factories.

    Parameters
    ----------
    serialization_element_factory : Callable[[], OutputSerializationElement]
        Factory function to create new instances of the target serialization element type.
    dt_serialization_element_factory : Callable[[], OutputDataTypeSerializationElement], optional
        Factory function to create new instances of the target data type serialization element type.
        If not provided, the `serialization_element_factory` will be used for data type elements as well.
    """
    self.serialization_element_factory = serialization_element_factory
    self.dt_serialization_element_factory = (
        dt_serialization_element_factory
        if dt_serialization_element_factory is not None
        else serialization_element_factory
    )

convert_deser_to_ser

convert_deser_to_ser(deser_element: DeserializationElement) -> OutputSerializationElement

Convert a deserialization element to a serialization element.

The concrete serialization element type produced is determined by the serialization_element_factory provided during initialization.

Parameters:

Name Type Description Default
deser_element DeserializationElement

The deserialization element to convert.

required

Returns:

Type Description
OutputSerializationElement

The converted serialization element.

Source code in pydexpi/loaders/tree_ref_base/format_converter.py
def convert_deser_to_ser(
    self, deser_element: DeserializationElement
) -> OutputSerializationElement:
    """Convert a deserialization element to a serialization element.

    The concrete serialization element type produced is determined by the
    `serialization_element_factory` provided during initialization.

    Parameters
    ----------
    deser_element : DeserializationElement
        The deserialization element to convert.

    Returns
    -------
    OutputSerializationElement
        The converted serialization element.
    """
    self._refuse_unread_structure(deser_element)
    new_ser_element = self.serialization_element_factory()

    # Set ID
    new_ser_element.set_id(deser_element.get_id())

    # Set dexpi class
    dexpi_class_name = deser_element.get_dexpi_class()

    base_class, uri = self._base_class_and_uri(dexpi_class_name, deser_element)
    new_ser_element.set_dexpi_class(base_class, dexpi_class_name, uri)

    # Set compositional attributes
    ser_comp_attrs = {}
    deser_comp_attrs = deser_element.get_composition_attributes()
    for attr_name, attr_value in deser_comp_attrs.items():
        ser_comp_attrs[attr_name] = call_on_list_or_object_or_none(
            self.convert_deser_to_ser, attr_value
        )

    new_ser_element.set_comp_attributes(ser_comp_attrs)

    # Set reference attributes (directly, since they are just ID references)
    new_ser_element.set_reference_pointers(dict(deser_element.get_reference_attributes()))

    # Set data attributes
    ser_data_attrs = {}
    deser_data_attrs = deser_element.get_data_attributes()
    for attr_name, attr_value in deser_data_attrs.items():
        ser_data_attrs[attr_name] = call_on_list_or_object_or_none(
            self.dt_deser_element_to_dt_ser_element, attr_value
        )
    new_ser_element.set_data_attributes(ser_data_attrs)

    return new_ser_element

dt_deser_element_to_dt_ser_element

dt_deser_element_to_dt_ser_element(dt_deser_element: DataTypeDeserializationElement | DeserializerDataFieldType) -> OutputDataTypeSerializationElement | SerializerDataFieldType

Convert a data type deserialization element to a data type serialization element.

The concrete serialization element type produced is determined by the dt_serialization_element_factory provided during initialization.

Parameters:

Name Type Description Default
dt_deser_element DataTypeDeserializationElement | DeserializerDataFieldType

The data type deserialization element to convert.

required

Returns:

Type Description
OutputDataTypeSerializationElement | SerializerDataFieldType

The converted data type serialization element.

Source code in pydexpi/loaders/tree_ref_base/format_converter.py
def dt_deser_element_to_dt_ser_element(
    self, dt_deser_element: DataTypeDeserializationElement | DeserializerDataFieldType
) -> OutputDataTypeSerializationElement | SerializerDataFieldType:
    """Convert a data type deserialization element to a data type serialization element.

    The concrete serialization element type produced is determined by the
    `dt_serialization_element_factory` provided during initialization.

    Parameters
    ----------
    dt_deser_element : DataTypeDeserializationElement | DeserializerDataFieldType
        The data type deserialization element to convert.

    Returns
    -------
    OutputDataTypeSerializationElement | SerializerDataFieldType
        The converted data type serialization element.
    """
    if isinstance(dt_deser_element, DataTypeDeserializationElement):
        self._refuse_unread_structure(dt_deser_element)
        new_dt_ser_element = self.dt_serialization_element_factory()

        # Set dexpi class
        dexpi_class_name = dt_deser_element.get_dexpi_class()
        base_class, uri = self._base_class_and_uri(dexpi_class_name, dt_deser_element)
        new_dt_ser_element.set_dexpi_class(base_class, dexpi_class_name, uri)

        # Set data attributes
        ser_data_attrs = {}
        deser_data_attrs = dt_deser_element.get_data_attributes()
        for attr_name, attr_value in deser_data_attrs.items():
            ser_data_attrs[attr_name] = call_on_list_or_object_or_none(
                self.dt_deser_element_to_dt_ser_element, attr_value
            )
        new_dt_ser_element.set_data_attributes(ser_data_attrs)
        return new_dt_ser_element

    else:
        return dt_deser_element  # For primitive types, return as is

Error Registry

Problems encountered while loading a document, including where in the source they came from. See Loading Errors for how to read them.

pydexpi.loaders.error_registry.ErrorRegistryEntry dataclass

ErrorRegistryEntry(message: str, level: ErrorLevels, object_id: str | None = None, exception: Exception | None = None, element: str | None = None, xpath: str | None = None, line: int | None = None, column: int | None = None, content: str | None = None)

Data class representing an error entry in the error registry.

The element, xpath, line and column fields locate the error in the source document. They are populated on a best-effort basis: a loader fills in whatever its format can provide, and leaves the rest as None. Errors that do not originate from a specific element of the source document (loader-level errors, for instance) legitimately leave all four unset.

Parameters:

Name Type Description Default
message str

Human-readable error message

required
level ErrorLevels

Severity level of the error

required
object_id str or None

pyDEXPI object ID associated with the error if available, by default None

None
exception Exception or None

The exception that caused the error, if applicable, by default None

None
element str or None

Name of the offending element, e.g. the XML tag "Nozzle" or a DEXPI class name. This is a name, never an element object, by default None

None
xpath str or None

Path of the offending element within the source document, e.g. "/PlantModel/Equipment[2]/Nozzle[1]" for XML or a JSON Pointer for JSON. This is a structural path into the exact document that was parsed: it is position dependent and is not a stable identifier across edits of that document, by default None

None
line int or None

1-based line number in the source document, by default None

None
column int or None

1-based column number in the source document. Only available for well-formedness errors, as XML parsers do not report a column per element, by default None

None
content str or None

What the entry is about, when the load does not keep it, serialized in the document's format so the data stays recoverable from the report: an object dropped because DEXPI 2.0.1 gives it no owner, a value the 2.0.1 model cannot hold, or repeated <Data> values the loader did not read. It is the text as it stood when dropped -- for a DEXPI 2.0 document, after its conversion to 2.0.1, so with the renames applied (MoleFractions, RatioUnit) -- not the source document's own spelling, by default None

None

pydexpi.loaders.error_registry.SourceLocation dataclass

SourceLocation(element: str | None = None, xpath: str | None = None, line: int | None = None, column: int | None = None)

Where in a source document an error originated.

This is the same information as the location fields of ErrorRegistryEntry, bundled up so that format-specific readout logic can return it in one piece. Helpers that derive it from concrete documents live in pydexpi.loaders.source_location.

Parameters:

Name Type Description Default
element str or None

Name of the offending element, e.g. the XML tag "Nozzle" or a DEXPI class name. This is always a name, never an element object, by default None

None
xpath str or None

Path of the offending element within the source document, by default None

None
line int or None

1-based line number in the source document, by default None

None
column int or None

1-based column number in the source document, by default None

None

pydexpi.loaders.error_registry.ErrorRegistry

ErrorRegistry()

Registry for collecting and managing errors during serialization.

This class provides a centralized way to collect, store, and retrieve errors that occur during the parsing and serialization process.

Initialize an empty error registry.

get_errors

get_errors(levels: list[ErrorLevels] | None = None) -> list[ErrorRegistryEntry]

Retrieve all registered errors.

Parameters:

Name Type Description Default
levels list[ErrorLevels] or None

If provided, filter errors by their severity level. If None, return all errors. Defaults to None.

None

Returns:

Type Description
list[ErrorRegistryEntry]

List of all error entries in the registry

register_error

register_error(message: str, level: ErrorLevels | Literal['info', 'warning', 'error', 'critical'], object_id: str | None = None, exception: Exception | None = None, element: str | None = None, xpath: str | None = None, line: int | None = None, column: int | None = None, content: str | None = None) -> None

Convenience method to create and register an error entry.

This method allows for quick registration of errors by providing the necessary information directly as parameters. It creates an ErrorRegistryEntry and registers it.

Parameters:

Name Type Description Default
message str

Human-readable error message

required
level ErrorLevels or str

Severity level of the error. A string is converted to the matching ErrorLevels member, so that an entry reports the same type whichever loader registered it.

required
object_id str or None

pyDEXPI object ID associated with the error if available, by default None

None
exception Exception or None

The exception that caused the error, if applicable, by default None

None
element str or None

Name of the offending element in the source document, by default None

None
xpath str or None

Path of the offending element within the source document, by default None

None
line int or None

1-based line number in the source document, by default None

None
column int or None

1-based column number in the source document, by default None

None
content str or None

What was dropped, when the load does not keep it, serialized as it stood when dropped (for a DEXPI 2.0 document, after its conversion to 2.0.1), by default None

None

Toolkits

Utilities for working with DEXPI models programmatically.

Model Toolkit

pydexpi.toolkits.model_toolkit

Engineering Model Toolkit

This module contains functions and tools that concern overall of an overall engineering model/conceptual model.

ModelT module-attribute

ModelT = TypeVar('ModelT', bound=PyDexpiBaseModel)

_ANY_VALUE module-attribute

_ANY_VALUE: Any = object()

EngineeringModel

Bases: PyDexpiBaseModel

An entire DEXPI engineering model.

PlantModel

Bases: ConceptualModel

PyDexpiBaseModel

Bases: ABC, BaseModel

Base Model for the classes created for the dexpi data model, containing some overarching functionality

_detach_from_composition_parents

_detach_from_composition_parents(all_instances: list[PyDexpiBaseModel], dexpi_objects: Iterable[PyDexpiBaseModel]) -> None

Detach each of dexpi_objects from whichever instance composes it.

Shared by remove_object_from_model and remove_children_from_model so the two cannot drift. Matching is identity-based, and each object is detached at most once: the scan returns as soon as every object has been accounted for, which for a single object reproduces the "stop at the first composition parent" behaviour that remove_object_from_model documents.

Parameters:

Name Type Description Default
all_instances list[PyDexpiBaseModel]

The model's instances, already materialized by the caller so that a batch costs one traversal rather than one per object.

required
dexpi_objects Iterable[PyDexpiBaseModel]

The objects to detach from their composition parents.

required

clone_model

clone_model(the_model: ModelT) -> ModelT

Creates a deep copy of the model and generates new IDs for all instances in the model.

Parameters:

Name Type Description Default
the_model ModelT

The model to be cloned.

required

Returns:

Type Description
ModelT

A deep copy of the model with new IDs for all instances.

combine_engineering_models

combine_engineering_models(models: list[EngineeringModel], engineering_model_kwargs: dict | None = None, plant_model_kwargs: dict | None = None) -> EngineeringModel

This function combines the contents of a list of engineering models and returns a new model with its contents.

The combination is performed on all list attributes of the plant model. The data attributes of the engineering model and the plant model can be passed in via kwargs.

Parameters:

Name Type Description Default
models list[EngineeringModel]

The list of engineering models to be combined.

required
engineering_model_kwargs dict

The keyword arguments for the engineering model. This can be used to set data attributes of the engineering model. By default, an empty dict is used.

None
plant_model_kwargs dict

The keyword arguments for the plant model. This can be used to set data attributes of the plant model. By default, an empty dict is used.

None

Returns:

Type Description
EngineeringModel

The combined engineering model.

Raises:

Type Description
NotImplementedError

Combining diagram information is not yet implemented. If any model has diagram or shapeCatalog attributes that are not None, Error is raised.

get_all_instances_in_model

get_all_instances_in_model(the_model: PyDexpiBaseModel, dexpi_classes: tuple[PyDexpiBaseModel] | None = None) -> list[PyDexpiBaseModel]

Recursively discover and collect all instances of specified classes within a model.

This function traverses the attributes of the provided model object, recursively inspecting its attributes and sub-attributes to find all instances of the specified classes. The function returns a list of all discovered instances.

Parameters:

Name Type Description Default
the_model PyDexpiBaseModel

The root model object to inspect.

required
dexpi_classes class or tuple of classes

A tuple of class types to discover within the model. Only instances of these classes will be collected. If None, all objects are collected.

None

Returns:

Type Description
list

A list of all discovered instances of the specified classes within the model.

get_instances_by_ids

get_instances_by_ids(the_model: PyDexpiBaseModel, ids: Iterable[str]) -> dict[str, PyDexpiBaseModel]

Resolve DEXPI ids to the live objects carrying them, in one traversal.

An id that no object in the model carries is simply absent from the result. That is the useful answer rather than an error: the caller is typically working from a record of what a change touched, and an object it removed is legitimately gone.

Parameters:

Name Type Description Default
the_model PyDexpiBaseModel

The model to search. Its whole composition tree is walked once.

required
ids Iterable[str]

The ids to look for.

required

Returns:

Type Description
dict[str, PyDexpiBaseModel]

The requested ids that were found, mapped to their objects.

Raises:

Type Description
ValueError

If a requested id is carried by more than one object. The request is genuinely ambiguous, and returning whichever one happened to win would be silent: ModelCompositionTreeUnique does report two distinct objects sharing an id, but it is model-scoped, so the scoped validation this lookup serves (ValidationEngine.validate_objects) does not run it, and nothing downstream would notice the wrong object had been validated. Ids duplicated elsewhere in the model are not this function's business and are not reported.

get_instances_with_attribute

get_instances_with_attribute(the_model: PyDexpiBaseModel, attribute_name: str, target_value: Any = _ANY_VALUE, dexpi_classes: tuple[PyDexpiBaseModel] | None = None) -> list[PyDexpiBaseModel]

Retrieve all subinstances in a engineering model with a specific attribute.

The engineering model is searched recursively through compositional attributes of all levels. The attribute name is specified as a string. If the target_value is not specified, any value is permitted. If the target_value is specified, only instances with that value are returned. The dexpi_classes parameter can be used to specify the classes to be considered. If None, all classes are considered.

Parameters:

Name Type Description Default
the_model PyDexpiBaseModel

The model to be searched.

required
attribute_name str

The name of the attribute to be identified.

required
target_value Any

The value this attribute is supposed to have. If not specified any value is permitted.

_ANY_VALUE
dexpi_classes tuple

Can specify the classes if only certain class types are to be considered. If None, all are considered, by default None.

None

Returns:

Type Description
list

List of discovered instances that have the attribute specified.

get_instances_with_condition

get_instances_with_condition(the_model: PyDexpiBaseModel, condition: Callable[[PyDexpiBaseModel], bool], dexpi_classes: tuple[PyDexpiBaseModel] | None = None) -> list[PyDexpiBaseModel]

Retrieve all subinstances in a engineering model that satisfy a specific condition.

Recursively searches through the model's attributes and sub-attributes to find all instances that satisfy the given condition. The condition is specified as a callable that takes a PyDexpiBaseModel instance as an argument and returns a boolean value.

Parameters:

Name Type Description Default
the_model PyDexpiBaseModel

The model to be searched.

required
condition Callable[[PyDexpiBaseModel], bool]

The condition that the instances must satisfy.

required
dexpi_classes tuple

Can specify the classes if only certain class types are to be considered. If None, all are considered, by default None.

None

Returns:

Type Description
list

List of discovered instances that satisfy the condition.

get_objects_referencing

get_objects_referencing(dexpi_model: EngineeringModel, dexpi_objects: Iterable[PyDexpiBaseModel]) -> list[tuple[PyDexpiBaseModel, str]]

Find every reference attribute in the model that points at one of dexpi_objects.

Returns (holder, attribute_name) pairs: the object holding the reference and the attribute it holds it in. A holder appears once per matching attribute.

DEXPI references are one-directional -- an ActuatingSystem points at the valve it operates, the valve points at nothing -- so answering "what refers to this?" means scanning. The model is traversed exactly once regardless of how many objects are passed, and matching is identity-based (is), never by id field or equality, so distinct objects sharing an id do not alias. Only objects reachable from dexpi_model through composition are visited.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to search.

required
dexpi_objects Iterable[PyDexpiBaseModel]

The objects to find references to.

required

Returns:

Type Description
list[tuple[PyDexpiBaseModel, str]]

(referencing object, reference attribute name) pairs, in traversal order, and for one holder in the order its class declares the attributes. The order is deterministic, so a caller can record what it clears in it.

import_model_contents_into_model

import_model_contents_into_model(target_model: EngineeringModel, import_models: list[EngineeringModel]) -> None

Imports the contents of passed models into target model. Conceptually similar to combine_dexpi_models, but the target_model is manipulated in place and its other, non-list attributes are preserved.

Parameters:

Name Type Description Default
target_model EngineeringModel

The model that is manipulated in place and that the other models are imported into.

required
import_models list[EngineeringModel]

The models that are to be imported to the target.

required

Raises:

Type Description
NotImplementedError

Combining diagram information is not yet implemented. If any model has diagram or shapeCatalog attributes that are not None, Error is raised.

make_id

make_id() -> str

Generate a new unique ID for a pyDEXPI object.

object_is_in_model

object_is_in_model(dexpi_model: EngineeringModel, dexpi_object: PyDexpiBaseModel) -> bool

Checks if the given dexpi_object is in some way a member of the given dexpi_model.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the object.

required
dexpi_object PyDexpiBaseModel

The object to be searched for in the model.

required

Returns:

Type Description
bool

True if object is found in the model, False otherwise

reassign_ids_in_model

reassign_ids_in_model(the_model: PyDexpiBaseModel) -> None

Reassigns new IDs to all instances in the model.

Parameters:

Name Type Description Default
the_model PyDexpiBaseModel

The model in which to reassign IDs.

required

remove_children_from_model

remove_children_from_model(dexpi_model: EngineeringModel, dexpi_object: PyDexpiBaseModel) -> None

Removes all children of the given dexpi_object from the given dexpi_model.

The children are removed as one batch: the model is traversed a constant number of times regardless of how many children there are. Calling remove_object_from_model per child instead costs two full-model traversals each, a multiplicative cost on a large model.

Batching also makes the outcome independent of iteration order. Removing the children one at a time cleared references held by a later child to an earlier one but not the reverse, because the earlier child was already detached and so no longer reachable when the later one was processed. References between the removed children are now cleared symmetrically.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the children.

required
dexpi_object PyDexpiBaseModel

The object whose children are to be removed from the model.

required

Raises:

Type Description
ValueError

If any child is not found in the model.

remove_external_references

remove_external_references(object: PyDexpiBaseModel) -> None

Removes all references to objects outside of the given object.

This is done by searching through the object's reference attributes to find any references to objects that are not part of the object's composition tree. These references are then removed.

Parameters:

Name Type Description Default
object PyDexpiBaseModel

The object whose external references are to be removed.

required

remove_object_from_model

remove_object_from_model(dexpi_model: EngineeringModel, dexpi_object: PyDexpiBaseModel) -> None

Removes the given dexpi_object from the given dexpi_model.

This is done by searching through the model's compositional and reference attributes to find the object and remove it. This ensures that the object is removed from both the model's compositional tree and the cross-references.

Assumptions

Assumes that the object is in the model's composition tree only once, as it should for a well-formed model. If the object is found in multiple places, only the first occurrence will be removed. Scanning stops as soon as the composition parent is found; in a malformed model with the object in multiple locations, remaining occurrences are left intact.

Note that any references from the removed object to other objects in the model are not removed. This can be done with remove_references_to_object, if desired.

Note also that this covers the generic case. However, for some specific cases, such as removing a pipe in a segment, this can lead to an invalid model.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the object.

required
dexpi_object PyDexpiBaseModel

The object to be removed from the model.

required

Raises:

Type Description
ValueError

If the object is not found in the model.

remove_references_to_children

remove_references_to_children(dexpi_model: EngineeringModel, dexpi_object: PyDexpiBaseModel) -> None

Removes all references to the children of the given dexpi_object from the given dexpi_model.

This is done by searching through the model's reference attributes to find the children and remove them. This ensures that the children are removed from the cross-references. References to dexpi_object itself are kept.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the references.

required
dexpi_object PyDexpiBaseModel

The object whose children's references are to be removed from the model.

required

remove_references_to_object

remove_references_to_object(dexpi_model: EngineeringModel, dexpi_object: PyDexpiBaseModel) -> None

Removes all references to the given dexpi_object from the given dexpi_model.

This is done by searching through the model's reference attributes to find the object and remove it. This ensures that the object is removed from the cross-references. References to the compositional descendants of dexpi_object are removed as well, in the same single model traversal.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the references.

required
dexpi_object PyDexpiBaseModel

The object whose references are to be removed from the model.

required

remove_references_to_objects

remove_references_to_objects(dexpi_model: EngineeringModel, dexpi_objects: Iterable[PyDexpiBaseModel]) -> None

Removes all references to any of the given dexpi_objects from the given dexpi_model.

The model is traversed exactly once, regardless of how many objects are passed, and every reference attribute is checked against the batch with an O(1) membership test. Matching is identity-based (is), never by id field or equality, so distinct objects that happen to share an id do not alias.

Scalar references to a matched object are set to None; this raises a pydantic ValidationError if the reference attribute is required. List references are filtered. Attributes without a match are left untouched (no reassignment, so no validation pass). Only objects reachable from dexpi_model through composition are visited: references held by already-detached subtrees are not cleared.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the references.

required
dexpi_objects Iterable[PyDexpiBaseModel]

The objects whose references are to be removed from the model.

required

replace_object_in_model

replace_object_in_model(dexpi_model: EngineeringModel, old_object: PyDexpiBaseModel, new_object: PyDexpiBaseModel, replace_obj_references: bool = True) -> None

Replaces the given old_object with the new_object in the given dexpi_model.

This is done by searching through the model's compositional and reference attributes to find the old object and replace it with the new object. This ensures that the object is replaced in both the model's compositional tree and the cross-references.

Assumptions

Assumes that the old object is in the model's composition tree only once, as it should for a well-formed model. If the old object is found in multiple places, only the first occurrence will be replaced.

Note that any references from the old object to other objects in the model are not removed or updated. These can be removed with remove_references_to_object, if desired.

Note also that any nested references are not updated, as not to assume that the new object has the same nested structure as the old object. Instead, these references are removed. Nested references need to be updated manually on children of the new object, if desired.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to be searched for the old object.

required
old_object PyDexpiBaseModel

The object to be replaced in the model.

required
new_object PyDexpiBaseModel

The object to replace the old object with.

required
replace_obj_references bool

If True, the new object will be assigned the old object's references. If False, the new object will not be assigned the old object's references. By default, True.

True

Raises:

Type Description
ValueError

If the old object is not found in the model.

resolve_class_by_name

resolve_class_by_name(module, class_name: str, base_class)

Return the class named class_name from module, validated as a subclass of base_class. Raises ValueError on failure.

Safe against arbitrary code execution: getattr only returns names already present in the module namespace, and the issubclass check restricts the result to the expected DEXPI class hierarchy.

Equipment Toolkit

pydexpi.toolkits.equipment_toolkit

Utility functions for DEXPI equipment classes.

InternalSlot dataclass

InternalSlot(declared: tuple[type[PyDexpiBaseModel], ...], accepts: tuple[type[PyDexpiBaseModel], ...], is_list: bool)

One composition attribute that holds equipment internals, as :func:internal_slots reads it.

Attributes:

Name Type Description
declared tuple[type[PyDexpiBaseModel], ...]

The classes the generated annotation names, such as Chamber or MotorAsComponent.

accepts tuple[type[PyDexpiBaseModel], ...]

Every equipment internal class the attribute takes: the declared classes and their subclasses, AlternatingCurrentMotorAsComponent for motors among them.

is_list bool

Whether the attribute is a list (chambers) or holds one object (tubeBundle).

Nozzle

Bases: ConceptualObject, ActuatingElectricalLocation, SensingLocation, PipingNodeOwner, PipingSourceItem, PipingTargetItem

A projecting short piece of pipe that is attached to another (hollow) body.

ProcessEquipment

Bases: ChamberOwner, NozzleOwner, TaggedPlantItem, TransmissionDriver

An apparatus or machine.

PyDexpiBaseModel

Bases: ABC, BaseModel

Base Model for the classes created for the dexpi data model, containing some overarching functionality

TaggedPlantItem

Bases: ConceptualObject, TechnicalItem

A fully tagged item in a plant.

_equipment_internal_classes cached

_equipment_internal_classes() -> tuple[type[PyDexpiBaseModel], ...]

:func:get_equipment_internal_classes, computed once: the generated classes are fixed.

_internal_class_set cached

_internal_class_set() -> frozenset[type]

_internal_slots cached

_internal_slots(owner_cls: type[PyDexpiBaseModel]) -> dict[str, InternalSlot]

:func:internal_slots, computed once per class. Never hand this dict out: it is shared.

_is_list

_is_list(annotation: object) -> bool

Whether an annotation is a list, or a union holding one.

_model_classes

_model_classes(annotation: object) -> list[type]

The model classes an annotation names, through X | None and list[X].

get_composition_attributes_from_class

get_composition_attributes_from_class(dexpi_class: type[PyDexpiBaseModel | PyDexpiDataTypeBaseModel]) -> set[str]

Return names of composition attributes for a DEXPI class.

Parameters:

Name Type Description Default
dexpi_class type[PyDexpiBaseModel | PyDexpiDataTypeBaseModel]

The class to inspect.

required

Returns:

Type Description
set[str]

Set of field names categorized as composition attributes.

get_equipment_internal_classes

get_equipment_internal_classes() -> list[type[PyDexpiBaseModel]]

Return all equipment internal component classes.

Equipment internals are classes that:

  1. Belong to the package Plant.ProcessEquipment
  2. Do NOT inherit from TaggedPlantItem
  3. Exclude Nozzle explicitly.
  4. Do NOT include superclasses of ProcessEquipment

The list holds ColumnSection and Vent, which classes outside it derive from: the top-level item TaggedColumnSection and the piping component VentLine. An isinstance test against the list is True for both. Ask :func:is_equipment_internal whether an object is an internal.

Returns:

Type Description
list[type[PyDexpiBaseModel]]

Sorted list of equipment internal component class objects; a new list on every call.

get_inheritance_from_dexpi_class

get_inheritance_from_dexpi_class(source: type[PyDexpiBaseModel]) -> list[type]

Get inheritance chain from a PyDexpiBaseModel class.

This method extracts the inheritance hierarchy from a PyDexpiBaseModel class, returning the class types in the inheritance chain.

Parameters:

Name Type Description Default
source type[PyDexpiBaseModel]

A PyDexpiBaseModel class (not an instance)

required

Returns:

Type Description
list[type]

List of class types in the inheritance chain, excluding base classes like PyDexpiBaseModel, ABC, and BaseModel

internal_slots

internal_slots(owner_cls: type[PyDexpiBaseModel]) -> dict[str, InternalSlot]

The composition attributes of owner_cls that hold equipment internals, in field order.

Read from the resolved annotations: generated fields carry string forward references such as 'Chamber | None', which model_fields leaves unresolved, so they go through typing.get_type_hints. Reference attributes never count -- Vessel.columnSections points at TaggedColumnSections, while ProcessColumn.columnSections composes its SubTaggedColumnSections.

Across the generated classes that is 25 attributes on 105 owner classes, 17 of them lists, and each pair of an owner class and an internal class that fit at all fit exactly one attribute, so :func:slot_for needs no trial assignment.

Parameters:

Name Type Description Default
owner_cls type[PyDexpiBaseModel]

A generated class: equipment, or an internal that holds internals itself, such as SubTaggedColumnSection or TransmissionSystem.

required

Returns:

Type Description
dict[str, InternalSlot]

Field name to slot; empty for a class that holds no internals. A new dict on every call; the reading behind it is done once per class.

is_equipment_internal

is_equipment_internal(obj_or_cls: object) -> bool

Whether an object, or a class, is an equipment internal: a chamber, an impeller, ...

An instance of exactly one of the classes :func:get_equipment_internal_classes lists, by membership and not issubclass. That list is every such class of Plant.ProcessEquipment, so a subclass outside it comes from elsewhere, and two do, each a different thing that a subclass test would take for an internal:

  • TaggedColumnSection derives from ColumnSection and from TaggedPlantItem. It is a top-level item in taggedPlantItems.
  • VentLine derives from Vent and from PipingComponent. It is an item of a piping segment.

Parameters:

Name Type Description Default
obj_or_cls object

A model object, or a class.

required

Returns:

Type Description
bool

True for the 41 internals of the corpus (26 chambers, the column internals arrangements, the SubTaggedColumnSections, ...), False for a TaggedColumnSection, a VentLine, a nozzle, equipment and anything else.

slot_for

slot_for(owner_cls: type[PyDexpiBaseModel], internal_cls: type[PyDexpiBaseModel]) -> str | None

The composition attribute of owner_cls that takes an internal_cls, or None.

A function of the two classes alone: no owner class has two attributes that take the same internal class (see :func:internal_slots).

Parameters:

Name Type Description Default
owner_cls type[PyDexpiBaseModel]

The class of the object the internal would go into.

required
internal_cls type[PyDexpiBaseModel]

An equipment internal class. ColumnSection and Vent fit no attribute of any class, and anything that is not an internal fits none either.

required

Returns:

Type Description
str | None

The field name, such as "chambers" or "tubeBundle".

Piping Toolkit

pydexpi.toolkits.piping_toolkit

Dexpi Piping Toolkit

This module contains functions and tools for the main piping concepts of a dexpi model, mainly reading and checking piping network segments.

logger module-attribute

logger = logging.getLogger(__name__)

DexpiConnectionException

Bases: Exception

Exception for dexpi piping, connection-related errors.

Raised if a connection or reconnection is attempted that is invalid or may cause corrupt segments

DexpiCorruptPipingSegmentException

Bases: Exception

Exception raised if a segment is encountered that violates piping network segment convention.

EngineeringModel

Bases: PyDexpiBaseModel

An entire DEXPI engineering model.

PipingConnectionConvention

Bases: Enum

Convention enumeration for creating connections for higher level piping tools where explicit assignment of source and target nodes/items becomes impractical.

Attributes:

Name Type Description
IN_NODE_0_OUT_NODE_1 int = 0

Node with index 0 is used as main inlet, node with index one is main outlet

USE_ITEMS int = 1

Use direct assignments to items

PipingTraversalException

Bases: Exception

Raised if an error occurs during traversal of connected piping components/items/segments.

PipingValidityCode

Bases: Enum

Enumeration of validity codes for piping related objects.

Attributes:

Name Type Description
VALID int = 0
INTERNAL_VIOLATION int = 1

Violation code for an internal violation in a pipingNetworkSegment. Examples are unconnected, loose pipes or items, interrupted segments.

CONNECTION_CONVENTION_VIOLATION int = 2

Violation code if the connection convention regarding a pipingNetworkSegment and its pipingConnections is violated

MULTIPLE_NODE_REFERENCE_VIOLATION int = 3

Violation of the convention that a node can only be referenced once

PlantModel

Bases: ConceptualModel

PyDexpiBaseModel

Bases: ABC, BaseModel

Base Model for the classes created for the dexpi data model, containing some overarching functionality

SegmentEnds

Bases: NamedTuple

Where a segment's items and connections start and end; see :func:segment_ends.

_flow_links(segment: PipingNetworkSegment) -> tuple[list, Callable[[Any], list]]

A segment's members, and what each one leads to inside the segment. Read-only.

The members are its items then its connections, as listed. An item leads to the connections of the segment that leave it (sourceItem); a connection leads to its targetItem when that is one of the segment's items, and to nothing when it ends outside the segment or nowhere. Identity throughout, never equality. The one definition :func:segment_chain and :func:segment_ends share.

_items_in_flow_order

_items_in_flow_order(segment: PipingNetworkSegment) -> list[piping.PipingNetworkSegmentItem]

segment's own items in :func:segment_chain order, without its connections.

_run_continuation

_run_continuation(segments: list[PipingNetworkSegment], segment: PipingNetworkSegment) -> piping.PipingNetworkSegment | None

The segment that continues segment's run, or None at the line's end.

The continuation starts on the item and the node object segment ends on. Raises PipingTraversalException when that cannot be decided (see :func:run_items).

connect_piping_connection

connect_piping_connection(connector_item: PipingSourceItem | PipingTargetItem, piping_connection: PipingConnection | None = None, piping_segment: PipingNetworkSegment | None = None, connector_node: PipingNode | None = None, as_source: bool = False, force_reconnect: bool = False) -> None

Connects the pipe to the target connector object.

If the connector object is a PipingNode, the connection is implemented via node connection. Otherwise, a connection via Source or TargetItem is attempted. Raises an error if the piping connection is already connected unless force_reconnect is True.

Parameters:

Name Type Description Default
connector_item Union[PipingSourceItem, PipingTargetItem]

The item to which the pipe will be connected.

required
piping_connection PipingConnection

The piping connection to be reconnected. If not passed, then reconnection only performed on the segment

None
piping_segment PipingNetworkSegment

Applies the reconnection to the piping segment too if passed along.

None
connector_node PipingNode | None

Optionally, the node of connector_item to be connected to. Must be a member of connector_item

None
as_source bool

If True, the connection is established as source, else as target. Defaults to False.

False
force_reconnect bool

If False, raises an exception if the piping connection is already connected. Defaults to False.

False

Returns:

Type Description
None

This function modifies the state of the piping connection but does not return any value.

Raises:

Type Description
ConnectionException

If the piping connection is already connected, and force_reconnect is False.

ValueError

If the piping_connection and piping_segment are found to be faulty or do not fit together, or if the optional connector_node is not associated to the connector_item

connect_piping_network_segment

connect_piping_network_segment(piping_segment: PipingNetworkSegment, connector_item: PipingSourceItem | PipingTargetItem, connector_node_index: int | None = None, as_source=False, force_reconnect=False) -> None

Connects a piping network segment by inferring the pipe to reconnect.

This is based on the assumption that a valid piping network segment has at most one unconnected incoming and at most one unconnected outflowing connection. Note that a valid piping network segment may end on a segment item like a valve, in which case there are no outgoing flow connections (same for input). In this case, a ValueError is raised. An Exception is also raised if the piping network segment is corrupted (i.e., has more than one inflowing or outflowing connection).

Parameters:

Name Type Description Default
piping_segment PipingNetworkSegment

Piping segment to be reconnected.

required
connector_item Union[PipingSourceItem, PipingTargetItem]

The connector object to which the segment will be connected. It can be either a PipingNode, PipingSourceItem, or PipingTargetItem.

required
connector_node_index int | None

Optionally, the index of the node of connector_item to be connected to, by default None

None
as_source bool

If True, the connection is established as source, else as target. Defaults to False.

False
force_reconnect bool

If False, raises an exception if the piping connection is already connected. Defaults to False.

False

Returns:

Type Description
None

This function modifies the state of the piping segment and the connection but does not return any value.

Raises:

Type Description
DexpiConnectionException

If the piping connection is already connected, and force_reconnect is False.

ValueError

If the piping network segment either has no unconnected output connection (or input connection if as_source) or has more than one such connections, in which case it is corrupted.

DexpiCorruptPipingSegmentException

If the segment to be connected is found to be corrupt

find_final_connection

find_final_connection(the_segment: PipingNetworkSegment, as_source: bool = False) -> piping.PipingConnection

Finds the first or last connection object in the_segment. It does so by comparing the source and target objects of the segment and the connections. Note that in a valid segment, these should be the first or final connection. However, this method includes some validity checks, so it is recommended to use this instead.

Parameters:

Name Type Description Default
the_segment pipingNetworkSegment

The segment whose final connection is to befound.

required
as_source bool

If the first connection should be found, else last, by default False.

False

Returns:

Type Description
pipingConnection

The first or last connection.

Raises:

Type Description
DexpiCorruptPipingSegmentException

If no connection with the same source and target info as the segment is found.

get_objects_referencing

get_objects_referencing(dexpi_model: EngineeringModel, dexpi_objects: Iterable[PyDexpiBaseModel]) -> list[tuple[PyDexpiBaseModel, str]]

Find every reference attribute in the model that points at one of dexpi_objects.

Returns (holder, attribute_name) pairs: the object holding the reference and the attribute it holds it in. A holder appears once per matching attribute.

DEXPI references are one-directional -- an ActuatingSystem points at the valve it operates, the valve points at nothing -- so answering "what refers to this?" means scanning. The model is traversed exactly once regardless of how many objects are passed, and matching is identity-based (is), never by id field or equality, so distinct objects sharing an id do not alias. Only objects reachable from dexpi_model through composition are visited.

Parameters:

Name Type Description Default
dexpi_model EngineeringModel

The model to search.

required
dexpi_objects Iterable[PyDexpiBaseModel]

The objects to find references to.

required

Returns:

Type Description
list[tuple[PyDexpiBaseModel, str]]

(referencing object, reference attribute name) pairs, in traversal order, and for one holder in the order its class declares the attributes. The order is deterministic, so a caller can record what it clears in it.

get_segment_from_item

get_segment_from_item(conceptual_model: PlantModel, item: PipingNetworkSegmentItem) -> piping.PipingNetworkSegment

Retrieves the piping network segment associated with a given item.

Parameters:

Name Type Description Default
conceptual_model PlantModel

The conceptual model containing the piping network systems.

required
item PipingNetworkSegmentItem

The item for which to retrieve the associated segment.

required

Returns:

Type Description
PipingNetworkSegment

The piping network segment associated with the given item.

Raises:

Type Description
ValueError

If the item is not part of any piping network segment.

get_unconnected_piping_segments

get_unconnected_piping_segments(conceptual_model: PlantModel, as_source=False) -> list[piping.PipingNetworkSegment]

Returns a list of unconnected piping segments ending in a connection in the conceptual model.

Parameters:

Name Type Description Default
conceptual_model ConceptualModel

The model to be analyzed.

required
as_source bool

Whether to look for unconnected sources or targets in piping segments. Defaults to False.

False

Returns:

Type Description
list[PipingNetworkSegment]

The list of identified unconnected segments.

node_holders

node_holders(model: EngineeringModel, node: PipingNode, *, excluding: Iterable[tuple[PyDexpiBaseModel, str]] = ()) -> list[tuple[PyDexpiBaseModel, str]]

Every connection or segment end that names node, as (holder, attribute) pairs.

This is the one answer to "is this port taken": the MCP discovery tools report a node as free exactly when it returns nothing. A node is one physical port, so a pipe attaches to it at most once; a segment's own end counts too, as a run ending on the port has it.

Only sourceNode/targetNode references count -- a node's own drawing refers to it as well.

Parameters:

Name Type Description Default
model EngineeringModel

The model to search.

required
node PipingNode

The node to check.

required
excluding Iterable[tuple[PyDexpiBaseModel, str]]

(holder, attribute) pairs to leave out, compared by identity, so a holder can ask whether any other end names the node.

()

Returns:

Type Description
list[tuple[PyDexpiBaseModel, str]]

The holders, in model traversal order. Empty when the node is free.

piping_network_segment_validity_check

piping_network_segment_validity_check(the_segment: PipingNetworkSegment) -> tuple[PipingValidityCode, str]

Performs a consistency check on the pipingNetworkSegment.

Returns an appropriate PipingValidityCode Enum type. Internally, this is done by going along all items and connections to see if they form a valid chain.

Parameters:

Name Type Description Default
the_segment pipingNetworkSegment

The piping network segment to be checked for consistency.

required

Returns:

Type Description
(PipingValidityCode, str)

An Enum type indicating the validity status of the piping network segment. msg contains the corresponding error message.

run_items

run_items(model: EngineeringModel, start: PipingNetworkSegment | PipingNetworkSegmentItem, *, across_segments: bool = True) -> list[piping.PipingNetworkSegmentItem]

The items along a line, in flow order, from start to the line's end. Read-only.

Within a segment the order is :func:segment_chain's: source to target. At the segment's end the walk carries on into the segment that starts on the same item and the same node object the previous one ends on, and only that one. That is how DEXPI encodes a run split at an item: at a tee the trunk ends on the tee's run outlet and the continuation starts on that same node, while a branch leaves from another node of the tee -- so the walk follows the run and never the branch. The same holds where a run was split at any other item (a valve, a reducer). The continuation may lie in another piping network system.

The walk stops where nothing continues the line: an open end, a nozzle, or an item no segment starts from on that node (a branch joining another line ends there too). That stop needs an item that can be a branch point. At an item with no more than two nodes, a segment leaving from another node continues the line, so the walk raises there rather than return part of the line as the whole; an item with more nodes is taken as a possible branch point and the walk stops. Items outside every segment, such as the nozzles at a line's two ends, are not listed. The walk only goes downstream; it never steps back upstream of start.

Parameters:

Name Type Description Default
model EngineeringModel

The model holding the line.

required
start PipingNetworkSegment | PipingNetworkSegmentItem

Where the walk starts. A segment starts at its first item; an item, which must be one of a segment's items, starts at itself and is listed first.

required
across_segments bool

Carry on into continuation segments, through tees along the run. False stops at the end of the segment the walk starts in. Defaults to True.

True

Returns:

Type Description
list[PipingNetworkSegmentItem]

The items in flow order, each once. Empty for a segment without items that nothing continues.

Raises:

Type Description
TypeError

If start is neither a segment nor a segment item.

ValueError

If start is not a segment of the model, or not an item of one of its segments.

DexpiCorruptPipingSegmentException

If a segment on the way is not one linear chain (see :func:segment_chain).

PipingTraversalException

If the run is ambiguous: two segments start on the node a segment ends on, a segment ends at an item without naming the node while segments start from that item, a segment starting from that item names no node, or the walk comes back to a segment or item it already passed (a cycle). Also when segments leave the item a segment ends at, none starts on the node it ends on, and the item has no more than two nodes: the line goes on there, but the model names two nodes for the junction, so the walk refuses rather than return the line cut short.

segment_chain

segment_chain(segment: PipingNetworkSegment) -> list[piping.PipingNetworkSegmentItem | piping.PipingConnection]

One segment's items and connections, interleaved in flow order. Read-only.

The order runs from the segment's source to its target and is derived from the connections' sourceItem / targetItem, never from list order, so an imported segment whose lists are shuffled still yields its chain. Every PipingConnection counts (a DirectPipingConnection as much as a Pipe), and both endpoint forms are understood: a segment may start or end at one of its own items, or with a connection to an item outside it (or to none). The start is the single element nothing inside the segment leads to.

The segment is left as it is.

Parameters:

Name Type Description Default
segment PipingNetworkSegment

The segment to walk.

required

Returns:

Type Description
list[PipingNetworkSegmentItem | PipingConnection]

The segment's members in flow order. Empty for an empty segment.

Raises:

Type Description
DexpiCorruptPipingSegmentException

When the segment is not one linear chain: an element listed twice, an item with two outgoing connections, an element with two predecessors, a cycle, or elements the walk from the start never reaches. A corrupt segment is refused rather than guessed at.

segment_ends

segment_ends(segment: PipingNetworkSegment) -> SegmentEnds

The elements a segment starts and ends at, read from its connections alone. Read-only.

A start is an element nothing inside the segment leads to: one of its items that none of its connections enters, or one of its connections whose sourceItem is not an item of the segment (an item outside it, or none). An end is the converse: an item none of its connections leaves, or a connection whose targetItem is not an item of the segment. These are the starts and ends :func:segment_chain looks for, but nothing here requires the segment to be one linear chain: a segment that branches has several ends, one that joins several starts, and a closed loop none. List order is never consulted.

Parameters:

Name Type Description Default
segment PipingNetworkSegment

The segment to read.

required

Returns:

Type Description
SegmentEnds

starts and ends, each element once, items before connections and otherwise in list order. Both empty for an empty segment.

segment_ends_in_connection

segment_ends_in_connection(the_segment: PipingNetworkSegment, as_source=False) -> bool

Checks if a segment ends in a connection, as opposed to an item.

Parameters:

Name Type Description Default
the_segment PipingNetworkSegment

The segment to be evaluated

required
as_source bool

Whether the segments source should be checked, by default False

False

Returns:

Type Description
bool

True if the segment ends in a connection.

segments_are_connected

segments_are_connected(segment_a: PipingNetworkSegment, segment_b: PipingNetworkSegment) -> bool

Checks if two segments are connected by comparing their source and target items.

Parameters:

Name Type Description Default
segment_a PipingNetworkSegment

The first segment to be checked.

required
segment_b PipingNetworkSegment

The second segment to be checked.

required

Returns:

Type Description
bool

True if the segments are connected, False otherwise.

Raises:

Type Description
ValueError

If the two segments are the same, as they cannot be connected to themselves.

sort_connected_items_and_connections

sort_connected_items_and_connections(items: list[PipingNetworkSegmentItem], connections: list[PipingConnection]) -> tuple[list[piping.PipingNetworkSegmentItem], list[piping.PipingConnection]]

Sorts the items and connections of a piping network segment in the order of occurrence.

Parameters:

Name Type Description Default
items list[PipingNetworkSegmentItem]

The list of items to be sorted.

required
connections list[PipingConnection]

The list of connections to be sorted.

required

Returns:

Type Description
tuple[list[PipingNetworkSegmentItem], list[PipingConnection]]

A tuple containing the sorted items and connections.

Raises:

Type Description
ValueError

If the segment is not validly connected, such as having multiple starting points or disconnected elements.

traverse_items_and_connections

traverse_items_and_connections(all_items: list[PipingNetworkSegmentItem], all_connections: list[PipingConnection], start_element: PipingNetworkSegmentItem | PipingConnection, end_condition: PipingNetworkSegmentItem | PipingConnection | Callable[[PipingNetworkSegmentItem | PipingConnection], bool], reverse=False) -> list[piping.PipingNetworkSegmentItem | piping.PipingConnection]

Traverses piping items and connections until the end condition is met.

The traversal is done in a depth-first manner, starting from the specified start element. Branching decisions are inferred from the order of the connections in the list of all connections. Branches that start with connections that are higher in the list are traversed first. The traversal can be done in reverse order by setting the reverse parameter to True.

Parameters:

Name Type Description Default
all_items list[PipingNetworkSegmentItem]

List of all items in the piping network segment.

required
all_connections list[PipingConnection]

List of all connections in the piping network segment.

required
start_element PipingNetworkSegmentItem | PipingConnection

The element from which to start the traversal.

required
end_condition piping.PipingNetworkSegmentItem|piping.PipingConnection|
        Callable[[piping.PipingNetworkSegmentItem|piping.PipingConnection], bool]

The condition that determines when to stop the traversal. This can be a specific element to be encountered or a callable function for more complex end conditions.

required
reverse bool

Whether to traverse in reverse order. Defaults to False.

False

Returns:

Type Description
list[PipingNetworkSegmentItem | PipingConnection]

A list of traversed elements.

Raises:

Type Description
PipingTraversalException

If a circle is encountered or if the traversal cannot continue due to invalid connections.

Display Name Toolkit

Derive human-readable captions for DEXPI objects. Applied as the displayName property of every graph export: nodes are captioned "{Humanised class} ({tag})", while relationships formed by collapsing a node carry a concise engineer-facing term such as Measurement or Pipe (47121). The tag is type-specific -- tagName for equipment, identifier for the objects of a DEXPI 2.0 process model -- and a process object's free-text label can caption it but is never its tag.

pydexpi.toolkits.display_name

Derive human-readable display names for pyDEXPI objects.

Graph exports expose the original DEXPI data attributes, but none of them is a consistent caption suitable for graph visualisation. Neo4j Browser, for example, picks a different property per node -- sometimes a tag, sometimes an object id, sometimes an unhelpful serialised value.

This module provides :func:get_display_name, a type-aware resolver that produces a short, deterministic caption for any PyDexpiBaseModel. It is applied once in the shared graph-building layer (:meth:pydexpi.loaders.graph_loader.GraphLoader._add_nodes), so every exporter -- GraphML, Neo4j CSV, LLM context, the CLI -- receives the same displayName value.

The resulting format is "{Humanised class name} ({identifier})"::

Centrifugal Pump (P4711)
Ball Valve (73KH12)
Nozzle (N1)
Piping Network Segment (S1)
Pipe (Pipe1)
Direct Piping Connection          # no identifier available

The class name stays in the caption because it is the most reliable piece of information available: many DEXPI objects carry no tag at all. The concrete class also remains available separately as the label node attribute and as the Neo4j node label.

Relationship captions

:class:~pydexpi.loaders.graph_loader.GraphAbstractor collapses a handful of classes into relationships when it builds the process and conceptual graphs. A collapsed edge inherits the node's attributes, so without further work it would be captioned with the same class-name caption -- Measuring Line Function (MeasuringLineFunction1) where an engineer wants Measurement.

:func:get_relationship_display_name supplies the engineer-facing term for those classes::

Measurement                 # MeasuringLineFunction
Signal                      # SignalConveyingFunction
Actuation                   # OperatedValveReference
Direct connection           # DirectPipingConnection
Pipe (47121)                # Pipe, identified by its line number

The wording has to exist as data on the edge because the Neo4j Browser resolves a caption as a single property name: a stylesheet template mixing literal text with a placeholder renders as nothing at all.

Extending the mapping

For nodes, add an entry to :data:DISPLAY_NAME_RESOLVERS. Entries are matched with isinstance in order, so more specific classes must be listed before their base classes. For collapsed relationships, add an entry to :data:RELATIONSHIP_DISPLAY_NAMES.

Examples:

>>> from pydexpi.pydexpi_classes.plant.piping import BallValve
>>> get_display_name(BallValve(pipingComponentName="73KH12"))
'Ball Valve (73KH12)'

CAPTION_FALLBACK_ATTRIBUTES module-attribute

CAPTION_FALLBACK_ATTRIBUTES: tuple[str, ...] = ('deviceTypeName', 'label')

DISPLAY_NAME_RESOLVERS module-attribute

DISPLAY_NAME_RESOLVERS: tuple[tuple[type, tuple[str, ...]], ...] = ((Nozzle, ('subTagName',)), (PipingNetworkSegment, ('segmentNumber',)), (PipingNetworkSystem, ('lineNumber',)), (PipingComponent, ('pipingComponentName', 'pipingComponentNumber')), (PipeOffPageConnector, ('pipeConnectorNumber',)), (SignalOffPageConnector, ('signalConnectorNumber',)), (InstrumentationLoopFunction, ('instrumentationLoopFunctionNumber',)), (ProcessInstrumentationFunction, ('processInstrumentationFunctionNumber',)), (ProcessSignalGeneratingFunction, ('processSignalGeneratingFunctionNumber',)), (SignalConveyingFunction, ('signalPointNumber',)), (MeasuringSystem, ('measuringSystemNumber',)), (ActuatingElectricalSystem, ('actuatingElectricalSystemNumber',)), (ActuatingSystem, ('actuatingSystemNumber',)), (ActuatingElectricalFunction, ('actuatingElectricalFunctionNumber',)), (ActuatingFunction, ('actuatingFunctionNumber',)), (TaggedPlantItem, ('tagName',)), (ProcessStep, ('identifier',)), (Port, ('identifier',)), (ProcessConnection, ('identifier',)), (ProcessStepDetail, ('identifier',)), (MaterialTemplate, ('identifier',)), (MaterialState, ('identifier',)), (MaterialStateType, ('identifier',)), (PureMaterialComponent, ('identifier', 'iUPAC_identifier', 'chEBI_identifier')), (CustomMaterialComponent, ('identifier', 'projectReference')), (MaterialComponent, ('identifier',)), (InstrumentationActivity, ('identifier',)), (InstrumentationSystemActivity, ('identifier',)))

GENERIC_TAG_ATTRIBUTES module-attribute

GENERIC_TAG_ATTRIBUTES: tuple[str, ...] = ('tagName', 'subTagName', 'pipingComponentName', 'pipingComponentNumber', 'segmentNumber', 'lineNumber', 'pipeConnectorNumber')

RELATIONSHIP_DISPLAY_NAMES module-attribute

RELATIONSHIP_DISPLAY_NAMES: dict[str, tuple[str, tuple[str, ...]]] = {'Pipe': ('Pipe', ('lineNumber',)), 'DirectPipingConnection': ('Direct connection', ()), 'MeasuringLineFunction': ('Measurement', ()), 'SignalConveyingFunction': ('Signal', ()), 'OperatedValveReference': ('Actuation', ())}

_CAMEL_SPLIT module-attribute

_CAMEL_SPLIT = re.compile('(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])')

__all__ module-attribute

__all__ = ['CAPTION_FALLBACK_ATTRIBUTES', 'DISPLAY_NAME_RESOLVERS', 'GENERIC_TAG_ATTRIBUTES', 'RELATIONSHIP_DISPLAY_NAMES', 'combine_name_and_tag', 'get_display_name', 'get_relationship_display_name', 'get_tag', 'humanize_class_name', 'referenced_object_name']

ActuatingElectricalFunction

Bases: ConceptualObject, SignalConveyingFunctionTarget, TechnicalItem

An actuation setting electrical function. It covers all types of electrical consumers, e.g., motors and heaters.

ActuatingElectricalSystem

Bases: ConceptualObject, TechnicalItem

An assembly of artefacts that is designed to fulfill an ActuatingElectricalFunction.

ActuatingFunction

Bases: ConceptualObject, SignalConveyingFunctionSource, SignalConveyingFunctionTarget, TechnicalItem

A function for acting control structures relating to the process.

ActuatingSystem

Bases: ConceptualObject, TechnicalItem

An assembly of artefacts that is designed to fulfill an ActuatingFunction.

CustomMaterialComponent

Bases: MaterialComponent

A CustomMaterialComponent defines a custom chemical compound used to specify the chemical composition of a material. These compounds are usually pseudocomponents that are used to simulate the thermodynamic properties of complex mixtures of organic compounds with high molecular weight.

InstrumentationActivity

Bases: ConceptualObject

This is the base type for control and instrumentation functions that are specified as part of the process design.

InstrumentationLoopFunction

Bases: ConceptualObject, TechnicalItem

An identified collection of related ProcessInstrumentationFunctions that interact for a known purpose.

InstrumentationSystemActivity

Bases: ConceptualObject

An InstrumentationSystemActivity is a collection of related InstrumentationActivity instances that together perform a specified function. Thus a MeasuringProcessVariable and a ControllingProcessVariable activity can be grouped into a single InstrumentationSystemActivity that represents the control loop.

MaterialComponent

Bases: ConceptualObject

This is the base type to chemical components in the model. It is parent class for PureMaterialComponent and CustomMaterialComponent.

MaterialState

Bases: ConceptualObject

This data structure contains the properties for a Stream. It contains one or more MaterialState objects: one for the total properties and then one for each of the phases present.

MaterialStateType

Bases: ConceptualObject

This data structure represents the properties of a material or a single material phase. The MaterialState data structure for a Stream is built up of one or more of these data structures: one for the total properties and then one for each of the phases present.

MaterialTemplate

Bases: ConceptualObject

This data structure defines the structure of Stream objects that correspond to a specific type of material. This structure contains information about the thermodynamic model used to represent this type of material. It contains the ListOfMaterialComponents: pure and pseudo components used to represent the system. It also determines the legend for a stream table when the model is used to represent a Process Flow Diagram.

MeasuringSystem

Bases: ConceptualObject, TechnicalItem

An assembly of artefacts that is designed to measure and transmit a property in order to fulfill one or more ProcessSignalGeneratingFunctions.

Nozzle

Bases: ConceptualObject, ActuatingElectricalLocation, SensingLocation, PipingNodeOwner, PipingSourceItem, PipingTargetItem

A projecting short piece of pipe that is attached to another (hollow) body.

PipeOffPageConnector

Bases: ConceptualObject, PipingNetworkSegmentItem, PipingNodeOwner

A connector that indicates that a piping network segment is continued elsewhere, either on the same PID or on another PID. Graphically, it is usually represented as an arrow.

PipingComponent

Bases: ConceptualObject, SensingLocation, PipingNetworkSegmentItem, PipingNodeOwner, PipingSourceItem, PipingTargetItem

A piping component

PipingNetworkSegment

Bases: ConceptualObject, ActuatingElectricalLocation, SensingLocation

PipingNetworkSystem

Bases: ConceptualObject, TechnicalItem

Port

Bases: ConceptualObject

A port represents a transfer of material, energy or information into or out of a process step. It provides the anchor point for connecting two process steps to each other. The term is derived from SysML and IDEF0 representations of processes. Note that a port in DEXPI Process is a conceptual object. It represents a contract regarding the material, energy or information coming into or out of a process step.

ProcessConnection

Bases: ConceptualObject

This data structure is the abstract parent for all connections between blocks in the DEXPI Process model.

ProcessInstrumentationFunction

Bases: ConceptualObject, SignalConveyingFunctionSource, SignalConveyingFunctionTarget, TechnicalItem

A requirement for instrumentation and/or control structures relating to Process Engineering.

ProcessSignalGeneratingFunction

Bases: ConceptualObject, SignalConveyingFunctionSource, TechnicalItem

A function for instrumentation and/or control structures relating to Process Engineering.

ProcessStep

Bases: ConceptualObject

The ProcessStep type is the parent type for all process steps in the model.

ProcessStepDetail

Bases: ConceptualObject

This type is the parent type for processes that cannot be run independently of another process.

PureMaterialComponent

Bases: MaterialComponent

A PureMaterialComponent represents a MaterialComponent that is modelled as a pure chemical compound with defined composition and structure. This is used in the MaterialTemplate to document the composition vector and define properties for use in the process.

PyDexpiBaseModel

Bases: ABC, BaseModel

Base Model for the classes created for the dexpi data model, containing some overarching functionality

SignalConveyingFunction

Bases: ConceptualObject

A function for conveying a signal.

SignalOffPageConnector

Bases: ConceptualObject

A signal connector that indicates that a SignalConveyingFunction is continued elsewhere, either on the same P&ID or on another P&ID. Graphically, it is usually represented as an arrow.

TaggedPlantItem

Bases: ConceptualObject, TechnicalItem

A fully tagged item in a plant.

_clean_string

_clean_string(value: object) -> str | None

Return value stripped, or None if it is not a usable string.

DEXPI string attributes are typed str | Undefined | None, where Undefined is a sentinel model instance rather than a string. Only genuine, non-blank strings are accepted.

Parameters:

Name Type Description Default
value object

The candidate value.

required

Returns:

Type Description
str or None

The stripped string, or None.

_first_string_attribute

_first_string_attribute(obj: object, attribute_names: tuple[str, ...]) -> str | None

Return the first non-blank string attribute of obj, or None.

Parameters:

Name Type Description Default
obj object

The object to read attributes from.

required
attribute_names tuple of str

Attribute names to try, in priority order.

required

Returns:

Type Description
str or None

The first usable value, stripped of surrounding whitespace.

_first_string_value

_first_string_value(attributes: Mapping[str, object], attribute_names: tuple[str, ...]) -> str | None

Return the first non-blank string entry of attributes, or None.

The mapping counterpart of :func:_first_string_attribute, used for graph attribute dictionaries rather than model objects.

Parameters:

Name Type Description Default
attributes Mapping

The attribute dictionary to read from.

required
attribute_names tuple of str

Keys to try, in priority order.

required

Returns:

Type Description
str or None

The first usable value, stripped of surrounding whitespace.

_resolve_identifier

_resolve_identifier(obj: PyDexpiBaseModel) -> str | None

Resolve the identifier part of the display name for obj.

Resolution order:

  1. The tag, as resolved by :func:get_tag.
  2. The :data:CAPTION_FALLBACK_ATTRIBUTES (a device type, or a process object's free-text label), which are not tags.
  3. The object id, unless it was auto-generated by make_id.

Parameters:

Name Type Description Default
obj PyDexpiBaseModel

The object to resolve.

required

Returns:

Type Description
str or None

The identifier, or None when no meaningful one exists.

combine_name_and_tag

combine_name_and_tag(name: str | None, tag: str | None) -> str | None

Combine a name and a tag into a single caption.

Applies the formatting rules for display names: blank and None values are ignored, identical values are not repeated, and parentheses are only used when both values are present.

Parameters:

Name Type Description Default
name str or None

The leading part of the caption.

required
tag str or None

The identifier shown in parentheses.

required

Returns:

Type Description
str or None

The combined caption, or None if neither value is usable.

Examples:

>>> combine_name_and_tag("Feed Pump", "P-4712")
'Feed Pump (P-4712)'
>>> combine_name_and_tag(None, "P-4712")
'P-4712'
>>> combine_name_and_tag("Feed Pump", "Feed Pump")
'Feed Pump'
>>> combine_name_and_tag("", "   ") is None
True

get_display_name

get_display_name(obj: PyDexpiBaseModel) -> str

Return a human-readable caption for a pyDEXPI object.

The caption is "{Humanised class name} ({identifier})", where the identifier is resolved from type-specific attributes. Objects without any usable identifier are captioned with the humanised class name alone. The result is deterministic and never empty.

Parameters:

Name Type Description Default
obj PyDexpiBaseModel

The object to name.

required

Returns:

Type Description
str

The display name, e.g. "Centrifugal Pump (P4711)".

Examples:

>>> from pydexpi.pydexpi_classes.plant.processEquipment import Tank
>>> get_display_name(Tank(tagName="T4750"))
'Tank (T4750)'

get_relationship_display_name

get_relationship_display_name(label: str, attributes: Mapping[str, object] | None = None, inheritance: str = '') -> str | None

Return the caption for a relationship formed by collapsing label.

Collapsed relationships are captioned with a concise engineer-facing term from :data:RELATIONSHIP_DISPLAY_NAMES instead of the class-name caption a node would receive. Classes that are not registered there return None, which leaves the caption inherited from the collapsed node untouched.

Parameters:

Name Type Description Default
label str

The concrete class name of the collapsed node.

required
attributes Mapping

The attributes of the relationship being built. Searched for the identifier shown in parentheses, which may originate from a node that was merged into the collapsed one earlier -- a pipe's lineNumber comes from its PipingNetworkSystem.

None
inheritance str

The collapsed node's colon-separated labels chain, used so that a subclass of a registered class still resolves.

""

Returns:

Type Description
str or None

The caption, or None if label denotes no registered relationship.

Examples:

>>> get_relationship_display_name("MeasuringLineFunction")
'Measurement'
>>> get_relationship_display_name("Pipe", {"lineNumber": "47121"})
'Pipe (47121)'
>>> get_relationship_display_name("Pipe")
'Pipe'
>>> get_relationship_display_name("BallValve") is None
True

get_tag

get_tag(obj: PyDexpiBaseModel) -> str | None

Return the engineering tag of obj, or None if it carries none.

This is the tag part of :func:get_display_name without its last resort: the object id is never returned. Use it wherever a tag is wanted as a tag -- a label in a diff, a search field -- rather than as a caption, so that an untagged object reads as untagged instead of as its id.

Resolution order:

  1. The type-specific attributes registered in :data:DISPLAY_NAME_RESOLVERS.
  2. The generic sweep over :data:GENERIC_TAG_ATTRIBUTES.

Only non-blank strings count; Undefined and blank values are skipped.

Parameters:

Name Type Description Default
obj PyDexpiBaseModel

The object to read the tag of.

required

Returns:

Type Description
str or None

The tag, stripped of surrounding whitespace, or None.

Examples:

>>> from pydexpi.pydexpi_classes.plant.piping import PipingNode
>>> from pydexpi.pydexpi_classes.plant.processEquipment import Tank
>>> get_tag(Tank(tagName="T4750"))
'T4750'
>>> get_tag(PipingNode(id="PipingNode-1")) is None
True

humanize_class_name cached

humanize_class_name(class_name: str) -> str

Split a CamelCase DEXPI class name into space-separated words.

Cached (bounded): it runs once per object whenever captions are built, over a few hundred distinct class names.

Parameters:

Name Type Description Default
class_name str

The class name, e.g. "CentrifugalPump".

required

Returns:

Type Description
str

The humanised name, e.g. "Centrifugal Pump". Acronym runs are kept together, so "PIDController" becomes "PID Controller".

instrument_tag

instrument_tag(function: ProcessInstrumentationFunction) -> str | None

The loop tag of an instrumentation function as a P&ID prints it: "PICSA-4712.02".

That is the category letter and the further function letters, a hyphen, and the number. :func:get_tag resolves an instrumentation function by its number alone ("4712.02"), which is what the diff matches on and the query index keeps as its canonical key. Queries also accept this printed tag as an additional key. This is the tag a person reads, and the one to show in a list, where the number alone repeats -- a PI and a PT of one loop share it.

Missing parts are left out rather than raising: no letters gives the number, no number gives the letters, neither gives None.

Examples:

>>> from pydexpi.pydexpi_classes.plant.instrumentation import ProcessInstrumentationFunction
>>> instrument_tag(ProcessInstrumentationFunction(
...     processInstrumentationFunctionCategory="P",
...     processInstrumentationFunctions="ICSA",
...     processInstrumentationFunctionNumber="4712.02",
... ))
'PICSA-4712.02'

is_generated_id

is_generated_id(value: object) -> bool

Return True if value has the shape of an id minted by make_id.

A generated id says nothing about where the object came from, only that no id was supplied when it was built. What that means for stability depends on the caller: an object loaded from a file without an id gets a fresh one on every load, while an object created in code keeps the id it was minted with once the model is saved as JSON. This function answers the shape question only; deciding whether to trust such an id is the caller's call.

Parameters:

Name Type Description Default
value object

The value to examine, normally an object's id.

required

Returns:

Type Description
bool

True for strings of the form id_<32 lowercase hex digits>.

referenced_object_name

referenced_object_name(obj: PyDexpiBaseModel | None) -> str

Name an object that a message mentions but affected_component_ids does not list.

The display name, as for the affected objects. For an untagged object whose caption does not already show its id (the id was minted by make_id), the id is appended: the message is then the only place a reader, or an agent fixing the model, can learn which node or item is meant. Ids are deliberately not added to affected_component_ids instead, because that list identifies the issue.

Validation messages and other messages that name objects use it, which is why it lives here rather than in the validation package.

Parameters:

Name Type Description Default
obj PyDexpiBaseModel or None

The referenced object, or None for an unset reference.

required

Returns:

Type Description
str

E.g. "Globe Valve (66KL21)", "Piping Node (PipingNode51)", "Piping Node id_3f2a...", or "none".

Display Labels Toolkit

Human-readable labels for enum values (units as their symbol, "barg"; other values in sentence case, "Fail close") and attribute names ("Design pressure").

pydexpi.toolkits.display_labels

Human-readable labels for enum values and attribute names.

:mod:pydexpi.toolkits.display_name captions objects. This module labels the two other things a user interface shows next to them: the value of an enumerated attribute, and the name of an attribute. Both follow the same rule as object captions, so that one source of wording serves the graph exports, the table export, search, validation messages and any API built on pyDEXPI:

  • :func:enum_value_label -- FailActionClassification.FailClose -> "Fail close", and a unit such as TemperatureUnit.DegreeCelsius -> "°C".
  • :func:attribute_label -- "designPressure" -> "Design pressure".

Neither function imports :mod:pydexpi.exports, whose __init__ pulls in pandas, openpyxl and networkx. The dependency points the other way: the table export's :func:~pydexpi.exports.table_export.format_string delegates to :func:humanize_attribute_name here, so there is one implementation of the attribute-name casing.

Units are not labelled here. :func:enum_value_label hands them to :func:pydexpi.toolkits.quantity_format.unit_symbol, the single home of the unit-symbol map.

Examples:

>>> from pydexpi.pydexpi_classes.core.physicalQuantities import PressureGaugeUnit
>>> from pydexpi.pydexpi_classes.plant.enumerations import FailActionClassification
>>> enum_value_label(FailActionClassification.FAILCLOSE)
'Fail close'
>>> enum_value_label(PressureGaugeUnit.BAR)
'barg'
>>> attribute_label("nominalDiameterRepresentation")
'Nominal diameter representation'

ATTRIBUTE_LABEL_OVERRIDES module-attribute

ATTRIBUTE_LABEL_OVERRIDES: dict[str, str] = {'processInstrumentationFunctionCategory': 'Measured variable letters', 'processInstrumentationFunctions': 'Function letters', 'processInstrumentationFunctionNumber': 'Loop number', 'processInstrumentationFunctionModifier': 'Modifier letters', 'caseUID': 'Case UID', 'chEBI_identifier': 'ChEBI identifier', 'iUPAC_identifier': 'IUPAC identifier', 'provenanceURI': 'Provenance URI', 'referenceDataURI': 'Reference data URI', 'sourceURI': 'Source URI', 'uri_': 'URI', 'pipingComponentName': 'Component name', 'pipingComponentNumber': 'Component number'}

ENUM_VALUE_LABEL_OVERRIDES module-attribute

ENUM_VALUE_LABEL_OVERRIDES: dict[tuple[type[Enum], str], str] = {(MeasuredQuantity, 'AudioVisual'): 'Audiovisual', (MeasuredQuantity, 'WeightMassForce'): 'Weight, mass or force'}

_ATTRIBUTE_CAPITAL module-attribute

_ATTRIBUTE_CAPITAL = re.compile('([A-Z])')

_DESIGNATION_ENUMS module-attribute

_DESIGNATION_ENUMS: frozenset[type[Enum]] = frozenset({NominalDiameterStandardClassification, NominalPressureStandardClassification})

_DESIGNATION_FILLER_WORDS module-attribute

_DESIGNATION_FILLER_WORDS: frozenset[str] = frozenset({'object', 'artefact'})

_ENCODED_CHARACTERS module-attribute

_ENCODED_CHARACTERS: tuple[tuple[str, str], ...] = (('_PER_', '/'), ('_COMMA_', '.'))

_ENUM_WORD_SPELLINGS module-attribute

_ENUM_WORD_SPELLINGS: dict[str, str] = {'din': 'DIN', 'dn': 'DN', 'en': 'EN', 'gmp': 'GMP', 'iso': 'ISO', 'kpa': 'kPa', 'nps': 'NPS', 'pn': 'PN'}

_LETTER_DIGIT_SPLIT module-attribute

_LETTER_DIGIT_SPLIT = re.compile('(?<=[A-Za-z])(?=[0-9])')

__all__ module-attribute

__all__ = ['ATTRIBUTE_LABEL_OVERRIDES', 'ENUM_VALUE_LABEL_OVERRIDES', 'attribute_label', 'enum_value_label', 'humanize_attribute_name']

MeasuredQuantity

Bases: StrEnum

NominalDiameterStandardClassification

Bases: StrEnum

NominalPressureStandardClassification

Bases: StrEnum

PhysicalQuantityUnit

Bases: StrEnum

_enum_value_label cached

_enum_value_label(enum_class: type[Enum], member_name: str) -> str

Label the member member_name of enum_class; see :func:enum_value_label.

_sentence_case_enum_value

_sentence_case_enum_value(value: str, *, drop_filler: bool) -> str

Split a generated enum value into words and sentence-case them.

Parameters:

Name Type Description Default
value str

The raw enum value, e.g. "Nps1_1_PER_2Artefact".

required
drop_filler bool

Whether to drop the Object / Artefact modelling words.

required

Returns:

Type Description
str

The label, e.g. "NPS 1 1/2".

attribute_label cached

attribute_label(name: str) -> str

Return the human-readable label for a DEXPI attribute name.

The curated :data:ATTRIBUTE_LABEL_OVERRIDES first, then the generic :func:humanize_attribute_name casing.

Parameters:

Name Type Description Default
name str

The attribute name as the generated classes spell it, e.g. "designPressure".

required

Returns:

Type Description
str

The label, e.g. "Design pressure".

Examples:

>>> attribute_label("designPressure")
'Design pressure'
>>> attribute_label("pipingComponentName")
'Component name'
>>> attribute_label("caseUID")
'Case UID'

enum_value_label

enum_value_label(member: Enum) -> str

Return the human-readable label for an enum member.

  • A unit (any :class:~pydexpi.pydexpi_classes.core.physicalQuantities.PhysicalQuantityUnit) is labelled with its symbol, the pressure reference spelled out: DegreeCelsius -> "°C", PressureGaugeUnit.Bar -> "barg". A unit with no symbol is labelled like any other member: RatioUnit.Fraction -> "Fraction".
  • Any other member is labelled with its value in sentence case: "FailClose" -> "Fail close". Acronyms keep their spelling ("Non-GMP relevant function"), standard designations lose their Object / Artefact suffix ("NPS 1 1/2", "EN 1333 PN 2.5"), and a few members have a curated label in :data:ENUM_VALUE_LABEL_OVERRIDES.

Parameters:

Name Type Description Default
member Enum

The enum member, e.g. FailActionClassification.FAILCLOSE.

required

Returns:

Type Description
str

The label.

Raises:

Type Description
TypeError

If member is not an enum member.

Examples:

>>> from pydexpi.pydexpi_classes.core.physicalQuantities import TemperatureUnit
>>> from pydexpi.pydexpi_classes.plant.enumerations import FailActionClassification
>>> enum_value_label(TemperatureUnit.DEGREECELSIUS)
'°C'
>>> enum_value_label(FailActionClassification.FAILCLOSE)
'Fail close'

humanize_attribute_name cached

humanize_attribute_name(name: str) -> str

Sentence-case a camelCase attribute name, without curated overrides.

This is the casing of the table export's column headers (:func:pydexpi.exports.table_export.format_string delegates here), and therefore of the keys of the translation tables in pydexpi.exports.table_locale. Changing it changes those keys. For a label to show a user, prefer :func:attribute_label.

Parameters:

Name Type Description Default
name str

The attribute name, e.g. "pipingComponentName".

required

Returns:

Type Description
str

The first character capitalised, and every later capital turned into a space and its lowercase form: "Piping component name". An empty name is returned unchanged.

Examples:

>>> humanize_attribute_name("designPressure")
'Design pressure'
>>> humanize_attribute_name("caseUID")
'Case u i d'

humanize_class_name cached

humanize_class_name(class_name: str) -> str

Split a CamelCase DEXPI class name into space-separated words.

Cached (bounded): it runs once per object whenever captions are built, over a few hundred distinct class names.

Parameters:

Name Type Description Default
class_name str

The class name, e.g. "CentrifugalPump".

required

Returns:

Type Description
str

The humanised name, e.g. "Centrifugal Pump". Acronym runs are kept together, so "PIDController" becomes "PID Controller".

unit_symbol

unit_symbol(unit: Enum, *, qualify_pressure_reference: bool = False) -> str

Return the display symbol for a PhysicalQuantityUnit literal.

Parameters:

Name Type Description Default
unit Enum

A unit literal from the generated DEXPI unit enums.

required
qualify_pressure_reference bool

If True, append "g" to gauge pressures and "a" to absolute pressures ("barg", "bara", "psig"). Off by default so the table export keeps rendering the bare symbol; label materialization turns it on, because P&ID labels conventionally spell the reference out and the DEXPI reference drawing does too.

``False``

Returns:

Type Description
str

The conventional symbol ("m", "kg/h", "°C"), or the raw enum value for unmapped literals. RatioUnit.Fraction has no symbol and renders ""; join a value to a symbol with :func:value_with_symbol, which then writes the bare number.


Versioning

Semantic diff and the Git projection. See Versioning for how they relate.

Semantic diff

pydexpi.diff.id_diff

id_diff(model1: EngineeringModel, model2: EngineeringModel, *, track_order: bool = False, scope: Iterable[str] | None = None) -> DiffResult

Compute the diff between two models using ID-based equality.

Every call builds its own PyDexpiDiffEngine. The engine keeps its object and match maps as instance state for the duration of a diff, so an engine shared between calls -- this function used to reuse one module-wide instance -- is neither re-entrant nor thread-safe: two concurrent diffs would overwrite each other's maps. Building an engine is cheap; the diff itself dominates.

Parameters:

Name Type Description Default
model1 EngineeringModel

The first model to compare.

required
model2 EngineeringModel

The second model to compare.

required
track_order bool

Also report reorders of order-significant composition lists (z-order, a segment's flow order); see pydexpi.diff.ordering. Off by default.

False
scope iterable of str

Ids of objects to restrict the result to, each with its composition subtree in either model -- one piece of equipment, a piping network system, a segment. This filters the whole-model diff: everything is still compared, so a valve moved out of a scoped segment is reported as moved, with its new parent outside the scope. To compare two subtrees on their own instead, use PyDexpiDiffEngine.diff_objects.

None

Returns:

Type Description
DiffResult

The result of the diff operation, containing all detected changes (within scope, if given).

Raises:

Type Description
ValueError

If a scope id is in neither model -- more likely a typo than an intent.

pydexpi.diff.DiffResult

Bases: BaseModel

Top-level container for the result of comparing two pyDEXPI models.

Attributes:

Name Type Description
format_version str

The serialized format's version: :data:DIFF_FORMAT_VERSION for a diff computed now, or "1" for a stored format-1 payload read back.

matching Matching

How objects were matched between the models. Only an "id" diff pairs each object with the one carrying the same id, which is what acting on the diff needs.

track_order bool

Whether reorders of the composition lists pyDEXPI orders beyond the specification (a segment's flow order) were compared. An empty result with track_order=False does not mean a segment's items are in the same order. Lists the specification declares ordered, the drawing's z-order among them, are compared either way.

moved_objects list[MovedObject]

Objects that exist in both models but have a changed parent in the composition hierarchy.

added_objects list[AddedObject]

Objects present only in the new model.

removed_objects list[RemovedObject]

Objects present only in the old model.

modified_objects list[ModifiedObject]

Objects present in both models with changed attributes.

added_objects class-attribute instance-attribute

added_objects: list[AddedObject] = Field(default_factory=list)

format_version class-attribute instance-attribute

format_version: DiffFormatVersion = DIFF_FORMAT_VERSION

has_changes property

has_changes: bool

Return True if any diff category is non-empty.

has_graphic_changes property

has_graphic_changes: bool

Return True if any change concerns drawing/layout data.

This does not mean the changes are limited to graphics: a diff can have both graphic and semantic changes. Check has_semantic_changes for the other half.

has_metadata_changes property

has_metadata_changes: bool

Return True if any change concerns document metadata.

has_semantic_changes property

has_semantic_changes: bool

Return True if any change concerns the engineering content (the conceptual model).

Document metadata -- the modification date, the exporting tool -- is not engineering content, and neither is the drawing.

matching class-attribute instance-attribute

matching: Matching = 'custom'

modified_objects class-attribute instance-attribute

modified_objects: list[ModifiedObject] = Field(default_factory=list)

moved_objects class-attribute instance-attribute

moved_objects: list[MovedObject] = Field(default_factory=list)

removed_objects class-attribute instance-attribute

removed_objects: list[RemovedObject] = Field(default_factory=list)

track_order class-attribute instance-attribute

track_order: bool = False

_entries

_entries() -> list[_ChangeEntry]

Every per-object entry, in the order of the four lists.

filter

filter(predicate: Callable[[DiffEntry], bool]) -> DiffResult

Return a diff with only the entries predicate accepts, and the same metadata.

Parameters:

Name Type Description Default
predicate callable

Called with each AddedObject, RemovedObject, ModifiedObject and MovedObject; the entry is kept when it returns True.

required

Returns:

Type Description
DiffResult

A new result. The original is not changed.

for_objects

for_objects(object_ids: Iterable[str]) -> DiffResult

Return the changes concerning the given objects.

An entry is kept when its object's id -- in either model -- is one of object_ids. To keep a whole subtree, pass its ids: pydexpi.diff.subtree_ids collects them.

Parameters:

Name Type Description Default
object_ids iterable of str

The ids of the objects of interest.

required

Returns:

Type Description
DiffResult

A new result with the same metadata.

graphics_only

graphics_only() -> DiffResult

Return only the drawing/layout changes.

in_domains

in_domains(*domains: ChangeDomain) -> DiffResult

Return the changes in the given domains ("conceptual", "graphics", ...).

metadata_only

metadata_only() -> DiffResult

Return only the document-metadata changes.

semantic_only

semantic_only() -> DiffResult

Return only the engineering (conceptual) changes.

pydexpi.diff.PyDexpiDiffEngine

PyDexpiDiffEngine(semantic_equality: SemanticEquality, *, track_order: bool = False)

A diff engine for comparing two pyDEXPI models based on a provided semantic equality.

The diff engine identifies changes in form of added, removed, moved, and modified objects, where modified objects are objects with changes in their data attributes or connectivity. The diff engine relies on a provided semantic equality function that defines the criteria for semantic equality between pyDEXPI objects of the same type.

To calculate the diffs, the engine first creates a mapping between semantically equal objects in the two models based on the provided SemanticEquality implementation. It also keeps track of objects, ids, and composition hierarchy for both models. Then, diffs are calculated: - Added objects: Objects in model 2 that have no semantic match in model 1. - Removed objects: Objects in model 1 that have no semantic match in model 2. - Moved objects: Objects that have a semantic match in the other model but a different container (i.e. parent in the composition hierarchy). - Modified objects: Objects that have a semantic match in the other model but changes in reference or data attributes.

Attributes:

Name Type Description
semantic_equality SemanticEquality

The semantic equality function used to compare pyDEXPI objects of the same type.

_object_map1 dict[str, PyDexpiBaseModel]

A mapping of object IDs to pyDEXPI objects for model 1.

_object_map2 dict[str, PyDexpiBaseModel]

A mapping of object IDs to pyDEXPI objects for model 2.

_semantic_map dict[str, str]

A mapping of object IDs from model 1 to object IDs from model 2 for pairs of objects considered equal within the composition hierarchy.

_second_semantic_map dict[str, str]

A mapping of object IDs from model 1 to object IDs from model 2 for pairs of objects considered equal globally.

_container_map1 dict[str, str | None]

A mapping of object IDs from model 1 to the ID of their direct container (i.e. parent in the composition hierarchy), or None for the root model itself.

_container_map2 dict[str, str | None]

A mapping of object IDs from model 2 to the ID of their direct container (i.e. parent in the composition hierarchy), or None for the root model itself.

_non_mapped_obj1 list[PyDexpiBaseModel]

A list of objects from model 1 that have no semantic match in model 2.

_non_mapped_obj2 list[PyDexpiBaseModel]

A list of objects from model 2 that have no semantic match in model 1.

Example usage

from pydexpi.diff.engine import PyDexpiDiffEngine from pydexpi.diff.semantic_equality import id_equality from pydexpi.pydexpi_classes.pydexpi_classes import EngineeringModel

Create two models to compare

model1 = EngineeringModel(...) model2 = EngineeringModel(...)

Initialize the diff engine with ID-based semantic equality

diff_engine = PyDexpiDiffEngine(semantic_equality=id_equality)

Perform the diff operation

diff_result = diff_engine.diff(model1, model2)

Initialize the diff engine with a specific semantic equality function.

Parameters:

Name Type Description Default
semantic_equality SemanticEquality

The semantic equality function to use for comparing pyDEXPI objects of the same type.

required
track_order bool

Also report reorders of the composition lists pyDEXPI gives an order -- a segment's flow order (pydexpi.diff.ordering.ORDERED_COMPOSITION) -- as AttributeChange(category="composition"). Off by default: most comparisons are about content. The lists the specification declares ordered (the drawing's z-order and geometry, a vector's values, a template's component list) are compared by position whether or not this is set; every other list is compared as a multiset.

False

diff

diff(model1: EngineeringModel, model2: EngineeringModel) -> DiffResult

Calculate the diff between model1 and model2.

Parameters:

Name Type Description Default
model1 EngineeringModel

The first pyDEXPI model to compare, considered the "old model".

required
model2 EngineeringModel

The second pyDEXPI model to compare, considered the "new model".

required

Returns:

Type Description
DiffResult

The result of the diff operation.

diff_objects

diff_objects(object1: PyDexpiBaseModel, object2: PyDexpiBaseModel) -> DiffResult

Compare two subtrees -- one piece of equipment, a segment -- on their own.

This is a comparison scope: only the two subtrees are traversed and matched, and nothing outside them is looked at. That differs from filtering a whole-model diff (id_diff(..., scope=...), DiffResult.for_objects) in ways that matter:

  • An object moved across the boundary -- a valve moved out of the compared segment -- is reported as removed (or added), not as moved: its other parent is out of view.
  • The two roots are matched like any other pair. If they do not match (different ids, under id equality), every object is reported as removed on one side and added on the other.
  • Domains are classified by class, since the subtree's location in a model is unknown.

Parameters:

Name Type Description Default
object1 PyDexpiBaseModel

The root of the old subtree.

required
object2 PyDexpiBaseModel

The root of the new subtree.

required

Returns:

Type Description
DiffResult

The changes within the two subtrees.

Git projection

pydexpi.versioning.model_to_projection

model_to_projection(model: PyDexpiBaseModel, *, id_policy: IdPolicy = 'source', include_graphics: bool = True) -> str

Render model as stable, reviewable text.

Parameters:

Name Type Description Default
model PyDexpiBaseModel

Normally an EngineeringModel.

required
id_policy ('source', 'all')

Which ids to trust across revisions (see the module docstring). "all" for models maintained as JSON with stable ids; "source" (the default) for models loaded from XML, whose unidentified objects are re-minted on every load.

"source"
include_graphics bool

Include the drawing (diagram and shapeCatalogues). On by default; leave it off for a review of the engineering content alone.

True

Returns:

Type Description
str

The projection, \n-separated, ending in a newline.


MCP server extensions

The MCP server registers the extensions installed under the entry-point group pydexpi.mcp_extensions (needs the mcp extra).

pydexpi.mcp_server.extensions

How the MCP server is extended: entry-point plug-ins, and the fields they add to replies.

The core server answers questions about a model; an installed extension can add tools, prompts and resources of its own. An extension is a callable register(mcp, sessions, file_access) that adds its tools, prompts and resources to a server, declared under the entry-point group :data:ENTRY_POINT_GROUP::

[project.entry-points."pydexpi.mcp_extensions"]
my_extension = "my_package.mcp:register"

The core never imports an extension by name: :func:discover_extensions finds what is installed, and create_server registers it after the core's own tools. Registered last, its tools would be listed last; an extension that wants them listed among the core's, where a client reads them in context, says so with :func:place_tools_after, and create_server arranges the list (:func:arrange_tools) once every extension is registered.

Discovery replies can say more than the core knows how to compute, and an extension can add fields to them. The core reply calls :class:ItemFields at each place such a field goes, and an extension registers a provider for that place with :func:register_item_fields; without one the reply simply leaves the field out. A tool's description says what its reply holds, so the core declares the passage of the description that documents each place (:func:declare_field_passages), and a provider may give the passage that documents its fields: the core's description then names no field the core does not give, and the extension's is the full one. Providers are per server, so a server built without extensions never shows another server's fields.

ITEM_FIELD_KINDS module-attribute

ITEM_FIELD_KINDS = ('signal_port', 'port_delegation', 'instrumentation_activity')

McpExtension

Bases: Protocol

What an entry point of :data:ENTRY_POINT_GROUP loads: a function registering an extension.

It is called once per server, after the core's tools, with the server, its sessions and its file-access policy; a tool that touches a file goes through file_access.resolve.

discover_extensions

discover_extensions() -> list[McpExtension]

The installed MCP server extensions, in the order of their entry-point names.

An entry point that fails to load is skipped, with a warning naming it and the error: one broken plug-in must not take the server down, and nothing else would say it is missing.

register_item_fields

register_item_fields(mcp: Any, kind: str, provider: ItemFieldProvider, passage: str | None = None) -> None

Have provider add fields to mcp's discovery replies at the place kind names.

passage is what the replying tool's description says of that place with these fields (:meth:ItemFields.register).

declare_field_passages

declare_field_passages(mcp: Any, tool: str, passages: Mapping[str, str]) -> None

Declare which passage of tool's description documents each place of extension fields.

passages maps a kind (:data:ITEM_FIELD_KINDS) to the core's passage for it: dedented whole lines, found once in the description. :func:arrange_tools puts a provider's passage in its place.

place_tools_after

place_tools_after(mcp: Any, anchor: str, names: Sequence[str]) -> None

List the tools names, in that order, right after the tool anchor in mcp's list.

Applied by :func:arrange_tools, once every extension is registered, in the order the placements were made.

arrange_tools

arrange_tools(mcp: Any) -> None

Apply the tool placements and description passages made for mcp.

create_server calls it after the extensions. The SDK has no public way to order its tool list or to change a registered tool's description, so this rebuilds its tool registry (_tool_manager._tools, insertion-ordered, which is the order tools/list gives); it is the one place in pyDEXPI that does. A placement or passage naming what is not there is a programming error and raises.

CLI plug-ins

The CLI adds the commands installed under the entry-point group pydexpi.cli_plugins and diff --patch prints with the writer installed under pydexpi.patch_writers (needs the cli extra).

pydexpi.cli

Command-line interface for pyDEXPI.

Exposes a practical subset of pyDEXPI's functionality through a clean, user-friendly terminal interface built with Typer_.

Available commands
  • pydexpi info — print a model summary (equipment, lines, instruments).
  • pydexpi validate — run every validation rule, reporting file:line per finding.
  • pydexpi convert — convert between DEXPI XML and JSON (--fast skips validation).
  • pydexpi svg — render a P&ID diagram to SVG.
  • pydexpi export-table — export equipment / line / instrument tables.
  • pydexpi export-graph — export the plant graph at a chosen abstraction level.
  • pydexpi diff — compare two model states by stable id (--json, --patch).
  • pydexpi git-projection — print stable review text of a model, for Git.

Plug-ins add commands: each callable declared under the entry-point group pydexpi.cli_plugins is called with the Typer app, and diff --patch prints with the writer declared under pydexpi.patch_writers. Without an installed writer, --patch exits 2.

Every command that loads a model takes --lenient (strict=False), which a DEXPI 2.0 process export usually needs. Run pydexpi --help or pydexpi <command> --help for full option details.

.. _Typer: https://typer.tiangolo.com/

CLI_PLUGIN_GROUP module-attribute

CLI_PLUGIN_GROUP = 'pydexpi.cli_plugins'

PATCH_WRITER_GROUP module-attribute

PATCH_WRITER_GROUP = 'pydexpi.patch_writers'

discover_plugins

discover_plugins(group: str) -> list[Callable[..., Any]]

The callables installed under the entry-point group, in the order of their names.

An entry point that fails to load is skipped, with a warning naming it and the error: one broken plug-in must not take the CLI down, and nothing else would say its commands are missing.

build_app

build_app() -> typer.Typer

The CLI: the core's commands, then every installed plug-in's (:data:CLI_PLUGIN_GROUP).

A plug-in may place its commands among the core's in the help, by rearranging app.registered_commands.


Synthetic Data Generation

Generate synthetic P&ID patterns for AI/ML applications.

SyntheticPIDGenerator

Main class for generating synthetic P&ID patterns.

pydexpi.syndata.generator.SyntheticPIDGenerator

SyntheticPIDGenerator(generator_function: GeneratorFunction, capping_function: CappingFunction = None, renaming_convention: ConnectorRenamingConvention = None, max_steps=100)

The generator contains mechanisms to connect patterns to a larger pattern and collect them in pattern distributions.

Attributes:

Name Type Description
_current_pattern Pattern

This attribute stores the (partially) generated P&ID as a pattern.

generator_function GeneratorFunction

The generator function that defines the generation strategy to be applied by this generator.

capping_function CappingFunction

The capping function that defines the capping strategy to be applied by this generator.

renaming_convention ConnectorRenamingConvention

The renaming convention that renames connectors of new patterns that are included.

max_steps int

The maximum number of steps to take before early termination.

_generation_history GenerationHistory

The generation history of the generator, which stores the steps taken during the generation process.

_current_step int

Attribute keeping track of the current step the generator is in.

_termination_flag bool

Internal flag that can be used to invoke termination of the current generation process.

Constructor of a synthetic P&ID generator

Parameters:

Name Type Description Default
generator_function GeneratorFunction

The function that defines the generation logic to be followed

required
capping_function CappingFunction

The function that defines the capping logic to be followed, by default None

None
renaming_convention ConnectorRenamingConvention

The function that defines the renaming logic to be followed, by default None

None
max_steps int

The maximum amount of steps to be taken before terminating the generation, by default 100

100

generate_pattern

generate_pattern(new_pattern_label: str) -> Pattern

Top-level function to create a random dexpi flowsheet.

For this, a pattern is first initialized according to the initialization defined by the generator_function. Then, while no stopping criteria are reached, the _current_pattern is aggregated iteratively with the next_step() function. Finally, unconnected connectors are dropped by calling drop_connector() on all connectors of the pattern, and the generated pattern is returned. The _current_pattern is reset to None for the next call of generate_pattern().

Parameters:

Name Type Description Default
new_pattern_label str

The label the new pattern should have.

required

Returns:

Name Type Description
Pattern Pattern

The generated pattern.

Raises:

Type Description
AttributeError:

If the _current_pattern was already initialized before the call of this method.

PatternDistribution

Manage and load pattern distributions for synthetic generation.

pydexpi.syndata.pattern_distribution.PatternDistribution

PatternDistribution(name: str, patterns: dict[str, Pattern], probabilities: dict[str, float], connector_labels: set[str])

A PatternDistribution is a collection of Patterns that share the same properties with regards to the generation logic. The patterns are grouped for the sake of randomness and for structure. The grouping is based on similarity mainly regarding: - Outside connectors. - Transition logic. - Conceptual similarity (e.g. distillation equipment).

Note: Pattern labels in a distribution are exclusive

Attributes:

Name Type Description
name str

The name of the pattern distribution, preferably an umbrella term for the patterns contained.

patterns dict[str, Pattern]

A dictionary containing the distributions patterns as values, with the pattern labels as keys.

probabilities dict[str, float]

A dictionary containing the distributions probabilities as values, with the pattern labels as keys.

connector_labels list[str]

The labels of the connectors that each of the patterns in the distribution must contain.

Constructor of a pattern distribution. Name, distribution, and connector_labels stored as attributes. Each pattern in the distribution is validated against the connector labels.

Parameters:

Name Type Description Default
name str

The name of the distribution.

required
patterns dict[str, Pattern]

A dictionary containing the distributions patterns as values, with the pattern labels as keys.

required
probabilities dict[str, float]

A dictionary containing the distributions probabilities as values, with the pattern labels as keys.

required
connector_labels set[str]

The labels of the connectors that each pattern needs to have to be part of the distribution.

required

Raises:

Type Description
ValueError

In case a pattern of the distribution does not fulfil the connector requirements.

load classmethod

load(dir_path: Path, name: str)

Loads a distribution in the directory specified in dir_path with the name given in name, as previously saved by the save_distribution method.

Parameters:

Name Type Description Default
dir_path Path

The directory the distribution is saved in.

required
name str

The name of the distribution.

required

Returns:

Type Description
PatternDistribution

The loaded pattern distribution.

save

save(dir_path: Path)

Saves a distribution to a directory in dirpath with the folder name taken from its name attribute. All patterns are saved in a new folder with the distribution name. Metadata, specifically the distribution probabilities and connector labels, are saved along in a separate json in that folder.

Parameters:

Name Type Description Default
dir_path str

The path of the directory the pattern is stored in

required

Additional Resources