Metadata-Version: 2.5
Name: scip_toolbox
Version: 0.1.2
Summary: Pure-Python notebook API for SCIP run analytics (.statistics + .vbc)
Project-URL: Homepage, https://gitlab.uni-hannover.de/lars.jaeger/scip_toolbox
Project-URL: Repository, https://gitlab.uni-hannover.de/lars.jaeger/scip_toolbox
Project-URL: Issues, https://gitlab.uni-hannover.de/lars.jaeger/scip_toolbox/-/issues
Author: Lars Jaeger
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.11
Requires-Dist: matplotlib>=3.8
Requires-Dist: nbformat>=4.2.0
Requires-Dist: networkx>=3.2
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.2
Requires-Dist: plotly>=5.22
Provides-Extra: dev
Requires-Dist: ipykernel>=6.29; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine>=7.0.0; extra == 'dev'
Provides-Extra: gurobi
Requires-Dist: grblogtools>=2.2.0; extra == 'gurobi'
Description-Content-Type: text/markdown

# SCIP Toolbox

A clean, **notebook-first Python API** for analysing SCIP solver runs. Two analysis tracks are
exposed as one importable package:

| Module                      | What it does                                         |
| ---------------------------- | ---------------------------------------------------- |
| `scip_toolbox.statistics`   | Parse `.statistics` files into pandas DataFrames     |
| `scip_toolbox.vbc`          | Parse `.vbc` files & build Plotly B&B-tree figures   |

There is **no CLI and no web UI** — everything is plain Python you call
from a Jupyter notebook (or any script). VBC visualisations return Plotly
`Figure` objects; statistics plots return Matplotlib `Figure`/`Axes` pairs.
Both render inline in notebooks and can be saved with their respective
library's export methods.

- **Source (GitLab):** <https://gitlab.uni-hannover.de/lars.jaeger/scip_toolbox>
- **Package (PyPI):** <https://pypi.org/project/scip-toolbox/>

## Install

Install the published package with `uv` or `pip`:

```powershell
uv add scip_toolbox
# or
pip install scip_toolbox
```

To work on the repo itself:

```powershell
# inside the repo root
uv sync
# or, with editable install + dev tools:
uv sync --extra dev
```

## Notebook quick start

A single import gets you the full API:

```python
from scip_toolbox import (
    # statistics
    load_directory, aggregate, extract_columns,
    TemplateIdParser,
    # vbc
    VBCParser, build_graph,
    plot_tree_plotly, plot_tree_at_step,
    BoundsPlot, GapPlot,
)
```

### 1. Aggregate `.statistics` files

```python
runs = load_directory("path/to/runs/", pattern="*.statistics")

df = aggregate(runs, simple={
    "status":     ("SCIP Status", "Status"),
    "total_time": ("Total Time",  "Total"),
    "primal":     ("Solution",    "Primal Bound"),
    "dual":       ("Solution",    "Dual Bound"),
    "gap":        ("Solution",    "Gap"),
    "nodes":      ("B&B Tree",    "nodes"),
})
df.head()
```

`load_directory` caches the parsed bundle next to the folder as a pickle.
Pass `cache=False` to disable, or `reload=True` to force a re-parse.

#### Choose the right aggregation rule

`aggregate()` produces one row per run. Each mapping key is the output
column name; its tuple identifies data in the parsed SCIP section. First,
inspect a run when adapting these examples to your SCIP version:

```python
run = next(iter(runs.values()))
print(run["Pricers"].head())
```

The bundled example file contains sections with these shapes:

```text
SCIP Status:  one row indexed by the run ID, column "SCIP Status"
B&B Tree:     one row indexed by the run ID, column "nodes"
Pricers:      named rows "problem variables", "VRP_pricer", ...;
              columns "ExecTime", "SetupTime", "Calls", "Vars"
LP:           named rows "primal LP", "dual LP", ...;
              columns "Time", "Calls", "Iterations", ...
```

```python
df = aggregate(
    runs,
    # simple: take a value from a one-row, run-level section.
    simple={
        "status": ("SCIP Status", "SCIP Status"),
        "nodes": ("B&B Tree", "nodes"),
    },
    # specific: take one column from one known named row in a table.
    specific={
        "vrp_pricer_calls": ("Pricers", "VRP_pricer", "Calls"),
    },
    # sums: add a numeric column over every row in a table.
    sums={
        "total_lp_calls": ("LP", "Calls"),
    },
    # multi: retain every named row as a separate output column.
    multi={
        "pricer_metrics": ("Pricers", ["ExecTime", "Calls"]),
    },
)
```

Use `simple` for a value that already describes the whole run. Use
`specific` when one row has a stable, meaningful name. Use `sums` when the
table rows are contributions to a total; non-numeric cells are ignored. Use
`multi` when row names vary or each row is independently useful: the example
creates columns such as `VRP_pricer (pricer_metrics) (ExecTime)`. Missing
`simple`, `specific`, and `sums` sources yield `0`; missing `multi` sources
do not create a column.

### 2. Custom instance-name parsers

Instance IDs often encode parameters
(`Instance_15_1_DEU_NLD_3_wj_zk_Config_1_1_1_0_1_0_0`).
`TemplateIdParser` turns the documented structure into named columns. Leave
`id_parser` unset when original IDs are sufficient. Custom callables that
conform to `IdParser` remain supported for programmatic parsing needs.

Template parser, in code:

```python
parser = TemplateIdParser(
    "Instance_{n_tasks}_{version}_{country:[A-Z]+_[A-Z]+}"
    "_{n_instance}_{weather}_{teams}"
    "_Config_{c1}_{c2}_{c3}_{c4}_{c5}_{c6}_{c7}",
    numeric=("n_tasks", "version", "n_instance"),
)
runs = load_directory("path/to/runs/", id_parser=parser)
```

Or, externalise it to a small text file
([examples/instance_id_template.txt](examples/instance_id_template.txt))
and load it without writing code:

```python
parser = TemplateIdParser.from_file("examples/instance_id_template.txt")
runs = load_directory("path/to/runs/", id_parser=parser)
```

The template file names fields in an instance ID and can also carry the
human-readable documentation shown with each parsed row in VS Code's
DataFrame viewer:

```text
template: i{n_customers}_m{n_vehicles}_{instance_no}_{algorithm}_{relaxed}_{time_limit}
numeric: n_customers, n_vehicles, instance_no, relaxed, time_limit
default_field_rx: [^_]+
drop_algorithm: false

<explanation>
This template parses CVRP benchmark runs.

Fields:
- n_customers: number of customers
- n_vehicles: number of available vehicles
- instance_no: benchmark instance number
- algorithm: solver configuration, for example BP or CM
- relaxed: whether the model is relaxed (0 or 1)
- time_limit: solver time limit in seconds
</explanation>
```

`{name}` captures one underscore-separated token into a column named
`name`; `{name:REGEX}` uses a custom regex when one logical field contains
underscores. `numeric` converts the named columns with `pd.to_numeric`.
The text between `<explanation>` and `</explanation>` is stored as the
string column `explanation` on every parsed instance.

### 3. Visualise a single `.vbc` run

```python
parser = VBCParser(filepath="run.vbc")
parser.parse()

g = build_graph(parser)

# Full B&B tree with realistic-depth (dual-bound) Y axis:
plot_tree_plotly(g, realistic_depth=True).show()

# Primal vs reconstructed global dual bound + relative gap:
BoundsPlot(parser, show_gap=True).build().show()

# Optimality gap over time:
GapPlot(parser).build().show()
```

Step-by-step replay:

```python
snapshots = parser.build_snapshots()
plot_tree_at_step(g, snapshots[42]).show()
```

Export any Plotly figure with `fig.write_html("tree.html")` /
`fig.write_image("tree.png")`.

## Public API

The notebook quick start deliberately imports only the most-used names. The
remaining public helpers are available from `scip_toolbox`:

| API | Purpose |
| --- | --- |
| `read_statistics_file(path)` | Parse one `.statistics`/`.stats` file into `InstanceStatistics`. |
| `extract_columns(runs, section)` | Stack the run-level values for one section into a DataFrame. |
| `summarize_by_group(df, ...)` | Build a grouped aggregate table from named metrics. |
| `create_table_of_df(df, ...)` | Build a multi-level comparison table by method. |
| `export_table_to_latex(table, ...)` | Write a comparison table to a LaTeX file. |
| `plot_boxplot_by_group(df, ...)` | Return a Matplotlib boxplot grouped by a DataFrame column. |
| `save_figure(fig, path)` | Save a Matplotlib figure, creating parent directories. |
| `build_graph_from_snapshot(snapshot, parser)` | Build the B&B graph at one replay step. |
| `collect_bound_event_summary_dataframe(folder)` | Collect first/last primal and dual-bound events across `.vbc` files. |
| `plot_last_bound_time_scatter(df, ...)` | Return a Matplotlib scatterplot comparing final primal and dual event times. |

`VBCParser` accepts either `filepath="run.vbc"` or `text="..."`. Call
`parse()` before accessing nodes, bound events, or graphs; use
`build_snapshots()` for the sequence consumed by `plot_tree_at_step()`.

## Testing

```powershell
uv run pytest -q
```

## Layout

```
src/scip_toolbox/
├── __init__.py            # flat re-exports for one-line notebook imports
├── statistics/            # .statistics file parsing & aggregation
│   ├── id_parser.py       # TemplateIdParser and IdParser protocol
│   ├── loader.py          # read_statistics_file, load_directory
│   └── summary.py         # aggregate, extract_columns
└── vbc/                   # .vbc file parsing & visualisation
    ├── models/            # NodeData, BoundEvent, layout helpers
    ├── parser/            # VBCParser, classifier, snapshot builder, graph builder
    └── viz/               # tree.py, bounds_plot.py, gap_plot.py, last_bound_scatter.py (all return Plotly/matplotlib figures)
```

See [examples/example.ipynb](examples/example.ipynb) for an end-to-end,
heavily-commented walkthrough that starts from raw SCIP output files and
ends with publication-ready tables and figures.

The `.statistics`/`.stats` and `.vbc` files used by that example (a vehicle routing problem solved with
branch-and-price, column generation, and a compact MIP model) come from
[vrp_example_scip_cpp](https://gitlab.uni-hannover.de/lars.jaeger/vrp_example_scip_cpp), which also serves
as a standalone teaching example of how to implement a branch-and-price algorithm with SCIP/SCIP-SoPlex in
C++. Check it out if you want to see how the analysed runs were produced, or are looking to implement your
own branch-and-price solver.

## License

Licensed under the [Apache License, Version 2.0](LICENSE).

## Citing

If you use `scip_toolbox` in your research, please cite it - see [CITATION.cff](CITATION.cff).

This toolbox only *analyses* output produced by the SCIP Optimization Suite. If you publish results
obtained by running SCIP (with or without this toolbox), please also cite SCIP itself, e.g. the original
SCIP paper:

```bibtex
@article{Achterberg2009,
  author  = {Tobias Achterberg},
  title   = {{SCIP}: solving constraint integer programs},
  journal = {Mathematical Programming Computation},
  year    = {2009},
  volume  = {1},
  number  = {1},
  pages   = {1--41},
  doi     = {10.1007/s12532-008-0001-1}
}
```

See [scipopt.org](https://scipopt.org) for the up-to-date recommended citation for the specific SCIP
Optimization Suite version you used.
