Metadata-Version: 2.4
Name: ofplang-schedule
Version: 0.8.0
Summary: Scheduler for Object-flow Programming Language workflows.
Author-email: Kazunari Kaizu <kwaizu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/ofplang/schedule
Project-URL: Repository, https://github.com/ofplang/schedule
Keywords: ofplang,dataflow,workflow,scheduler,cp-sat,ortools
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Requires-Dist: ortools>=9.15
Requires-Dist: ofplang-validate<0.3,>=0.2
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: ruff>=0.16; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Dynamic: license-file

# ofplang schedule

[![CI](https://github.com/ofplang/schedule/actions/workflows/ci.yml/badge.svg)](https://github.com/ofplang/schedule/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/ofplang-schedule.svg)](https://pypi.org/project/ofplang-schedule/)

A scheduler for **Object-flow Programming Language v0** — a YAML-based dataflow
workflow IR with linear Object tracking. The language is defined in the
[ofplang/spec](https://github.com/ofplang/spec) repository.

The scheduler takes one or more portable v0 workflows plus an execution environment
definition and plans when their work runs; it also replans from an execution
status. The design is documented in [docs/SPECIFICATIONS.md](docs/SPECIFICATIONS.md).

> **Status:** the **schema validators** (environment definition and execution
> document, spec §9) and the **scheduler** are implemented: it produces an optimal
> plan with mode selection, spot/device occupancy, and
> transport, lets a mode **hold a spot without holding its device** for storage and
> incubation (`device_access: false`, spec §4.4.2), pins a workflow's boundary
> material to spots via an `interface`
> (spec §6.8), respects **device-local consumable resources** — what a mode draws
> and what a **replenishment** puts back (spec §4.7) — and **replans** from an
> execution document (`--document`) by fixing completed/running activities and
> re-optimising the rest at or after `now`. Several workflows can be planned
> **together** against one environment as separate **jobs** (spec §6.11), so they
> compete for the same machines and share the same stocks — a refill neither needs
> alone is then planned once for both. A `visualize` command renders a plan as
> a self-contained SVG/HTML Gantt chart. The model is documented in
> [docs/FORMULATION.md](docs/FORMULATION.md).

This is a fresh implementation that targets the spec directly. The prototype
[`ofp-scheduler`](https://github.com/ofplang) (OR-Tools CP-SAT) is a reference
for ideas but not a dependency.

## Install

```sh
pip install ofplang-schedule
```

Requires Python 3.10+. Runtime dependencies are PyYAML, OR-Tools (the CP-SAT
solver used by the scheduler), and the sibling
[`ofplang-validate`](https://pypi.org/project/ofplang-validate/) (pulled in
automatically), which the CLI's front-door check uses. The scheduler *library*
never imports validate, so embedders that only call `ofplang.schedule` take no
validation overhead.

For development, install editable with the test extra from a clone:

```sh
pip install -e ".[test]"
```

## Command line

```sh
ofp-schedule validate <file>...                 # validate an environment or a plan/status
ofp-schedule schedule <workflow>... --env <env> [--document doc.yaml] [--withdraw ID] [--running-margin N] [--max-time SECONDS] [--seed N] [--max-transport-legs N] [--no-validate] [-o plan.yaml] [--format yaml|json]
ofp-schedule visualize <plan|status> [--view device|workflow|lane] [--theme light|dark|auto] [--format svg|html] [-o FILE]
```

`validate` auto-detects whether the file is an environment definition or an
execution document (pass `--kind` to force it); diagnostics are reported as
`file:line:col: <severity> <code>`. `schedule` produces an execution plan (§6),
minimising the objective the document declares (§4.8; makespan, then the number of
refills). **Give several workflows to plan them together** as separate **jobs** (§6.11):
they compete for the same machines and draw on the same stocks, so a refill neither
needs alone is planned once for both. They are numbered `job1`, `job2`, ... in the
order written, or write `ID=FILE` to name one yourself; every activity in the plan
then carries the job it belongs to. A job may be given its own `interface` and a
`release` time in the document's `jobs` roster, and each is promised the completion
its first plan achieves (`bound`) — which later plans keep, so a job already being
planned is not disturbed by one that arrives later.

A `--document` (execution document, §6) supplies the `interface` boundary
constraint (§6.8, where a workflow's entry inputs / final outputs sit — an entry
input has to be bound, while a final output left unbound comes to rest wherever the
schedule finds room, so bind the ones whose destination matters), the
`inventories` levels as of a moment it names (§6.10) where devices hold
consumables, the
`objective` (§6.1, now its only declaration site), the `jobs` roster (§6.11) and the
`occupied` spots something is physically holding (§6.12), and, when it sets `now`,
the prior status to replan from (§7) — emitting the full timeline (fixed history +
re-optimised future) that round-trips as the next status input. By default the solve is non-deterministic
(a multi-worker search that may return a different equally-optimal schedule each
run); `--seed N` makes it reproducible by fixing the CP-SAT seed and using a
single worker. `--max-time SECONDS` caps the search: the best schedule found so
far is returned instead of the proven optimum, which the plan says by reporting
`outcome: feasible` rather than `optimal` — and a search that found nothing in
the budget reports no schedule at all (exit `1`), since an instance is not
unschedulable merely because time ran out.
`--max-transport-legs N` is how many transport activities one Object-bearing arc may
be moved in (§6.4.1). It is **1 by default** — the single hop per arc this has
always planned. Raise it to describe a device the transporter reaches at one position
only, or a plate that has to cross a hand-off station: the arc is then carried in as
many legs as the shortest chain of moves between its endpoint spots takes, joined by
**relay** activities. Only the fewest possible moves are offered, so an arc one move
apart is never sent round by way of somewhere else.
`--withdraw ID` (repeatable) takes a job **out** of a joint plan (§6.11). The roster
is the set of jobs something of which is still in the laboratory, so an entry is
removed when nothing is — which the scheduler cannot see, hence an instruction rather
than an inference, and one refused while the document says the job still has work to
do or running. What makes it more than deleting an entry is the arithmetic: a job's
history is part of what the current stock levels are made of, so the plan restates
`inventories` as of `now` (§6.10) instead of giving the stock back everything that
job drew. Pass no workflow for a job being withdrawn. A final output you bound to a
spot is taken as collected; one you left unbound is not — the schedule chose where it
came to rest, so that spot is kept as an `occupied` entry (§6.12) and named in the
report.
`--ignore-resources` switches consumables off (§4.7.3): the
declarations are still checked for shape but nothing is applied, and the plan is
shaped as it would be from an environment that never declared one — a relaxation,
so it never turns a solvable instance unsolvable. `--no-validate` skips the one-shot `ofplang-validate` front-door
check of the workflow — use it when the workflow was already validated upstream
(e.g. by the `ofp` umbrella CLI); `$import` is still resolved, since that is
structural rather than a validation check. `visualize` renders any §6 execution
document — a plan, or the status a finished run produced — as a self-contained
Gantt chart, either SVG (fixed colours, transparent background, PowerPoint-safe)
or HTML. `--format` chooses; without it the output is SVG, except that an `-o`
path ending in `.html` or `.htm` is taken as asking for HTML, and an explicit
`--format` always wins — `--format svg -o chart.html` writes SVG. Exit codes:
`0` success, `1` validation errors or no feasible schedule, `2` usage/input
error.

This tool is also the `schedule` subcommand of the umbrella `ofp` CLI
([`ofplang`](https://pypi.org/project/ofplang/)), which forwards to it in-process
with this CLI's own subcommands intact: `ofp schedule schedule …`,
`ofp schedule visualize …`, each with the same options and exit codes as above.

## Feature support

v0 defines seven optional features (spec §4.2), and a document requiring one an
implementation does not have "is valid v0 but unsupported by that implementation"
(§4.1). So `ofp-validate` accepting a workflow does not mean this scheduler can
plan it:

| v0 feature | `ofplang-schedule` |
|---|---|
| `python_script_processes` | Supported. A script process is scheduled like any atomic one; its mode `duration` is the estimate of the compute cost. Running the script is the runner's job. |
| `scheduling_policies` | Accepted, then **ignored**: §23 makes these best-effort preferences, and a composite's `scheduling` section is dropped when the composite is flattened. The report's diagnostics carry a `scheduling_policies_ignored` warning. |
| `node_map`, `node_fold`, `node_do_while`, `node_branch` | **Not supported.** A structured node reshapes dataflow in ways the flat scheduler graph cannot represent, so it is refused with `unsupported_feature`. |
| `generic_processes` | **Not supported.** Refused with `unsupported_feature`. |

## Library

```python
from ofplang.schedule import schedule

report = schedule(workflow, environment, document_path=status)  # -> ScheduleReport
```

Alongside the plan, the report carries `stats`: what the *solve* cost, as opposed
to what it decided — timings (including CP-SAT's machine-independent
`deterministic_time`), the bound the answer was measured against, and the size of
the model. It is there on every path that reached the solver, an infeasible
instance included, and `None` where the inputs were refused before solving.
Passing `collect_solutions=True` additionally records each improving solution as
the search finds it (`stats.phases[-1].history`), which is what an anytime
measurement — how good was the schedule at time *t*? — reads; it is off by default
because a solution callback runs inside the search. None of this enters the plan:
a plan is a portable v0 document and says nothing about how it was found.

Each input is either a path or an already-loaded document (a mapping), so an
embedder that holds them in memory — a rolling-horizon runner rendering a fresh
status every replan — passes them straight in, with no temporary files and nothing
re-parsed. An in-memory document is read, never written to, and the plan it
produces shares no structure with it. Because such a document has no file to point
at, its diagnostics carry no `file:line:col` and locate by their `path` instead,
and the plan's `meta` provenance reads `<in-memory>` unless the caller names the
original file (`workflow_source` / `environment_source` / `document_source`).

The package lives under the `ofplang` PEP 420 namespace (`ofplang.schedule`),
shared across the organization's tools.

## Examples

[`examples/`](examples/README.md) holds complete workflow + environment pairs used
to drive and eyeball the scheduler: a minimal source → target, a workflow with
boundary material pinned by an `interface`, two jobs on a two-transporter fleet, a
plate-reformatting DAG, and a parametric generator that scales the instance up.
Each comes with its solved plan and a rendered chart under `examples/outputs/`.

## Tests

```sh
pytest
```
