The gecode-iris package binds the Rust reference implementation of Iris;
import it as gecode_iris. Parsing, reading, errors, JSON and dzn all come
from the Rust reader, so Python reads every input exactly as the
CLI does. The package needs Python 3.10 or newer and includes
type stubs. The Python API reference lists every public
class and method, and the spec builder reference
lists those of gecode_iris.build.
The package is on PyPI. Add it to a uv
project, install it into an existing environment, or run the iris command
without installing it:
Terminal window
uvaddgecode-iris
uvpipinstallgecode-iris
uvx--fromgecode-irisiris--help
To use unreleased changes, build the package from an Iris checkout instead,
which needs the Rust toolchain. With your uv project beside a checkout named
iris, run:
Terminal window
uvadd../iris/python
Package versions and language versions are separate; the current package is
0.1.0 and implements Iris 0.1. numpy is optional and never installed with the
package.
The examples on this page are in
python/examples,
and a test runs each one against the built package and compares its output.
They read files from the format library, so run them from the root of an
Iris checkout.
data = spec.read(b"3 10 20 30", origin="input.txt")
print(data["n"])
print(data.ints("row"))
print(data["row"])
3
[10, 20, 30]
[10, 20, 30]
Spec.parse parses and checks a reader spec given as str or bytes. The
declaration reads n, then a row of n integers. spec.read reads input
bytes; the origin names, such as example.iris and input.txt, appear in
error messages. A Spec can read any number of inputs, and threads can share
it.
data.entries lists the entries in the order the reader spec defines them.
data[name] returns an entry’s value as Python objects: an int, float or
str at depth 0, and nested lists at greater depths. Each access copies the
value.
data.ints(name, *path) returns the integer row that the index path selects.
A negative index counts from the end of its row, as in a list. data.to_dict()
returns every entry. An unknown name raises KeyError, an index outside its
row raises IndexError, and a selection of the wrong type raises
TypeError.
When numpy is installed and an array is rectangular, data.array(name, *path)
returns a numpy.ndarray of int64, float64 or str_ elements, with one
dimension per level. Otherwise it returns the same nested lists as
data[name], so check the type before using numpy methods:
A language error raises gecode_iris.Error. Its code is the stable
error code, and line, column and origin give
the input location when there is one, and otherwise the reader spec location.
Any of them can be None. path is the record path of a failed check over
records. str(error) is the same diagnostic that the CLI prints, and
error.to_json() returns the canonical error document.
E-EXPECTED-VALUE formats/dimacs-cnf/cases/literal-out-of-range/input.txt 2 3
expected an int in -3..3, found '4'
formats/dimacs-cnf/cases/literal-out-of-range/input.txt:2:3: expected an int in -3..3, found '4'
2 | 1 4 0
| ^
while reading: repeat [1 of 1] > clause
spec: formats/dimacs-cnf/dimacs-cnf.iris:7
Host failures raise built-in exceptions and have no error code: a missing file
raises FileNotFoundError, and other file access failures raise other
OSError subclasses. Python types that the API does not accept, such as an
integer parameter value, raise TypeError.
gecode_iris.build writes a reader spec from Python. Every statement that
defines an entry returns a typed handle, and data[handle] returns the entry
with the handle’s type, so a type checker such as pyright or mypy sees int
for n_jobs and list[list[int]] for machine, and flags a misspelled handle.
A builder adds no features of its own: build() prints Iris text and parses it
as Spec.parse does, so the language’s checks decide what is valid, and
source() returns the text. The
spec builder reference lists every statement of a
scope with the typed handle of each overload.
The examples below are in
python/examples,
one builder for each format in the format library. A test reads
every case of each format through its builder and compares the data or the
error with the case’s expected results, then runs each program and compares its
output with the output shown here. The programs read files from the format
library, so run them from the root of an Iris checkout, as in
python python/examples/build_jobshop.py.
print(f"\n{error.code} at {error.origin}:{error.line}:{error.column}")
print(error.message)
# The statement's call site: the line of the i.check call above.
site = error.call_site
assert site isnotNone
print(f"call site: {Path(site.filename).name}:{site.line}, in {site.function}()")
if __name__ =="__main__":
main()
handmade-or: 3 jobs on 3 machines
job 0: machines [0, 1, 2], durations [2, 3, 1]
job 1: machines [1, 2, 0], durations [1, 4, 2]
job 2: machines [2, 0, 1], durations [3, 1, 2]
E-CHECK-FAILED at formats/jobshop/cases/duplicate-machine/input.txt:3:1
check failed for record 1 of 'jobs': a job uses a machine twice; each job must use every machine once
call site: build_jobshop.py:43, in jobshop()
Blocks are with statements: b.instances(), i.sections() and
a.repeat(n_jobs) each open a scope, and the statements inside the with go
into it. A pattern is a tuple of text and handles, as in
(n_jobs, " ", n_machines). job.record is the handle of each record of a
repeat, which the builder names itself with each record_1: jobs[machine]
looks up a field and prints record_1.machine, and
ib.sum(jobs, lambda j: ...) prints a comprehension over the records.
job.as_name("jobs") names the list of records, as in repeat n_jobs as jobs,
for error messages.
Python operators build expressions: <=, ==, + and * work on handles as
they do in Iris, and &, | and ~ stand for &&, || and !, since
Python’s and, or and not cannot be overloaded. A range is
ib.int_range(0, below=n_machines) for Iris 0..<n_machines, or
ib.int_range(0, n) for 0..n.
data[handle] returns the Python type of the entry’s value type and depth: an
int for n_jobs, a str | None for the name, which only some files give,
and a list[list[int]] for machine and duration, which have one value per
step of each job. A handle names its entry, value type and depth, so a lookup
cannot read the wrong entry, and a handle from a different spec whose entry has
another type raises TypeError instead of returning the wrong type.
Collecting the handles in a NamedTuple with the spec, as Jobshop does, gives
the rest of a program one typed object to read with.
Every statement records the line of the call that wrote it. When a check fails
while reading, the error has the input location, and error.call_site names the
i.check line in build_jobshop.py, with its file name and function. A
gecode_iris.build.BuildError from build() has the same call site. A
statement in a helper function records the line in the helper, unless the
helper has the @ib.track_caller decorator, as clue_lines below does.
A function that takes a scope and returns handles is a reusable piece of a
reader spec. The nonogram builder reads the clues of the rows and the columns
with one helper, and builds two pairs of checks with two expression helpers.
build_nonogram.py
from __future__ import annotations
from typing import NamedTuple
import gecode_iris as iris
import gecode_iris.build as ib
classNonogram(NamedTuple):
"""A nonogram reader: the board size and the clues of each row and column."""
A builder rejects a second definition of a name, so a helper called twice by
mistake fails build() instead of quietly replacing an entry. That is why
clue_lines takes the entry name from its caller. The TSPLIB builder below
uses the same idea for its two coordinate sections.
time = b.define_type("time", ib.int_range(0, ib.div(ib.gecode.INT_MAX, 2)))
with b.repeat(n) as node:
node.as_name("nodes")
ready = node.read("ready", time)
due = node.read("due", time)
nodes = node.record
b.check(nodes[ready] <= nodes[due], "the time window of a node is invalid")
b.check(nodes.list[0][ready] ==0, "the depot (node 0) must open at time 0")
return Tsptw(b.build(), n, dist, ready, due)
defmain() -> None:
problem = tsptw()
data = problem.spec.read("formats/tsptw/cases/three-nodes/input.txt")
print(f"{data[problem.n]} nodes")
windows =zip(data[problem.ready], data[problem.due], strict=True)
for node, (distances, (ready, due)) inenumerate(zip(data[problem.dist], windows, strict=True)):
print(f"node {node}: open {ready}..{due}, distances {distances}")
if __name__ =="__main__":
main()
3 nodes
node 0: open 0..20, distances [0, 2, 4]
node 1: open 2..8, distances [3, 0, 2]
node 2: open 5..12, distances [4, 3, 0]
A TSPLIB file gives either a distance matrix or coordinates. t.set gives an
optional entry a default while keeping its handle’s type, b.if_ and
b.else_ take the checks that apply to each kind of file, and one_of is an
ordinary Python function that builds an expression:
build_tsplib.py
from __future__ import annotations
from typing import NamedTuple
import gecode_iris as iris
import gecode_iris.build as ib
classTsplib(NamedTuple):
"""A TSPLIB reader. A file gives either an explicit distance matrix or node
coordinates, from which the distances follow."""
spec: iris.Spec
name: ib.StrField[str|None]
n: ib.IntField[int]
edge_weight_type: ib.StrField[str|None]
weight: ib.IntField[list[list[int]] |None]
x: ib.FloatField[list[float] |None]
y: ib.FloatField[list[float] |None]
classCoordinates(NamedTuple):
"""The entries of one section of numbered coordinates."""
A reader spec written as an .iris file has no builder, but iris gen --lang python SPEC prints a module with a typed handle for each of its entries:
Terminal window
irisgen--langpythonformats/jobshop/jobshop.iris
"""Typed handles of a reader spec, generated by `iris gen --lang python`."""
import gecode_iris.build as ib
name = ib.declared("name", ib.StrField[str | None])
n_jobs = ib.declared("n_jobs", ib.IntField[int])
n_machines = ib.declared("n_machines", ib.IntField[int])
machine = ib.declared("machine", ib.IntField[list[list[int]]])
duration = ib.declared("duration", ib.IntField[list[list[int]]])
Save the output as a module, such as jobshop_view.py, and read data with
data[jobshop_view.machine] as with a builder’s handles; a type checker sees
the same types. ib.declared makes each
handle from the entry’s name and its handle type. An entry whose value type or
depth differs between the places that define it gets ib.DynField, which
returns any value or None. A declared handle reads data from any spec with
that entry, and a lookup checks the value type and depth, but no builder accepts
it, since no builder made it.
The package installs the iris command. It runs the same command line as the
Rust iris binary, with the same options, output and exit statuses, so the
CLI guide applies unchanged. In an environment with the package
installed, from the root of an Iris checkout:
formats/dimacs-cnf/cases/three-variables/input.txt [dimacs-cnf]
vars: int 3
clauses: int 3
clause: int array 3 x 1..3
You are visitor 01402520 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.