Skip to content

Rules

pd2af.asp.rules

ASP rule composition for the pd2af transformation modes.

Which groups a mode is made of is declared by the mode itself (:mod:pd2af.modes); this module owns the groups and composes the program text for a named mode. The rules are organised in layers:

  • topology:core (shared) — structural helpers: isSubunit, hasActiveDescendantSubunit.
  • topology:top_level (the complex-keeping modes normal and keep-reactions) — resolvesToTopLevel(SPECIES, TOPLEVEL) resolves every species to its outermost top-level entity, so a subunit is keyed by (and its influences routed to) its top-level complex rather than itself.
  • preparation (a slot: exactly one filler group per mode, and every consumer of the activity-key predicates depends on the slot rather than on a particular filler) — emits hasActivityCarrier(RAW_SPECIES, ACTIVITY_BEARER) and hasActivityKey(ACTIVITY_BEARER, KEY) where KEY is one of two per-species wrappers: keptSpeciesKey/1 (top-level species, and the top-level complex a subunit resolves to) or promotedSubunitKey/1 (a subunit promoted to top level when its complex is dissolved in the no-complex mode). PTM stripping, which merges the forms of the same base entity unless keep_species is set, happens at the build stage, not in the key.
  • influences:core (shared) — emits new(activity(KEY)) and new(positivelyInfluences(...)) / new(negativelyInfluences(...)) from hasActivityCarrier / hasActivityKey. The inference layers on top of it — multi-hop paths:chaining, influences:consumption and influences:binding_activation — are carried by the two path-inference modes only.

The keep-reactions mode reuses that whole scaffolding and swaps discovery and inference for two rules: every species is an activity (keep_reactions:activity) and every (reactant, product) pair of a reaction is a positive influence (keep_reactions:influences). It keeps the single-hop modulation influences of paths:core and drops the inference layers.

Functions:

Name Description
build_program

Return the composed ASP program text for a mode and an input language.

build_registry

Register the built-in rule groups plus every group the modes define.

get_excludable_groups

Return (excludable, mandatory) group ids for a transformation mode.

build_program

build_program(mode: TransformationMode, language: Language = CELLDESIGNER, exclude_groups: tuple[str, ...] = (), exclude_rules: tuple[str, ...] = ()) -> str

Return the composed ASP program text for a mode and an input language.

The mode names the groups its program is made of; the input language is an aspcompose variant. Language-agnostic rules live in each group's rules and are emitted for every language; language-specific rules live in variants={Language.CELLDESIGNER: ..., Language.SBGN_PD: ...} and are selected here by resolve(variant=language).

exclude_groups drops whole rule groups (the primary toggle: each excludable group is a coherent functional unit); excluding a group that another included group depends on raises ValueError with a friendly message. exclude_rules is a finer scalpel that drops individual rules by identifier (used for the whole-with-scalpel table groups); an id that does not name a resolved rule raises ValueError.

Source code in src/pd2af/asp/rules.py
def build_program(
    mode: pd2af.modes.TransformationMode,
    language: Language = Language.CELLDESIGNER,
    exclude_groups: tuple[str, ...] = (),
    exclude_rules: tuple[str, ...] = (),
) -> str:
    """Return the composed ASP program text for a mode and an input language.

    The mode names the groups its program is made of; the input language is an
    aspcompose *variant*. Language-agnostic rules live in each group's
    ``rules`` and are emitted for every language; language-specific rules live
    in ``variants={Language.CELLDESIGNER: ..., Language.SBGN_PD: ...}`` and are selected here
    by ``resolve(variant=language)``.

    ``exclude_groups`` drops whole rule groups (the primary toggle: each
    excludable group is a coherent functional unit); excluding a group that
    another included group depends on raises ``ValueError`` with a friendly
    message. ``exclude_rules`` is a finer scalpel that drops individual rules
    by identifier (used for the whole-with-scalpel table groups); an id that
    does not name a resolved rule raises ``ValueError``.
    """
    plan = CollectionPlan(build_registry())
    for group_id in mode.rule_group_ids:
        plan.add_group(group_id)
    for group_id in exclude_groups:
        plan.exclude_group(group_id)
    try:
        resolved = plan.resolve(variant=language)
    except PlanInvalidError as error:
        raise ValueError(_friendly_exclusion_message(error)) from error
    if exclude_rules:
        resolved_ids = {rule.identifier for rule in resolved}
        unknown = sorted(set(exclude_rules) - resolved_ids)
        if unknown:
            raise ValueError("unknown rule id(s): " + ", ".join(unknown))
        excluded = set(exclude_rules)
        resolved = [rule for rule in resolved if rule.identifier not in excluded]
    return "\n".join(rule.text for rule in resolved)

build_registry

build_registry(modes: Iterable[TransformationMode] | None = None) -> RuleRegistry

Register the built-in rule groups plus every group the modes define.

modes is an iterable of :class:pd2af.modes.TransformationMode, defaulting to every known mode. Passing a narrower set — the built-in modes alone, say — yields a registry that ignores contributed groups.

Two modes may not define the same group id, and every group a mode references must be registered; either raises naming the mode.

Source code in src/pd2af/asp/rules.py
def build_registry(
    modes: collections.abc.Iterable["pd2af.modes.TransformationMode"] | None = None,
) -> RuleRegistry:
    """Register the built-in rule groups plus every group the modes define.

    ``modes`` is an iterable of :class:`pd2af.modes.TransformationMode`,
    defaulting to every known mode. Passing a narrower set — the built-in modes
    alone, say — yields a registry that ignores contributed groups.

    Two modes may not define the same group id, and every group a mode
    references must be registered; either raises naming the mode.
    """
    if modes is None:
        modes = pd2af.modes.get_transformation_modes().values()
    modes = list(modes)
    contributed_groups = []
    mode_by_contributed_group_id: dict[str, str] = {}
    for mode in modes:
        for group in mode.rule_group_definitions:
            already_contributed_by = mode_by_contributed_group_id.get(group.identifier)
            if already_contributed_by is not None:
                raise RuntimeError(
                    f"transformation modes {already_contributed_by!r} and "
                    f"{mode.name!r} both define rule group "
                    f"{group.identifier!r}"
                )
            mode_by_contributed_group_id[group.identifier] = mode.name
            contributed_groups.append(group)
    registry = RuleRegistry()
    registry.register(list(_BUILTIN_GROUPS) + contributed_groups)
    for mode in modes:
        for group_id in mode.rule_group_references:
            if group_id not in registry.groups:
                raise RuntimeError(
                    f"transformation mode {mode.name!r} references rule group "
                    f"{group_id!r} in its rule_group_references, which no mode "
                    f"defines and which is not a built-in group"
                )
    issues = registry.validate()
    if issues:
        raise RuntimeError(
            "pd2af rule registry is invalid:\n"
            + "\n".join(f"  - {issue}" for issue in issues)
        )
    return registry

get_excludable_groups

get_excludable_groups(mode_name: str) -> tuple[frozenset[str], frozenset[str]]

Return (excludable, mandatory) group ids for a transformation mode.

A group is mandatory when another included group depends on it (the dependency graph forbids excluding it — resolve would raise excluded_dependency); every other included group is an excludable leaf that --exclude-group can drop cleanly. A dependency on a slot resolves to the included group filling it, so a filler is mandatory exactly like a directly named dependency.

Source code in src/pd2af/asp/rules.py
def get_excludable_groups(mode_name: str) -> tuple[frozenset[str], frozenset[str]]:
    """Return ``(excludable, mandatory)`` group ids for a transformation mode.

    A group is *mandatory* when another included group depends on it (the
    dependency graph forbids excluding it — ``resolve`` would raise
    ``excluded_dependency``); every other included group is an *excludable*
    leaf that ``--exclude-group`` can drop cleanly. A dependency on a *slot*
    resolves to the included group filling it, so a filler is mandatory
    exactly like a directly named dependency.
    """
    registry = build_registry()
    mode = pd2af.modes.get_transformation_mode(mode_name)
    included = frozenset(mode.rule_group_ids)
    depended_on: set[str] = set()
    for group_id in included:
        for dependency in registry.groups[group_id].depends_on:
            if dependency in included:
                depended_on.add(dependency)
            else:
                depended_on.update(
                    registry.slots.get(dependency, frozenset()) & included
                )
    mandatory = frozenset(depended_on)
    excludable = included - mandatory
    return excludable, mandatory