Metadata-Version: 2.4
Name: aspalchemy
Version: 1.0.0
Summary: A Python ORM interface for building and solving clingo ASP programs
Author: Jolyon Bloomfield
Author-email: Jolyon Bloomfield <jolyonb84@gmail.com>
License-Expression: MIT
License-File: LICENSE.txt
Requires-Dist: clingo>=5.8.0,<6
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# ASPAlchemy

ASPAlchemy is a Python library for building clingo ASP (Answer Set Programming) programs with a clean, object-oriented interface.

## Term Hierarchy

The library is built around a rich hierarchy of term types that represent different ASP constructs:

- `Term` (abstract base class)
  - `ComparableTerm` (abstract: usable in comparisons; provides `==`, `!=`, `<`, `<=`, `>`, `>=`)
    - `Value` (abstract, for basic values — also a `BasicTerm`)
      - `Variable` (e.g., `X`, `Y`)
      - `ConstantBase` (abstract)
        - `Number` (numeric constants, e.g., `42`)
        - `String` (string literals, e.g., `"hello"`)
        - `DefinedConstant` (#const-defined constants, e.g., `max_size`)
        - `Supremum` / `Infimum` (`#sup`/`#inf`, the ordering's end markers; `SUP` and `INF` are the singletons)
    - `Expression` (arithmetic expressions, e.g., `X+Y*2`)
    - `AggregateBase` (abstract marker so core can recognize aggregates)
      - `Aggregate` (abstract)
        - `Count`, `Sum`, `SumPlus`, `Min`, `Max`
  - `BasicTerm` (abstract, can be direct predicate arguments: `Value`, `Predicate`, `Pool`)
    - `Predicate` (e.g., `person(john, 42)`)
    - `Pool` (abstract)
      - `RangePool` (e.g., `1..5`)
      - `ExplicitPool` (e.g., `(1;3;5)`)
  - `Negatable` (abstract mixin: `~` builds `not term` on atoms; on plain comparisons it builds the complement — see `Not`)
    - `Comparison` (comparisons, e.g., `X < Y`)
    - `DefaultNegation` (default negation, e.g., `not p(X)`)
  - `ConditionalLiteral` (e.g., `p(X) : q(X)`)
  - `Choice` (e.g., `{ p(X) : q(X) }`)

## Core Concepts

### Values

Values represent basic elements in an ASP program. Plain Python literals coerce
automatically wherever terms are expected — an int becomes an ASP number, a str
becomes a quoted ASP string. Variables you construct by hand, and bare atoms
(the `n` in `direction(n)`) are zero-arity predicates:

```python
from aspalchemy import Predicate, Variable

X = Variable("X")  # an ASP variable
n = Predicate.define("n", [], show=False)()  # a bare atom: n, distinct from the string "n"
```

### Predicates

Declare predicates as classes; fields are statically checked, so typos and
missing arguments are type errors, and instances autocomplete:

```python
from aspalchemy import Predicate, PredicateField

class Person(Predicate):
    name: PredicateField
    age: PredicateField

john = Person(name="john", age=30)
mary = Person(name="mary", age=25)
```

The ASP name defaults to the class name, snake-cased (HasSymbol becomes
has_symbol); class kwargs override it and set namespacing and visibility (`class Pipe(Predicate, name="pipe_seg",
show=False)`). When the schema is only known at runtime, build the same thing
dynamically:

```python
Edge = Predicate.define("edge", ["a", "b"])
```

For fully typed fields, annotate them as `Field[int]`, `Field[str]`, or
`Field[SomePredicate]`. Writes accept rule terms (Variables, Expressions) as
well as ground values, and are validated per field; reads are plain Python
values, statically typed — so solution atoms come back as real ints and strs:

```python
from aspalchemy import ASPProgram, Field, Predicate

class Score(Predicate):
    player: Field[str]
    points: Field[int]

program = ASPProgram()
program.fact(Score(player="ada", points=3), Score(player="ben", points=5))
model = program.solve().first()  # raises UnsatisfiableError if there is no model
total = sum(score.points for score in model.atoms(Score))  # plain ints
assert total == 8
```

### Rules

Build rules with clear syntax:

```python
from aspalchemy import ANY, ASPProgram, Variable

program = ASPProgram()
X, Y = Variable("X"), Variable("Y")
Adult = Predicate.define("adult", ["name"])

# Facts
program.fact(john)
program.fact(mary)

# Rules
program.when(Person(name=X, age=Y), Y >= 18).derive(Adult(name=X))

# Constraints
program.forbid(Person(name=ANY, age=Y), Y < 0)
```

### Comparisons

Compare terms:

```python
X < Y  # Creates a comparison X < Y
```

### Aggregates

Use aggregates for advanced computations:

```python
from aspalchemy import ANY, Count, Variable

X = Variable("X")
count = Count(X, Person(name=X, age=ANY)) > 5
```

### Solving

Solve your ASP program:

```python
# Generate ASP code
asp_code = program.render()
print(asp_code)

# Solve
result = program.solve()  # a lazy stream: consume the models you want
for model in result:
    print("Solution found:")
    for adult in model.atoms(Adult):
        print(f"  {adult}")
print(f"Satisfiable: {result.satisfiable}")
```

### Diagnostics point at your code

Every statement remembers the Python line that authored it. A `when()` left
unclosed reports where it was opened; grounding diagnostics from clingo carry
a "generated by file:line" note back to your source;
`program.render(annotate=True)` appends a trailing `% file:line` comment to
each rule — without changing line numbers, so the annotated text doubles as
a map from raw-clingo error lines back to your source; and when a grounding
is slow or huge, `program.ground().analyze_grounding()` reports ground-atom
counts per signature, largest first, each joined back to the lines that
derive it. Frameworks built on
aspalchemy call `register_skip_package()` (whole package, or per-module dotted
prefixes) so locations point at *their* caller's code; mark a single helper
with `@attribute_to_caller` to attribute its statements to its call sites.
Switch capture off with `ASPProgram(source_locations=False)` when
generating rules in bulk.

## Requirements

- Python 3.14+
- clingo 5.8+

## License

MIT License
