Source code for formulation_bench.reformulation

import json
import subprocess
from dataclasses import dataclass
from pathlib import Path

from ._codegen import generate_map
from .formulation import Formulation
from .models import ParameterMap


[docs] @dataclass(frozen=True) class Reformulation: """A pair of MILP formulations with a reformulation label. Consists of two MILP formulations ``a`` and ``b`` and a boolean ``is_reformulation`` label indicating whether ``b`` is a reformulation of ``a``. The formal definition of *reformulation* is given in :ref:`reformulation-definition`. Positive entries (``is_reformulation=True``) are accompanied by a Lean 4 proof whose path is accessible via the ``lean_proof_path`` attribute; negative entries have no proof and ``lean_proof_path`` resolves to ``None``. Attributes ---------- a : Formulation The base formulation. b : Formulation The reformulation candidate. is_reformulation : bool ``True`` iff ``b`` is a *reformulation* of ``a``. path : pathlib.Path Resolved absolute path to this pair's directory. lean_proof_path : pathlib.Path or None For positive entries, the path to the accompanying Lean 4 proof file. For negative entries, ``None`` since no proof exists. parameter_map : ParameterMap The loaded ``map.json``, which states how each parameter of ``b`` is computed from the parameters of ``a``. Examples -------- Formulation ``b`` of :doc:`/problems/p12` is a reformulation of formulation ``a``:: >>> from formulation_bench import Dataset >>> ds = Dataset("dataset") >>> reform = ds.reformulations[73] # corresponds to p12.a -> p12.b >>> reform.a.problem.name 'Traveling Salesman Problem (TSP)' >>> reform.b.problem.name 'Traveling Salesman Problem (TSP)' >>> reform.b.constraints[-1].description # cutting plane added by p12.b 'Depot-Exit Position Bound (EC1)...' >>> reform.is_reformulation True """ a: Formulation b: Formulation is_reformulation: bool @property def path(self) -> Path: problem_dir = self.a.problem.path pair = f"{self.a.path.name}_{self.b.path.name}" return problem_dir.parent.parent / "reformulations" / problem_dir.name / pair @property def lean_proof_path(self) -> Path | None: if not self.is_reformulation: return None return self.path / "Reformulation.lean" @property def parameter_map(self) -> ParameterMap: return ParameterMap.from_dict(json.loads((self.path / "map.json").read_text()))
[docs] def gen_map_py(self) -> str: """Generate a Python script computing ``b``'s parameters from ``a``'s. The script is generated from the ``python`` code snippets of this pair's ``map.json``. The resulting script takes the path to ``a``'s ``parameters.json`` and the path to write ``b``'s as positional arguments. Examples -------- Generate the parameter-map script for the ``a`` to ``b`` pair of :doc:`/problems/p1`, whose parameters are a renaming:: >>> from formulation_bench import Dataset >>> ds = Dataset("dataset") >>> reform = ds.reformulations[0] # corresponds to p1.a -> p1.b >>> script = reform.gen_map_py() >>> print(script) import argparse import json <BLANKLINE> <BLANKLINE> def main(params_path: str, output_path: str) -> None: with open(params_path, "r") as f: data = json.load(f) <BLANKLINE> # Source Parameters CashMachineProcessingRate = data["CashMachineProcessingRate"] ... # Parameter Map A = CashMachineProcessingRate ... """ return generate_map(self)
[docs] def run_map( self, input_path: str | Path | None = None, output_path: str | Path | None = None, ) -> None: """Write this pair's ``map.py`` and run it. The script generated by :meth:`gen_map_py` is written to ``map.py`` in the pair's directory, then applied to ``a``'s parameters to produce ``b``'s. Parameters ---------- input_path : str or pathlib.Path, optional Path to a ``parameters.json`` holding formulation ``a``'s parameters. Defaults to ``parameters.json`` in ``a``'s formulation directory. output_path : str or pathlib.Path, optional Path to write the mapped parameters. Defaults to ``parameters.json`` in this pair's directory. Examples -------- Map formulation ``a``'s parameters to formulation ``b``'s for :doc:`/problems/p1`:: >>> import json >>> from formulation_bench import Dataset >>> ds = Dataset("dataset") >>> reform = ds.reformulations[0] # corresponds to p1.a -> p1.b >>> reform.a.run_gen_params() >>> params = json.load(open(reform.a.path / "parameters.json")) >>> params["CashMachineProcessingRate"] # a's parameter 20 >>> reform.run_map() >>> params = json.load(open(reform.path / "parameters.json")) >>> params["A"] # b's name for "CashMachineProcessingRate" 20 """ script = self.path / "map.py" script.write_text(self.gen_map_py()) if input_path is None: input_path = self.a.path / "parameters.json" if output_path is None: output_path = self.path / "parameters.json" subprocess.run( ["python", str(script), str(input_path), str(output_path)], check=True )