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_xpicks which group definesq(X)—slot_x:aorslot_x:b— whileconsumerdepends onslot_xpolymorphically, without naming either filler.profile_a/profile_bare bulk toggles: one call toadd_profilepulls in a coherent set of groups (here, aslot_xfiller and theconsumer). Theextensiongroup carries no profile and is added withadd_groupwhen wanted.variant_1/variant_2switchconsumer's emitted rule payload atresolve()time. The variants derive from the sharedr(X), so the choice is independent of both the profile and the slot filler —plan_arenders cleanly under either variant, andvariant_1works the same inplan_b.auto_include_depsonly walks concretedepends_onedges; 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. Declaresdepends_on(group identifiers or slot names), may fill aslot, may carry any number ofprofiles(tags), and may define per-flavorvariants. Frozen.- Slot — an abstract role that multiple groups can fill (e.g.
"input_format", filled byformat_sbmlorformat_kgml). A plan may include at most one filler per slot, anddepends_onmay 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")emitsrulesplus 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-curatedincluded/excludedselection.validate()returns structured issues;resolve()returns a flat, ordered rule list or raisesPlanInvalidError.
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_formatslot filled byformat_sbmlorformat_kgml; asolver_hintsslot filled byclingo_hintsordlv_hints. Downstream groups writedepends_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 withplan.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
pathwaysgroup has shared logic inrulesplus a few format-specific tail rules invariants={"sbml": ..., "kgml": ...}. The same plan yields two programs viaresolve(variant="sbml")andresolve(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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5802658074710ad69e724af0475a7ffa8925006f03fa3d2c0132f80e6071ac0b
|
|
| MD5 |
85a39435216beff4b4016bbf03c5eaff
|
|
| BLAKE2b-256 |
5df2999502affea8e5b15a9a5b56f8b8ce1e8d65d01ee190c67b058df66d6649
|
Provenance
The following attestation bundles were made for aspcompose-0.1.0.tar.gz:
Publisher:
release.yml on adrienrougny/aspcompose
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aspcompose-0.1.0.tar.gz -
Subject digest:
5802658074710ad69e724af0475a7ffa8925006f03fa3d2c0132f80e6071ac0b - Sigstore transparency entry: 1472819170
- Sigstore integration time:
-
Permalink:
adrienrougny/aspcompose@7ea60faa00821f7e929e397b511a638c4f5d7769 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/adrienrougny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7ea60faa00821f7e929e397b511a638c4f5d7769 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7124e6d701566daeb97e5b2acc2b18e5fb4b4a694fda73d5a8202cb0ac87e71e
|
|
| MD5 |
657828de029bc867e8c751b3558a4767
|
|
| BLAKE2b-256 |
1d2ee0f23e5924b76f333d27cd3be1f89a6ea66ab85aa1cd3695c712e9858c76
|
Provenance
The following attestation bundles were made for aspcompose-0.1.0-py3-none-any.whl:
Publisher:
release.yml on adrienrougny/aspcompose
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aspcompose-0.1.0-py3-none-any.whl -
Subject digest:
7124e6d701566daeb97e5b2acc2b18e5fb4b4a694fda73d5a8202cb0ac87e71e - Sigstore transparency entry: 1472819378
- Sigstore integration time:
-
Permalink:
adrienrougny/aspcompose@7ea60faa00821f7e929e397b511a638c4f5d7769 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/adrienrougny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7ea60faa00821f7e929e397b511a638c4f5d7769 -
Trigger Event:
push
-
Statement type: