Skip to main content

Define, group, select, and validate ASP (Answer Set Programming) rule collections.

Project description

aspcompose

Define, group, select, and validate ASP (Answer Set Programming) rule collections. Solver-agnostic: rule text is opaque to the library, so any ASP dialect (clingo, DLV, ...) works. Zero runtime dependencies.

Install

pip install -e .[dev]

Quickstart

A single registry exercising all three composition axes — slots, profiles, and variants — plus a plain depends_on chain:

Group identifiers, slot-filler identifiers, and rule identifiers all use colon-delimited namespaces (base, slot_x:a, consumer:variant_1:r_1) so every string says where it lives:

from aspcompose import Rule, RuleGroup, RuleRegistry, CollectionPlan

# Plain base group: no slot, no profile, no variants.
base = RuleGroup(
    identifier="base",
    rules=(Rule(identifier="base:r_1", text="p(X) :- q(X)."),),
)

# Two interchangeable fillers of slot_x. Each carries a different profile
# so a bulk `add_profile` pulls in exactly one of them.
slot_x_a = RuleGroup(
    identifier="slot_x:a",
    rules=(Rule(identifier="slot_x:a:r_1", text="q(X) :- a(X)."),),
    depends_on=frozenset({"base"}),
    slot="slot_x",
    profiles=frozenset({"profile_a"}),
)
slot_x_b = RuleGroup(
    identifier="slot_x:b",
    rules=(Rule(identifier="slot_x:b:r_1", text="q(X) :- b(X)."),),
    depends_on=frozenset({"base"}),
    slot="slot_x",
    profiles=frozenset({"profile_b"}),
)

# Polymorphic consumer: depends on slot_x (any filler will do), carries
# both profiles so it's included under either, and declares two variants
# that both derive from the shared predicate `r(X)` — so they work the
# same regardless of which slot_x filler is active.
consumer = RuleGroup(
    identifier="consumer",
    rules=(Rule(identifier="consumer:r_1", text="r(X) :- p(X)."),),
    depends_on=frozenset({"base", "slot_x"}),
    profiles=frozenset({"profile_a", "profile_b"}),
    variants={
        "variant_1": (
            Rule(identifier="consumer:variant_1:r_1", text="u(X) :- r(X)."),
        ),
        "variant_2": (
            Rule(identifier="consumer:variant_2:r_1", text="v(X) :- r(X)."),
        ),
    },
)

# Opt-in extension (plain group, no slot or profile). Two rules show the
# `r_1`/`r_2` numbering inside a single group's namespace.
extension = RuleGroup(
    identifier="extension",
    rules=(
        Rule(identifier="extension:r_1", text="t(X) :- r(X)."),
        Rule(identifier="extension:r_2", text="s(X) :- t(X)."),
    ),
    depends_on=frozenset({"consumer"}),
)

registry = RuleRegistry()
registry.register([base, slot_x_a, slot_x_b, consumer, extension])
assert registry.validate() == []

# One plan, two outputs — variants are decided at resolve() time, so the
# same selection produces different rule sets without re-planning.
plan_a = CollectionPlan(registry)
plan_a.add_profile("profile_a")      # includes: slot_x:a, consumer
plan_a.auto_include_deps()           # pulls in concrete deps: base

for rule in plan_a.resolve(variant="variant_1"):
    print(rule.text)
# p(X) :- q(X).          (base)
# q(X) :- a(X).          (slot_x:a, filling slot_x)
# r(X) :- p(X).          (consumer, shared)
# u(X) :- r(X).          (consumer, variant_1 payload)

for rule in plan_a.resolve(variant="variant_2"):
    print(rule.text)
# p(X) :- q(X).
# q(X) :- a(X).
# r(X) :- p(X).
# v(X) :- r(X).          (consumer, variant_2 payload — same plan, new flavor)

# A different profile picks a different slot_x filler, and the extension
# is pulled in explicitly. The same variant_1 still works — variants
# don't care which filler is active, because they only depend on `r(X)`.
plan_b = CollectionPlan(registry)
plan_b.add_profile("profile_b")      # includes: slot_x:b, consumer
plan_b.add_group("extension")        # opt-in, no profile carries it
plan_b.auto_include_deps()

for rule in plan_b.resolve(variant="variant_1"):
    print(rule.text)
# p(X) :- q(X).
# q(X) :- b(X).          (slot_x:b, filling slot_x)
# r(X) :- p(X).
# u(X) :- r(X).          (consumer, variant_1 payload — unchanged)
# t(X) :- r(X).          (extension, rule r_1)
# s(X) :- t(X).          (extension, rule r_2)

What each axis does in this example:

  • slot_x picks which group defines q(X)slot_x:a or slot_x:b — while consumer depends on slot_x polymorphically, without naming either filler.
  • profile_a / profile_b are bulk toggles: one call to add_profile pulls in a coherent set of groups (here, a slot_x filler and the consumer). The extension group carries no profile and is added with add_group when wanted.
  • variant_1 / variant_2 switch consumer's emitted rule payload at resolve() time. The variants derive from the shared r(X), so the choice is independent of both the profile and the slot filler — plan_a renders cleanly under either variant, and variant_1 works the same in plan_b.
  • auto_include_deps only walks concrete depends_on edges; slot deps are left to the user (or to a profile) to resolve.

Concepts

  • Rule — a single ASP rule with an identifier, source text, and optional docs.
  • RuleGroup — rules that are always added/removed together. Declares depends_on (group identifiers or slot names), may fill a slot, may carry any number of profiles (tags), and may define per-flavor variants. Frozen.
  • Slot — an abstract role that multiple groups can fill (e.g. "input_format", filled by format_sbml or format_kgml). A plan may include at most one filler per slot, and depends_on may name a slot instead of a concrete group — that's a polymorphic dependency on "some filler of this role".
  • Profile — a free-form label carried by any number of groups (e.g. "experimental", "sbml"). plan.add_profile(name) includes every group tagged with it in a single call.
  • Variant — a format/flavor key declared inside a group: variants={"key": (Rule, ...)}. plan.resolve(variant="key") emits rules plus the matching payload, so one plan can produce N programs (one per variant key).
  • RuleRegistry — stores every known group. validate() catches unknown deps and cycles.
  • CollectionPlan — user-curated included / excluded selection. validate() returns structured issues; resolve() returns a flat, ordered rule list or raises PlanInvalidError.

Slots vs profiles vs variants

The three are orthogonal: they act on different axes of the composition problem, and a single group can use all three at once without interference.

Slot Profile Variant
Answers "which group fills this role?" "which groups go together?" "which rule flavor to emit?"
Cardinality at most one filler per slot, per plan any number of tagged groups one variant key per resolve() call
Decided at plan-composition time plan-composition time resolve() time — same plan, N outputs
In depends_on yes — polymorphic dependency no no
Conflict slot_conflict if two fillers are included none missing_variant if the key isn't found

Use cases:

  • Slot — interchangeable implementations of the same interface. An input_format slot filled by format_sbml or format_kgml; a solver_hints slot filled by clingo_hints or dlv_hints. Downstream groups write depends_on={"input_format"} and don't care which filler the user picks.
  • Profile — bulk toggles for cross-cutting themes. Tag a dozen groups scattered across features with "experimental"; flip them all on at once with plan.add_profile("experimental"). Profiles never exclude anything and never conflict — they're a user-ergonomic shortcut, not a composition constraint.
  • Variant — one group, multiple output flavors. A pathways group has shared logic in rules plus a few format-specific tail rules in variants={"sbml": ..., "kgml": ...}. The same plan yields two programs via resolve(variant="sbml") and resolve(variant="kgml") — no re-selection, no duplicate groups, no N×M explosion.

A single group can fill a slot, carry profiles, and define variants all at the same time.

Validation issues

kind Meaning
missing_dependency Included group requires a group that is not included
excluded_dependency Included group requires a group that is explicitly excluded
unfilled_slot Included group needs a slot but no group filling it is in
slot_conflict Two included groups fill the same slot
unknown_dependency A depends_on target is not registered
missing_variant Included group has variants but not for the requested key
cycle Cyclic depends_on graph (registry-level)

Testing

pytest

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aspcompose-0.1.0.tar.gz (14.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aspcompose-0.1.0-py3-none-any.whl (9.6 kB view details)

Uploaded Python 3

File details

Details for the file aspcompose-0.1.0.tar.gz.

File metadata

  • Download URL: aspcompose-0.1.0.tar.gz
  • Upload date:
  • Size: 14.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aspcompose-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5802658074710ad69e724af0475a7ffa8925006f03fa3d2c0132f80e6071ac0b
MD5 85a39435216beff4b4016bbf03c5eaff
BLAKE2b-256 5df2999502affea8e5b15a9a5b56f8b8ce1e8d65d01ee190c67b058df66d6649

See more details on using hashes here.

Provenance

The following attestation bundles were made for aspcompose-0.1.0.tar.gz:

Publisher: release.yml on adrienrougny/aspcompose

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aspcompose-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: aspcompose-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 9.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aspcompose-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7124e6d701566daeb97e5b2acc2b18e5fb4b4a694fda73d5a8202cb0ac87e71e
MD5 657828de029bc867e8c751b3558a4767
BLAKE2b-256 1d2ee0f23e5924b76f333d27cd3be1f89a6ea66ab85aa1cd3695c712e9858c76

See more details on using hashes here.

Provenance

The following attestation bundles were made for aspcompose-0.1.0-py3-none-any.whl:

Publisher: release.yml on adrienrougny/aspcompose

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page