Metadata-Version: 2.4
Name: cgshop2027-pyutils
Version: 0.2.0
Summary: Utilities for verifying solutions of the CG:SHOP 2027 Competition.
Author: Phillip Keldenich
License-Expression: MIT
Project-URL: Homepage, https://github.com/CG-SHOP/pyutils27
Project-URL: Issues, https://github.com/CG-SHOP/pyutils27/issues
Keywords: cgshop,computational geometry,optimization challenge
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: matplotlib>=3.6
Requires-Dist: numpy>=1.24
Requires-Dist: pydantic>=2.0.0
Requires-Dist: shapely>=2.1.2
Requires-Dist: typing_extensions>=4.0.0; python_version < "3.12"
Provides-Extra: dev
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# pyutils27 (cgshop2027-pyutils)

Utilities for working with CG:SHOP 2027 instances & solutions. This is a pure
Python package containing a parser/verifier for our instance format, solution
format and a verifier for solutions.

---

## Installation

```bash
pip install cgshop2027-pyutils
```

The current state of `main`, ahead of the latest release:

```bash
pip install git+https://github.com/CG-SHOP/pyutils27
```

From a local clone (editable, for development):

```bash
git clone https://github.com/CG-SHOP/pyutils27
cd pyutils27
pip install -e ".[dev]"
```

---

## Layout

```
src/cgshop2027_pyutils/
├── schemas/            # Pydantic models for instances & solutions
├── io/                 # reading instances & solutions from paths or file objects
├── zip/                # safe reading/writing of instance & solution archives
├── instance_database/  # query instances from a folder or a zip
├── grid.py             # cell-set algebra on the integer grid
├── verify.py           # checking a solution against its instance
└── visualize.py        # matplotlib plots and animations
tests/                  # pytest test suite
```

---

## Usage

Verifying a solution against its instance:

```python
from cgshop2027_pyutils.io import read_instance, read_solution
from cgshop2027_pyutils.verify import check_for_errors

errors = check_for_errors(
    read_instance("demo.instance.json"),
    read_solution("demo.solution.json"),
)
```

`check_for_errors` returns a list of human-readable messages; an empty list
means the solution is feasible. When checking many solutions for the same
instance, use `SolutionValidator(instance)` directly so that the
instance-derived precomputation is only paid once.

A solution consists of exactly `number_of_cutters` tours, one per cutter:

```python
solution = CGSHOP2027Solution(
    instance_uid="demo",
    tours=[{"x": [0, 3, 3, 0], "y": [0, 0, 1, 1]}, {"x": [7], "y": [7]}],
)
```

Each tour lists the points the cutter's _center_ passes through and is closed
implicitly, so the cutter travels from the last point back to the first one.
Consecutive points must be axis-parallel; a point may be visited again later,
but not immediately again. A tour of a single point is a cutter that stays where
it is. Cutters do not interfere with each other and may leave the region; all
that is required is that together they sweep the whole region.

The objective is `solution.max_tour_length`, the length of the longest tour, and
it is to be minimized.

Bundling instances or solutions into an archive:

```python
from cgshop2027_pyutils.zip import ZipWriter

with ZipWriter("solutions.zip") as writer:
    for solution in solutions:
        writer.add_solution(solution)
```

Reading a submitted archive, with size, CRC and path-traversal checks:

```python
from cgshop2027_pyutils.zip import BadSolutionFile, ZipSolutionIterator, ZipReaderError

try:
    for solution in ZipSolutionIterator("solutions.zip"):
        print(solution.instance_uid)
except (ZipReaderError, BadSolutionFile) as e:
    print(f"Rejected: {e}")
```

`example_zip.ipynb` walks the whole submission path end to end: reading the
instances, checking solutions against them, packing an archive, and what a
rejection looks like. It runs on the two archives next to it:
`example_instances.zip`, a copy of the published example instances, and
`example_solutions.zip`, one feasible solution for each of them to check against
and to beat.

Looking instances up by name, from either a folder or a zip:

```python
from cgshop2027_pyutils.instance_database import InstanceDatabase

db = InstanceDatabase("instances.zip")
instance = db["demo"]
for instance in db:
    ...
```

Plotting an instance, a solution, or an animation of the cutters at work:

```python
from cgshop2027_pyutils.visualize import (
    create_instance_plot,
    create_solution_animation,
    create_solution_plot,
)

create_instance_plot(instance).savefig("instance.png")
create_solution_plot(instance, solution).savefig("solution.png")

animation = create_solution_animation(instance, solution)
animation.save("solution.gif", writer="pillow")
```

The solution plot draws one tour per cutter, shades the swept area and marks any
cells left uncovered in red. The animation strides through the tours to fit
`max_frames` (300 by default), filling in coverage as the cutters move; pass
`step` to control the stride yourself.

Both draw the swept area in one flat colour by default. Pass `coverage="shaded"`
to have each cutter shade the area it sweeps in its own colour instead, graded
by how often it passed over each cell: washed out towards white where it went by
once, deepened towards black over the cell it worked hardest. All cutters share
one scale, so the same shade means the same number of passes whichever cutter
drew it, and where two of them sweep the same cell their fills blend. This needs
a count grid per cutter, so on large instances it is noticeably slower and uses
more memory, increasingly so with more cutters.

```python
create_solution_plot(instance, solution, coverage="shaded").savefig("shaded.png")
```

All three take `bare=True`, which drops the axes, the grid and every title and
trims the figure to the drawing, for a picture meant for a paper or a slide
rather than for debugging:

```python
create_solution_plot(instance, solution, bare=True).savefig("solution.pdf")
```

Instance files must be named `NAME.instance.json`. A lookup key may be the bare
name, the file name, or a path to it: `db["demo"]`, `db["demo.instance.json"]`
and `db["batch1/demo.instance.json"]` all resolve to the same instance. Note
that every `.json` file in the folder or archive is treated as an instance, so
keep solutions in a separate location.

---

## Development

Set up the tooling (linting & formatting run through pre-commit):

```bash
pip install -e ".[dev]"
pre-commit install
```

Run the checks manually:

```bash
ruff check .
ruff format .
pytest
```

Linting, formatting and import sorting are all handled by
[ruff](https://docs.astral.sh/ruff/); its configuration lives in
`pyproject.toml`.

---

## Contributing

Issues & PRs welcome. Please:

1. Add/adjust unit tests for new behavior
2. Keep the public API documented here
3. Run the test suite before submitting

---

## Changelog

- **0.2.0** (2026-10-16): Incorporated a faster and more memory-efficient
  verification routine, with a fallback to the old routine in case of unusually
  large coordinates, in addition to stricter enforced limits on the coordinate
  sizes in tours. The solution plot and the animation now take
  `coverage="shaded"` as optional argument, which draws coverage per cutter and
  shaded by how often each cell was passed over; the default, `"flat"`, keeps
  the single flat area (it's a bit faster and uses less memory, looking less
  nice as a trade-off). Adds `CellCounts` and `dilate_counts` to `grid.py` and
  makes `SolutionValidator.reachable_anchors` public.
- **0.1.0** (2026-08-25): First release. Instance and solution schemas, the
  solution verifier, archive reading and writing, the instance database, and
  plotting and animation.

---

## License

MIT, see `LICENSE`.
