Source code for moment_sos.serialize

"""Language-independent problem and result format (JSON).

A problem is a dict (or a JSON file)::

    {"n": 2, "sense": "max", "name": "demo",
     "box": {"lo": -1, "hi": 1},                      # scalars or length-n lists; omit for Boolean problems
     "boolean": false,                                # true, false, or a length-n list of booleans
     "objective": [{"c": 1.0, "m": [[0, 2], [1, 1]]}, {"c": -1.0, "m": [[0, 1]]}],
     "constraints": [{"kind": "ineq", "terms": [{"c": 1.0, "m": []}, {"c": -1.0, "m": [[0, 2]]}]}]}

A term ``{"c": coefficient, "m": [[variable index, power], ...]}`` is ``c * prod x_i^p`` (0-based indices; the empty
list is the constant monomial). ``kind`` is ``"ineq"`` (poly >= 0) or ``"eq"`` (poly == 0).
This is the interface used by the command line tool and by the Julia package.
"""
from __future__ import annotations
import json
import numpy as np
from .poly import Poly, Constraint
from .problem import Problem

FORMAT_VERSION = 1


def poly_to_terms(p):
    return [{"c": float(c), "m": [[i, int(e)] for i, e in enumerate(exps) if e]} for exps, c in sorted(p.terms.items())]


def terms_to_poly(n, terms):
    out = {}
    for t in terms:
        exps = [0] * n
        for i, pw in t["m"]:
            if not 0 <= i < n:
                raise ValueError(f"variable index {i} out of range for n={n}")
            exps[i] += int(pw)
        key = tuple(exps)
        out[key] = out.get(key, 0.0) + float(t["c"])
    return Poly(n, out)


[docs] def problem_to_dict(problem): """Serialise a :class:`Problem` (the user's original, un-normalised data).""" d = {"format": FORMAT_VERSION, "n": problem.n, "sense": problem.sense, "name": problem.name, "boolean": [bool(b) for b in problem.bool_mask] if not problem.bool_mask.all() and problem.bool_mask.any() else bool(problem.bool_mask.all()), "objective": poly_to_terms(problem.objective), "constraints": [{"kind": c.kind, "terms": poly_to_terms(c.poly)} for c in problem.constraints]} if not problem.bool_mask.all(): d["box"] = {"lo": problem.lo.tolist(), "hi": problem.hi.tolist()} return d
[docs] def problem_from_dict(d): """Build a :class:`Problem` from the dict format described in the module docstring.""" if "n" not in d or "objective" not in d: raise ValueError("problem dict needs the keys 'n' and 'objective'") n = int(d["n"]) obj = terms_to_poly(n, d["objective"]) cons = [] for c in d.get("constraints", []): if c["kind"] not in ("ineq", "eq"): raise ValueError("constraint kind must be 'ineq' or 'eq'") cons.append(Constraint(terms_to_poly(n, c["terms"]), c["kind"])) box = d.get("box") box = None if box is None else (box["lo"], box["hi"]) return Problem(obj, cons, sense=d.get("sense", "max"), box=box, boolean=d.get("boolean", False), name=d.get("name", ""))
[docs] def load_problem(path): with open(path) as f: return problem_from_dict(json.load(f))
[docs] def save_problem(problem, path): with open(path, "w") as f: json.dump(problem_to_dict(problem), f)
[docs] def result_to_dict(res, rigorous=False, round_point=True): """Machine-readable summary of a :class:`Result` (bound, kind, point, history).""" P, R = res.problem, res.relaxation out = {"format": FORMAT_VERSION, "sense": P.sense, "bound": float(res.bound), "bound_kind": res.bound_kind, "order": int(R.r), "blocks": len(R.blocks), "largest_block": int(max(R.sizes())), "time": float(res.time), "evaluations": int(res.n_eval), "history": [[float(t), int(n), float(P.f_off + P.f_scale * u) * (1 if P.sense == "max" else -1)] for t, n, u in res.history]} if rigorous: out["rigorous"] = {k: (float(v) if isinstance(v, (int, float, np.floating)) else v) for k, v in res.rigorous().items() if isinstance(v, (int, float, np.floating, bool, str))} if round_point: x, val = res.round() out["x"] = None if x is None else [float(v) for v in x] out["value"] = None if val is None else float(val) return out