GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Iris
  • Introduction
  • Tutorial
  • 1. Read DIMACS CNF
  • 2. Read TSPLIB sections
  • 3. Read job shop records and checks
  • Guides
  • Patterns
  • Sections
  • Records and checks
  • Several instances
  • Parameters
  • Errors
  • Performance

This view

  • Original page
  • New address

GeezerVison is showing iris.gecode.dev — Python

Python

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.

Install the package

Section titled “Install the package”

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
uv add gecode-iris
uv pip install gecode-iris
uvx --from gecode-iris iris --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
uv add ../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.

Parse a reader spec and read input

Section titled “Parse a reader spec and read input”

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.

read.py
import gecode_iris as iris
spec = iris.Spec.parse("n:int row[n]:int", origin="example.iris")
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.

Read files, instances and parameters

Section titled “Read files, instances and parameters”

Spec.load reads a reader spec file. spec.read and spec.catalog take a path as str or os.PathLike; a str is always a path, never input text.

files.py
import gecode_iris as iris
spec = iris.Spec.load("formats/jobshop/jobshop.iris")
path = "formats/jobshop/cases/or-library/input.txt"
catalog = spec.catalog(path)
print(catalog.to_dict())
data = spec.read(path, params={"instance": "handmade-or"})
print(data["n_jobs"], data["n_machines"])
{'instances': 1, 'name': ['handmade-or']}
3 3

spec.catalog lists the instances in a file as data. The instance parameter selects one instance during a read, by name or by its number, counting from

  1. Parameter values are strings, as the Specification defines; pass {"instance": "2"}, not {"instance": 2}.

Access data

Section titled “Access data”

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.

access.py
import gecode_iris as iris
spec = iris.Spec.load("formats/dimacs-cnf/dimacs-cnf.iris")
data = spec.read("formats/dimacs-cnf/cases/three-variables/input.txt")
for entry in data.entries:
print(entry)
print("clause" in data, "model" in data)
print(data["clause"])
print(data.ints("clause", -1))
print(data.to_dict())
Entry(name='vars', value_type='int', depth=0)
Entry(name='clauses', value_type='int', depth=0)
Entry(name='clause', value_type='int', depth=2)
True False
[[1, -2], [2, 3, -1], [-3]]
[-3]
{'vars': 3, 'clauses': 3, 'clause': [[1, -2], [2, 3, -1], [-3]]}

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:

arrays.py
# Needs numpy.
import numpy as np
import gecode_iris as iris
spec = iris.Spec.load("formats/jobshop/jobshop.iris")
data = spec.read("formats/jobshop/cases/jsplib/input.txt")
duration = data.array("duration")
assert isinstance(duration, np.ndarray) # a ragged array stays nested lists
print(duration.shape, duration.dtype)
print(duration.sum(axis=1))
print(data.array("duration", 0))
(3, 3) int64
[9 6 9]
[3 2 4]

JSON and dzn

Section titled “JSON and dzn”

data.to_json() returns the canonical JSON document and data.to_dzn() the MiniZinc data of the dzn appendix. Both end in a newline.

output.py
import gecode_iris as iris
spec = iris.Spec.load("formats/dimacs-cnf/dimacs-cnf.iris")
data = spec.read("formats/dimacs-cnf/cases/three-variables/input.txt")
print(data.to_dzn(), end="")
print(data.to_json(), end="")
vars = 3;
clauses = 3;
% clause: array[int] of tuple(array[int] of int)
clause = [([1, -2],), ([2, 3, -1],), ([-3],)];
{"iris":"0.1","entries":[{"name":"vars","type":"int","depth":0,"value":3},{"name":"clauses","type":"int","depth":0,"value":3},{"name":"clause","type":"int","depth":2,"value":[[1,-2],[2,3,-1],[-3]]}]}

Handle errors

Section titled “Handle errors”

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.

errors.py
import gecode_iris as iris
spec = iris.Spec.load("formats/dimacs-cnf/dimacs-cnf.iris")
try:
spec.read("formats/dimacs-cnf/cases/literal-out-of-range/input.txt")
except iris.Error as error:
print(error.code, error.origin, error.line, error.column)
print(error.message)
print(error)
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.

Build a reader spec

Section titled “Build a reader spec”

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.

The job shop builder

Section titled “The job shop builder”

This builder writes the job shop reader spec. main reads an instance selected by name, then a file whose check fails.

build_jobshop.py
from __future__ import annotations
from pathlib import Path
from typing import NamedTuple
import gecode_iris as iris
import gecode_iris.build as ib
class Jobshop(NamedTuple):
"""A job shop reader and the entries a scheduler needs."""
spec: iris.Spec
name: ib.StrField[str | None]
n_jobs: ib.IntField[int]
n_machines: ib.IntField[int]
machine: ib.IntField[list[list[int]]]
duration: ib.IntField[list[list[int]]]
def jobshop() -> Jobshop:
"""Reads OR-Library and JSPLIB files: a header line with the numbers of
jobs and machines, then one line per job of machine and duration pairs."""
b = ib.SpecBuilder("jobshop")
b.comment("#", "+++")
with b.instances() as i:
name = i.optional_capture("name", ib.word)
n_jobs = i.capture("n_jobs", ib.at_least(1))
n_machines = i.capture("n_machines", ib.at_least(1))
with i.sections(skip_other=True) as arms:
arms.arm(("instance ", name), optional=True)
with arms.arm((n_jobs, " ", n_machines), last=True) as a:
a.check(
n_jobs <= ib.div(ib.div(ib.gecode.INT_MAX - 2, 2), n_machines),
"there are too many jobs and machines",
)
with a.repeat(n_jobs) as job:
job.as_name("jobs") # error messages name the records
with job.repeat(n_machines) as step:
machine = step.read("machine", ib.int_range(0, below=n_machines))
duration = step.read("duration", ib.at_least(0))
jobs = job.record
i.check(
ib.distinct(jobs[machine]),
"a job uses a machine twice; each job must use every machine once",
)
i.check(
ib.sum(jobs, lambda j: ib.sum(j[duration])) <= ib.gecode.INT_MAX,
"the total duration is too large",
)
return Jobshop(b.build(), name, n_jobs, n_machines, machine, duration)
def main() -> None:
shop = jobshop()
data = shop.spec.read(
"formats/jobshop/cases/or-library/input.txt", params={"instance": "handmade-or"}
)
name = data[shop.name] or "unnamed"
print(f"{name}: {data[shop.n_jobs]} jobs on {data[shop.n_machines]} machines")
steps = zip(data[shop.machine], data[shop.duration], strict=True)
for job, (machines, durations) in enumerate(steps):
print(f"job {job}: machines {machines}, durations {durations}")
try:
shop.spec.read(
"formats/jobshop/cases/duplicate-machine/input.txt",
params={"instance": "handmade-invalid"},
)
except iris.Error as error:
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 is not None
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.

Typed data

Section titled “Typed data”

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.

Errors name your code

Section titled “Errors name your code”

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 helper used twice

Section titled “A helper used twice”

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
class Nonogram(NamedTuple):
"""A nonogram reader: the board size and the clues of each row and column."""
spec: iris.Spec
width: ib.IntField[int]
height: ib.IntField[int]
row: ib.IntField[list[list[int]]]
col: ib.IntField[list[list[int]]]
class Clues(NamedTuple):
"""The clue lines of one side of the board."""
lines: ib.Record
clues: ib.IntField[list[list[int]]]
@ib.track_caller
def clue_lines(s: ib.Scope0[ib.Required], count: ib.IntField[int], name: str) -> Clues:
"""Reads ``count`` lines of comma-separated clues into the entry ``name``."""
with s.repeat(count) as line:
clues = line.list(name, ib.at_least(0).until(ib.eol))
return Clues(line.record, clues)
def zero_stands_alone(clues: ib.IntField[list[list[int]]]) -> ib.IntExpr:
"""A 0 clue says the line is empty, so it must stand alone."""
return (ib.count(clues) <= 1) | (ib.min(clues) > 0)
def clues_fit(clues: ib.IntField[list[list[int]]], length: ib.IntField[int]) -> ib.IntExpr:
"""The clues and one gap between each pair of them fit into ``length``."""
return ib.sum(clues) + ib.count(clues) - 1 <= length
def nonogram() -> Nonogram:
b = ib.SpecBuilder("nonogram")
b.separators(",")
width = b.capture("width", ib.at_least(1))
height = b.capture("height", ib.at_least(1))
with b.sections(skip_other=True) as s:
s.arm(("width ", width))
s.arm(("height ", height))
with s.arm("rows") as a:
rows = clue_lines(a, height, "row")
with s.arm("columns") as a:
cols = clue_lines(a, width, "col")
row, col = rows.clues, cols.clues
b.check(height <= ib.div(1_000_000, width), "the board must have at most a million fields")
b.check(
(ib.size(rows.lines) == height) & (ib.size(cols.lines) == width),
"the numbers of rows and columns differ from the board size",
)
b.check(zero_stands_alone(row), "a 0 must be the only clue of its row")
b.check(zero_stands_alone(col), "a 0 must be the only clue of its column")
b.check(clues_fit(row, width), "the clues of a row do not fit into the width of the board")
b.check(clues_fit(col, height), "the clues of a column do not fit into the height of the board")
return Nonogram(b.build(), width, height, row, col)
def main() -> None:
puzzle = nonogram()
data = puzzle.spec.read("formats/nonogram/cases/five-by-five/input.txt")
print(f"{data[puzzle.width]}x{data[puzzle.height]} board")
print(f"rows: {data[puzzle.row]}")
print(f"columns: {data[puzzle.col]}")
if __name__ == "__main__":
main()
5x5 board
rows: [[1, 1], [1, 1], [1], [1, 1], [1, 1]]
columns: [[1, 1], [1, 1], [1], [1, 1], [1, 1]]

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.

The other formats

Section titled “The other formats”
build_dimacs_cnf.py
from __future__ import annotations
from typing import NamedTuple
import gecode_iris as iris
import gecode_iris.build as ib
class DimacsCnf(NamedTuple):
"""A DIMACS CNF reader: the number of variables and one row of literals per
clause."""
spec: iris.Spec
vars: ib.IntField[int]
clause: ib.IntField[list[list[int]]]
def dimacs_cnf() -> DimacsCnf:
b = ib.SpecBuilder("dimacs-cnf")
b.comment("c")
b.stop("%") # SATLIB files end with "%" and "0"
variables = b.capture("vars", ib.int_range(0, ib.gecode.INT_MAX))
clauses = b.capture("clauses", ib.at_least(0))
b.line(("p cnf ", variables, " ", clauses))
with b.repeat(clauses) as c:
# The terminating 0 is not stored.
clause = c.list("clause", ib.int_range(-variables, variables).until(0))
return DimacsCnf(b.build(), variables, clause)
def main() -> None:
cnf = dimacs_cnf()
data = cnf.spec.read("formats/dimacs-cnf/cases/three-variables/input.txt")
print(f"{data[cnf.vars]} variables")
for clause in data[cnf.clause]:
print(clause)
if __name__ == "__main__":
main()
3 variables
[1, -2]
[2, 3, -1]
[-3]
build_sudoku.py
from __future__ import annotations
from typing import NamedTuple
import gecode_iris as iris
import gecode_iris.build as ib
class Sudoku(NamedTuple):
"""A Sudoku reader: the grid size, the box size, and the cells, with 0 for a
blank."""
spec: iris.Spec
n: ib.IntField[int]
box_width: ib.IntField[int]
box_height: ib.IntField[int]
cells: ib.IntField[list[list[int]]]
SYMBOLS = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0"
"""Cell values from 1 to 36, so ``1`` reads as 1 and ``0`` as 36."""
def sudoku() -> Sudoku:
b = ib.SpecBuilder("sudoku")
with b.instances() as i:
n = i.capture("n", ib.at_least(1))
box_width = i.capture("box_width", ib.at_least(1))
box_height = i.capture("box_height", ib.at_least(1))
i.line(("# size=", n, " box=", box_width, "x", box_height))
cells = i.read("cells", ib.alphabet(SYMBOLS, blank=".").dims(n, n))
i.check(box_width * box_height == n, "boxes must tile the grid")
i.check(ib.max(cells) <= n, "a value is larger than the size of the grid")
return Sudoku(b.build(), n, box_width, box_height, cells)
def main() -> None:
puzzle = sudoku()
data = puzzle.spec.read("formats/sudoku/cases/four-by-four/input.txt")
n, width, height = data[puzzle.n], data[puzzle.box_width], data[puzzle.box_height]
print(f"{n}x{n} grid with {width}x{height} boxes")
for row in data[puzzle.cells]:
print(row)
if __name__ == "__main__":
main()
4x4 grid with 2x2 boxes
[1, 0, 0, 4]
[0, 4, 1, 0]
[0, 1, 4, 0]
[4, 0, 0, 1]

define_type gives a named type, used here for both ends of a time window:

build_tsptw.py
from __future__ import annotations
from typing import NamedTuple
import gecode_iris as iris
import gecode_iris.build as ib
class Tsptw(NamedTuple):
"""A reader for the travelling salesman problem with time windows: a
distance matrix and a time window for each node, with the depot as node 0."""
spec: iris.Spec
n: ib.IntField[int]
dist: ib.IntField[list[list[int]]]
ready: ib.IntField[list[int]]
due: ib.IntField[list[int]]
def tsptw() -> Tsptw:
b = ib.SpecBuilder("tsptw")
b.comment("#")
n = b.read("n", ib.int_range(2, 1000))
dist = b.read("dist", ib.int_range(0, ib.div(ib.div(ib.gecode.INT_MAX, 2), n)).dims(n, n))
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)
def main() -> 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)) in enumerate(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
class Tsplib(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]
class Coordinates(NamedTuple):
"""The entries of one section of numbered coordinates."""
nodes: ib.Record
x: ib.FloatField[list[float] | None]
y: ib.FloatField[list[float] | None]
def one_of(entry: ib.StrField[str | None], *values: str) -> ib.IntExpr:
"""Whether ``entry`` is one of ``values``: ``entry == "A" || entry == "B"``."""
first, *rest = values
test = entry == first
for value in rest:
test = test | (entry == value)
return test
Sections = ib.Arms[ib.Scope0[ib.Required], ib.Scope0[None]]
@ib.track_caller
def coordinate_section(arms: Sections, title: str, n: ib.IntField[int], prefix: str) -> Coordinates:
"""Adds the optional section ``title`` of ``n`` lines ``node x y``, with the
entries named ``{prefix}node``, ``{prefix}x`` and ``{prefix}y``."""
with arms.arm(title, optional=True) as a, a.repeat(n) as line:
line.read(f"{prefix}node", ib.int_)
x = line.read(f"{prefix}x", ib.float_)
y = line.read(f"{prefix}y", ib.float_)
return Coordinates(line.record, x, y)
def tsplib() -> Tsplib:
b = ib.SpecBuilder("tsplib")
b.comment("COMMENT")
name = b.optional_capture("name", ib.rest)
problem_type = b.optional_capture("type", ib.word)
n = b.capture("n", ib.int_range(2, 46340))
edge_weight_type = b.optional_capture("edge_weight_type", ib.word)
edge_weight_format = b.optional_capture("edge_weight_format", ib.word)
node_coord_type = b.optional_capture("node_coord_type", ib.word)
display_data_type = b.optional_capture("display_data_type", ib.word)
with b.sections(until="EOF") as s:
s.arm(("NAME : ", name), optional=True)
s.arm(("TYPE : ", problem_type), optional=True)
s.arm(("DIMENSION : ", n))
s.arm(("EDGE_WEIGHT_TYPE : ", edge_weight_type), optional=True)
s.arm(("EDGE_WEIGHT_FORMAT : ", edge_weight_format), optional=True)
s.arm(("NODE_COORD_TYPE : ", node_coord_type), optional=True)
s.arm(("DISPLAY_DATA_TYPE : ", display_data_type), optional=True)
coordinates = coordinate_section(s, "NODE_COORD_SECTION", n, "")
with s.arm("EDGE_WEIGHT_SECTION", optional=True) as a:
with a.if_(~ib.defined(edge_weight_format)) as t:
t.set(edge_weight_format, "FULL_MATRIX")
bound = ib.div(ib.gecode.INT_MAX, n)
weights = ib.int_range(0, bound).dims(n, n).layout(edge_weight_format)
weight = a.read("weight", weights)
coordinate_section(s, "DISPLAY_DATA_SECTION", n, "display_")
with b.if_(~ib.defined(edge_weight_type)) as t:
t.set(edge_weight_type, "EXPLICIT")
x, y = coordinates.x, coordinates.y
with b.if_(edge_weight_type == "EXPLICIT") as t:
t.check(ib.defined(weight), "no EDGE_WEIGHT_SECTION")
t.check(ib.size(weight) == n, "edge weights do not match DIMENSION")
with b.else_() as e:
e.check(
one_of(edge_weight_type, "EUC_2D", "CEIL_2D", "ATT", "GEO"),
"EDGE_WEIGHT_TYPE must be EXPLICIT, EUC_2D, CEIL_2D, ATT, or GEO",
)
e.check(ib.defined(x), "no NODE_COORD_SECTION")
e.check(ib.size(coordinates.nodes) == n, "coordinates do not match DIMENSION")
e.check(
(x >= -1e9) & (x <= 1e9) & (y >= -1e9) & (y <= 1e9),
"coordinates must be between -1e9 and 1e9",
)
return Tsplib(b.build(), name, n, edge_weight_type, weight, x, y)
def main() -> None:
problem = tsplib()
for case in ["explicit-five", "euc-2d-five"]:
data = problem.spec.read(f"formats/tsplib/cases/{case}/input.txt")
name = data[problem.name] or "unnamed"
weight_type = data[problem.edge_weight_type] or "?"
print(f"{name}: {data[problem.n]} cities, {weight_type}")
weight = data[problem.weight]
if weight is not None:
for row in weight:
print(f" {row}")
x, y = data[problem.x], data[problem.y]
if x is not None and y is not None:
print(f" x {x}\n y {y}")
if __name__ == "__main__":
main()
handmade-explicit-five: 5 cities, EXPLICIT
  [0, 2, 4, 6, 3]
  [2, 0, 5, 4, 7]
  [4, 5, 0, 3, 6]
  [6, 4, 3, 0, 2]
  [3, 7, 6, 2, 0]
handmade-euc-2d-five: 5 cities, EUC_2D
  x [0.0, 3.0, 3.0, 0.0, 1.5]
  y [0.0, 0.0, 4.0, 4.0, 2.0]

Typed handles for a text spec

Section titled “Typed handles for a text spec”

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
iris gen --lang python formats/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 iris command

Section titled “The iris command”

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:

Terminal window
iris read formats/dimacs-cnf/dimacs-cnf.iris formats/dimacs-cnf/cases/three-variables/input.txt
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.