lp2graph 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lp2graph/__init__.py +54 -0
- lp2graph/cli.py +238 -0
- lp2graph/codec/__init__.py +41 -0
- lp2graph/codec/latex.py +884 -0
- lp2graph/codec/normalize.py +82 -0
- lp2graph/core/__init__.py +35 -0
- lp2graph/core/graph.py +183 -0
- lp2graph/core/loader.py +63 -0
- lp2graph/core/model.py +437 -0
- lp2graph/core/validate.py +237 -0
- lp2graph/export/__init__.py +13 -0
- lp2graph/export/dgl.py +51 -0
- lp2graph/export/latex.py +126 -0
- lp2graph/export/networkx_adapter.py +50 -0
- lp2graph/export/pyg.py +79 -0
- lp2graph/export/pyomo_stub.py +81 -0
- lp2graph/metrics/__init__.py +58 -0
- lp2graph/metrics/classification.py +113 -0
- lp2graph/metrics/flags.py +122 -0
- lp2graph/metrics/result.py +26 -0
- lp2graph/metrics/structural.py +236 -0
- lp2graph/mining/__init__.py +47 -0
- lp2graph/mining/cluster/__init__.py +65 -0
- lp2graph/mining/cluster/agglomerative.py +82 -0
- lp2graph/mining/cluster/distance.py +65 -0
- lp2graph/mining/cluster/operator.py +218 -0
- lp2graph/mining/cluster/silhouette.py +88 -0
- lp2graph/mining/cluster/stability.py +178 -0
- lp2graph/mining/cluster/taxonomy.py +268 -0
- lp2graph/mining/corpusmgr/__init__.py +70 -0
- lp2graph/mining/corpusmgr/dedup.py +183 -0
- lp2graph/mining/corpusmgr/manager.py +79 -0
- lp2graph/mining/corpusmgr/manifest.py +82 -0
- lp2graph/mining/corpusmgr/record.py +101 -0
- lp2graph/mining/corpusmgr/select.py +128 -0
- lp2graph/mining/homologize/__init__.py +82 -0
- lp2graph/mining/homologize/concept.py +134 -0
- lp2graph/mining/homologize/entity.py +217 -0
- lp2graph/mining/homologize/lemmatize.py +80 -0
- lp2graph/mining/homologize/signature.py +166 -0
- lp2graph/mining/homologize/thesaurus.py +70 -0
- lp2graph/mining/homologize/tokenize.py +255 -0
- lp2graph/mining/homologize/vectorize.py +141 -0
- lp2graph/mining/ingest/__init__.py +59 -0
- lp2graph/mining/ingest/code_importers.py +104 -0
- lp2graph/mining/ingest/dispatch.py +148 -0
- lp2graph/mining/ingest/latex_normalizer.py +243 -0
- lp2graph/mining/ingest/pyomo_importer.py +297 -0
- lp2graph/mining/ingest/result.py +124 -0
- lp2graph/mining/isomorphism/__init__.py +26 -0
- lp2graph/mining/isomorphism/report.py +178 -0
- lp2graph/mining/label/__init__.py +70 -0
- lp2graph/mining/label/classifier.py +161 -0
- lp2graph/mining/label/features.py +35 -0
- lp2graph/mining/label/guardrails.py +176 -0
- lp2graph/mining/label/loop.py +314 -0
- lp2graph/mining/label/rules.py +92 -0
- lp2graph/mining/label/store.py +164 -0
- lp2graph/mining/label/vocab.py +64 -0
- lp2graph/mining/provenance.py +90 -0
- lp2graph/mining/versions.py +51 -0
- lp2graph/nl/__init__.py +15 -0
- lp2graph/nl/describe.py +301 -0
- lp2graph/render/__init__.py +11 -0
- lp2graph/render/palette.py +80 -0
- lp2graph/render/svg.py +220 -0
- lp2graph/solve/__init__.py +50 -0
- lp2graph/solve/grounder.py +405 -0
- lp2graph/solve/instance.py +76 -0
- lp2graph/transform/__init__.py +30 -0
- lp2graph/transform/bigm.py +173 -0
- lp2graph/views/__init__.py +17 -0
- lp2graph/views/ground.py +477 -0
- lp2graph/views/hybrid.py +202 -0
- lp2graph/views/schema.py +208 -0
- lp2graph-0.3.0.dist-info/METADATA +206 -0
- lp2graph-0.3.0.dist-info/RECORD +80 -0
- lp2graph-0.3.0.dist-info/WHEEL +4 -0
- lp2graph-0.3.0.dist-info/entry_points.txt +2 -0
- lp2graph-0.3.0.dist-info/licenses/LICENSE +205 -0
lp2graph/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""lp2graph — typed-graph representation of LP, MIP, and MILP formulations.
|
|
2
|
+
|
|
3
|
+
The public API is intentionally small. The canonical entry points are:
|
|
4
|
+
|
|
5
|
+
from lp2graph import load, validate
|
|
6
|
+
from lp2graph import views, metrics, render, export
|
|
7
|
+
|
|
8
|
+
See ``docs/data-model.md`` for the schema and ``docs/views.md`` for the
|
|
9
|
+
view derivations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from lp2graph.codec import (
|
|
15
|
+
canonical_normal_form,
|
|
16
|
+
from_canonical_latex,
|
|
17
|
+
to_canonical_latex,
|
|
18
|
+
)
|
|
19
|
+
from lp2graph.core.loader import load, loads
|
|
20
|
+
from lp2graph.core.model import (
|
|
21
|
+
Binding,
|
|
22
|
+
ConstraintTemplate,
|
|
23
|
+
Formulation,
|
|
24
|
+
Index,
|
|
25
|
+
Objective,
|
|
26
|
+
Parameter,
|
|
27
|
+
Quantifier,
|
|
28
|
+
Term,
|
|
29
|
+
VariableTemplate,
|
|
30
|
+
)
|
|
31
|
+
from lp2graph.core.validate import ValidationError, validate
|
|
32
|
+
from lp2graph.nl import describe
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Binding",
|
|
36
|
+
"ConstraintTemplate",
|
|
37
|
+
"Formulation",
|
|
38
|
+
"Index",
|
|
39
|
+
"Objective",
|
|
40
|
+
"Parameter",
|
|
41
|
+
"Quantifier",
|
|
42
|
+
"Term",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
"VariableTemplate",
|
|
45
|
+
"canonical_normal_form",
|
|
46
|
+
"describe",
|
|
47
|
+
"from_canonical_latex",
|
|
48
|
+
"load",
|
|
49
|
+
"loads",
|
|
50
|
+
"to_canonical_latex",
|
|
51
|
+
"validate",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
__version__ = "0.3.0"
|
lp2graph/cli.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Command-line entry point: ``lp2graph``.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
- ``lp2graph validate <file>`` — load and validate a formulation file.
|
|
6
|
+
- ``lp2graph view <file> --view {schema,hybrid,ground} [--card I=5 ...]``
|
|
7
|
+
— derive a view and print a summary.
|
|
8
|
+
- ``lp2graph render <file> --view ... --output graph.svg`` — render to
|
|
9
|
+
SVG.
|
|
10
|
+
- ``lp2graph metrics <file>`` — compute and print all metrics.
|
|
11
|
+
- ``lp2graph export <file> --format {networkx,pyg,dgl,latex,pyomo}`` —
|
|
12
|
+
export.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from lp2graph import load
|
|
24
|
+
from lp2graph import validate as _validate
|
|
25
|
+
from lp2graph import views as _views
|
|
26
|
+
from lp2graph.metrics.flags import presence_flags
|
|
27
|
+
from lp2graph.metrics.structural import structural_summary
|
|
28
|
+
from lp2graph.render.svg import render_svg
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from lp2graph.core.graph import Graph
|
|
32
|
+
from lp2graph.core.model import Formulation
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main(argv: list[str] | None = None) -> int:
|
|
36
|
+
parser = argparse.ArgumentParser(prog="lp2graph")
|
|
37
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
38
|
+
|
|
39
|
+
p_validate = sub.add_parser("validate", help="Validate a formulation file.")
|
|
40
|
+
p_validate.add_argument("path", type=Path)
|
|
41
|
+
|
|
42
|
+
p_view = sub.add_parser("view", help="Derive a view and print a summary.")
|
|
43
|
+
p_view.add_argument("path", type=Path)
|
|
44
|
+
p_view.add_argument("--view", choices=("schema", "hybrid", "ground"), default="schema")
|
|
45
|
+
p_view.add_argument(
|
|
46
|
+
"--card",
|
|
47
|
+
action="append",
|
|
48
|
+
default=[],
|
|
49
|
+
help="Cardinality, e.g. --card I=5 --card T=8 (only for --view ground).",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
p_render = sub.add_parser("render", help="Render a view to SVG.")
|
|
53
|
+
p_render.add_argument("path", type=Path)
|
|
54
|
+
p_render.add_argument("--view", choices=("schema", "hybrid", "ground"), default="hybrid")
|
|
55
|
+
p_render.add_argument("--card", action="append", default=[])
|
|
56
|
+
p_render.add_argument("--output", type=Path, required=True)
|
|
57
|
+
|
|
58
|
+
p_metrics = sub.add_parser("metrics", help="Compute all metrics.")
|
|
59
|
+
p_metrics.add_argument("path", type=Path)
|
|
60
|
+
p_metrics.add_argument("--view", choices=("schema", "hybrid"), default="schema")
|
|
61
|
+
|
|
62
|
+
p_export = sub.add_parser("export", help="Export to a downstream format.")
|
|
63
|
+
p_export.add_argument("path", type=Path)
|
|
64
|
+
p_export.add_argument(
|
|
65
|
+
"--format",
|
|
66
|
+
choices=("networkx", "pyg", "dgl", "latex", "pyomo"),
|
|
67
|
+
required=True,
|
|
68
|
+
)
|
|
69
|
+
p_export.add_argument("--view", choices=("schema", "hybrid", "ground"), default="hybrid")
|
|
70
|
+
p_export.add_argument("--card", action="append", default=[])
|
|
71
|
+
p_export.add_argument("--output", type=Path, default=None)
|
|
72
|
+
|
|
73
|
+
p_latex = sub.add_parser(
|
|
74
|
+
"latex", help="Emit reversible, paper-style canonical LaTeX (graph -> text)."
|
|
75
|
+
)
|
|
76
|
+
p_latex.add_argument("path", type=Path)
|
|
77
|
+
p_latex.add_argument("--output", type=Path, default=None)
|
|
78
|
+
|
|
79
|
+
p_parse = sub.add_parser(
|
|
80
|
+
"parse", help="Parse a canonical-LaTeX document back to JSON (text -> graph)."
|
|
81
|
+
)
|
|
82
|
+
p_parse.add_argument("path", type=Path, help="A .tex file produced by `lp2graph latex`.")
|
|
83
|
+
p_parse.add_argument("--output", type=Path, default=None)
|
|
84
|
+
|
|
85
|
+
p_describe = sub.add_parser(
|
|
86
|
+
"describe",
|
|
87
|
+
help="Generate a natural-language problem description (graph -> text).",
|
|
88
|
+
)
|
|
89
|
+
p_describe.add_argument("path", type=Path)
|
|
90
|
+
p_describe.add_argument("--instance", type=Path, default=None)
|
|
91
|
+
p_describe.add_argument("--output", type=Path, default=None)
|
|
92
|
+
|
|
93
|
+
p_solve = sub.add_parser("solve", help="Ground with instance data and solve the MILP.")
|
|
94
|
+
p_solve.add_argument("path", type=Path)
|
|
95
|
+
p_solve.add_argument("--instance", type=Path, required=True)
|
|
96
|
+
p_solve.add_argument("--solver", choices=("cbc", "highs", "gurobi"), default="cbc")
|
|
97
|
+
|
|
98
|
+
args = parser.parse_args(argv)
|
|
99
|
+
|
|
100
|
+
if args.cmd == "validate":
|
|
101
|
+
f = load(args.path)
|
|
102
|
+
_validate(f)
|
|
103
|
+
print(f"OK: {f.id} ({f.family})")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
if args.cmd == "view":
|
|
107
|
+
f = load(args.path)
|
|
108
|
+
g = _derive(f, args.view, args.card)
|
|
109
|
+
print(json.dumps({"nodes": len(g.nodes), "edges": len(g.edges), "view": g.view}, indent=2))
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
if args.cmd == "render":
|
|
113
|
+
f = load(args.path)
|
|
114
|
+
g = _derive(f, args.view, args.card)
|
|
115
|
+
svg = render_svg(g, title=f.name)
|
|
116
|
+
args.output.write_text(svg, encoding="utf-8")
|
|
117
|
+
print(f"wrote {args.output}")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
if args.cmd == "metrics":
|
|
121
|
+
f = load(args.path)
|
|
122
|
+
g = _derive(f, args.view, [])
|
|
123
|
+
result: dict[str, object] = {}
|
|
124
|
+
for name, m in structural_summary(g).items():
|
|
125
|
+
result[name] = m.value
|
|
126
|
+
for name, m in presence_flags(f).items():
|
|
127
|
+
result[name] = m.value
|
|
128
|
+
print(json.dumps(result, indent=2, default=str))
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
if args.cmd == "export":
|
|
132
|
+
f = load(args.path)
|
|
133
|
+
if args.format == "latex":
|
|
134
|
+
from lp2graph.export.latex import to_latex
|
|
135
|
+
|
|
136
|
+
out = to_latex(f)
|
|
137
|
+
elif args.format == "pyomo":
|
|
138
|
+
from lp2graph.export.pyomo_stub import to_pyomo_stub
|
|
139
|
+
|
|
140
|
+
out = to_pyomo_stub(f)
|
|
141
|
+
else:
|
|
142
|
+
g = _derive(f, args.view, args.card)
|
|
143
|
+
if args.format == "networkx":
|
|
144
|
+
from lp2graph.export.networkx_adapter import to_networkx
|
|
145
|
+
|
|
146
|
+
nxg = to_networkx(g)
|
|
147
|
+
out = f"<NetworkX MultiDiGraph: {len(nxg)} nodes, {nxg.number_of_edges()} edges>"
|
|
148
|
+
elif args.format == "pyg":
|
|
149
|
+
from lp2graph.export.pyg import to_pyg
|
|
150
|
+
|
|
151
|
+
pyg = to_pyg(g)
|
|
152
|
+
out = repr(pyg)
|
|
153
|
+
else: # dgl
|
|
154
|
+
from lp2graph.export.dgl import to_dgl
|
|
155
|
+
|
|
156
|
+
dglg = to_dgl(g)
|
|
157
|
+
out = repr(dglg)
|
|
158
|
+
if args.output:
|
|
159
|
+
args.output.write_text(out, encoding="utf-8")
|
|
160
|
+
print(f"wrote {args.output}")
|
|
161
|
+
else:
|
|
162
|
+
print(out)
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
if args.cmd == "latex":
|
|
166
|
+
from lp2graph.codec import to_canonical_latex
|
|
167
|
+
|
|
168
|
+
out = to_canonical_latex(load(args.path))
|
|
169
|
+
_emit(out, args.output)
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
if args.cmd == "parse":
|
|
173
|
+
from lp2graph.codec import from_canonical_latex
|
|
174
|
+
|
|
175
|
+
text = args.path.read_text(encoding="utf-8")
|
|
176
|
+
f = from_canonical_latex(text)
|
|
177
|
+
_emit(f.model_dump_json(indent=2, exclude_defaults=True), args.output)
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
if args.cmd == "describe":
|
|
181
|
+
from lp2graph.nl import describe
|
|
182
|
+
|
|
183
|
+
inst = None
|
|
184
|
+
if args.instance is not None:
|
|
185
|
+
from lp2graph.solve import Instance
|
|
186
|
+
|
|
187
|
+
inst = Instance.load(args.instance)
|
|
188
|
+
out = describe(load(args.path), inst)
|
|
189
|
+
_emit(out, args.output)
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
if args.cmd == "solve":
|
|
193
|
+
import pulp
|
|
194
|
+
|
|
195
|
+
from lp2graph.solve import Instance, solve
|
|
196
|
+
|
|
197
|
+
solver = {
|
|
198
|
+
"cbc": lambda: pulp.PULP_CBC_CMD(msg=0, threads=1),
|
|
199
|
+
"highs": lambda: pulp.HiGHS(msg=False),
|
|
200
|
+
"gurobi": lambda: pulp.GUROBI(msg=0),
|
|
201
|
+
}[args.solver]()
|
|
202
|
+
res = solve(load(args.path), Instance.load(args.instance), solver=solver)
|
|
203
|
+
print(
|
|
204
|
+
json.dumps(
|
|
205
|
+
{
|
|
206
|
+
"status": res.status,
|
|
207
|
+
"objective": res.objective,
|
|
208
|
+
"n_vars": res.n_vars,
|
|
209
|
+
"n_constraints": res.n_constraints,
|
|
210
|
+
"solver": res.solver,
|
|
211
|
+
},
|
|
212
|
+
indent=2,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _emit(text: str, output: Path | None) -> None:
|
|
221
|
+
if output is not None:
|
|
222
|
+
output.write_text(text, encoding="utf-8")
|
|
223
|
+
print(f"wrote {output}")
|
|
224
|
+
else:
|
|
225
|
+
print(text)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _derive(f: Formulation, view: str, card_args: list[str]) -> Graph:
|
|
229
|
+
if view == "schema":
|
|
230
|
+
return _views.schema(f)
|
|
231
|
+
if view == "hybrid":
|
|
232
|
+
return _views.hybrid(f)
|
|
233
|
+
cards = {k: int(v) for arg in card_args for k, v in [arg.split("=", 1)]}
|
|
234
|
+
return _views.ground(f, cards)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
sys.exit(main())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Deterministic, reversible LaTeX <-> canonical-model codec.
|
|
2
|
+
|
|
3
|
+
This subpackage adds a *bidirectional, deterministic* text interface on
|
|
4
|
+
top of the canonical model:
|
|
5
|
+
|
|
6
|
+
- :func:`to_canonical_latex` renders a :class:`~lp2graph.core.model.Formulation`
|
|
7
|
+
as a paper-style LaTeX document (``\\mathcal`` index sets, ``\\sum``,
|
|
8
|
+
``\\forall`` quantifiers, big-M, ...). The algebra is genuine LaTeX an
|
|
9
|
+
author could paste into a paper; a compact ``%@`` annotation header
|
|
10
|
+
carries the non-algebraic metadata (kinds, roles, descriptions, ...).
|
|
11
|
+
|
|
12
|
+
- :func:`from_canonical_latex` parses that document back into a
|
|
13
|
+
``Formulation``. The parse is *deterministic*: same text in, same model
|
|
14
|
+
out, no model/LLM in the loop.
|
|
15
|
+
|
|
16
|
+
The two functions are inverses up to a documented normalization
|
|
17
|
+
(:func:`lp2graph.codec.normalize.canonical_normal_form`). The strongest
|
|
18
|
+
guarantee the codec offers — and the one the tests assert — is text-level
|
|
19
|
+
idempotence::
|
|
20
|
+
|
|
21
|
+
to_canonical_latex(from_canonical_latex(to_canonical_latex(f))) == to_canonical_latex(f)
|
|
22
|
+
|
|
23
|
+
i.e. the LaTeX serialization is a fixed point. The *solvable* content
|
|
24
|
+
(sets, parameters, variables, constraints, objective) round-trips
|
|
25
|
+
exactly; only incidental labels (a literal term's ``ref`` name, a
|
|
26
|
+
redundant ``offset`` field that disagrees with its own ``expr``) are
|
|
27
|
+
normalized.
|
|
28
|
+
|
|
29
|
+
See :mod:`lp2graph.codec.latex` for the grammar.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from lp2graph.codec.latex import from_canonical_latex, to_canonical_latex
|
|
35
|
+
from lp2graph.codec.normalize import canonical_normal_form
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"canonical_normal_form",
|
|
39
|
+
"from_canonical_latex",
|
|
40
|
+
"to_canonical_latex",
|
|
41
|
+
]
|