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.
Files changed (80) hide show
  1. lp2graph/__init__.py +54 -0
  2. lp2graph/cli.py +238 -0
  3. lp2graph/codec/__init__.py +41 -0
  4. lp2graph/codec/latex.py +884 -0
  5. lp2graph/codec/normalize.py +82 -0
  6. lp2graph/core/__init__.py +35 -0
  7. lp2graph/core/graph.py +183 -0
  8. lp2graph/core/loader.py +63 -0
  9. lp2graph/core/model.py +437 -0
  10. lp2graph/core/validate.py +237 -0
  11. lp2graph/export/__init__.py +13 -0
  12. lp2graph/export/dgl.py +51 -0
  13. lp2graph/export/latex.py +126 -0
  14. lp2graph/export/networkx_adapter.py +50 -0
  15. lp2graph/export/pyg.py +79 -0
  16. lp2graph/export/pyomo_stub.py +81 -0
  17. lp2graph/metrics/__init__.py +58 -0
  18. lp2graph/metrics/classification.py +113 -0
  19. lp2graph/metrics/flags.py +122 -0
  20. lp2graph/metrics/result.py +26 -0
  21. lp2graph/metrics/structural.py +236 -0
  22. lp2graph/mining/__init__.py +47 -0
  23. lp2graph/mining/cluster/__init__.py +65 -0
  24. lp2graph/mining/cluster/agglomerative.py +82 -0
  25. lp2graph/mining/cluster/distance.py +65 -0
  26. lp2graph/mining/cluster/operator.py +218 -0
  27. lp2graph/mining/cluster/silhouette.py +88 -0
  28. lp2graph/mining/cluster/stability.py +178 -0
  29. lp2graph/mining/cluster/taxonomy.py +268 -0
  30. lp2graph/mining/corpusmgr/__init__.py +70 -0
  31. lp2graph/mining/corpusmgr/dedup.py +183 -0
  32. lp2graph/mining/corpusmgr/manager.py +79 -0
  33. lp2graph/mining/corpusmgr/manifest.py +82 -0
  34. lp2graph/mining/corpusmgr/record.py +101 -0
  35. lp2graph/mining/corpusmgr/select.py +128 -0
  36. lp2graph/mining/homologize/__init__.py +82 -0
  37. lp2graph/mining/homologize/concept.py +134 -0
  38. lp2graph/mining/homologize/entity.py +217 -0
  39. lp2graph/mining/homologize/lemmatize.py +80 -0
  40. lp2graph/mining/homologize/signature.py +166 -0
  41. lp2graph/mining/homologize/thesaurus.py +70 -0
  42. lp2graph/mining/homologize/tokenize.py +255 -0
  43. lp2graph/mining/homologize/vectorize.py +141 -0
  44. lp2graph/mining/ingest/__init__.py +59 -0
  45. lp2graph/mining/ingest/code_importers.py +104 -0
  46. lp2graph/mining/ingest/dispatch.py +148 -0
  47. lp2graph/mining/ingest/latex_normalizer.py +243 -0
  48. lp2graph/mining/ingest/pyomo_importer.py +297 -0
  49. lp2graph/mining/ingest/result.py +124 -0
  50. lp2graph/mining/isomorphism/__init__.py +26 -0
  51. lp2graph/mining/isomorphism/report.py +178 -0
  52. lp2graph/mining/label/__init__.py +70 -0
  53. lp2graph/mining/label/classifier.py +161 -0
  54. lp2graph/mining/label/features.py +35 -0
  55. lp2graph/mining/label/guardrails.py +176 -0
  56. lp2graph/mining/label/loop.py +314 -0
  57. lp2graph/mining/label/rules.py +92 -0
  58. lp2graph/mining/label/store.py +164 -0
  59. lp2graph/mining/label/vocab.py +64 -0
  60. lp2graph/mining/provenance.py +90 -0
  61. lp2graph/mining/versions.py +51 -0
  62. lp2graph/nl/__init__.py +15 -0
  63. lp2graph/nl/describe.py +301 -0
  64. lp2graph/render/__init__.py +11 -0
  65. lp2graph/render/palette.py +80 -0
  66. lp2graph/render/svg.py +220 -0
  67. lp2graph/solve/__init__.py +50 -0
  68. lp2graph/solve/grounder.py +405 -0
  69. lp2graph/solve/instance.py +76 -0
  70. lp2graph/transform/__init__.py +30 -0
  71. lp2graph/transform/bigm.py +173 -0
  72. lp2graph/views/__init__.py +17 -0
  73. lp2graph/views/ground.py +477 -0
  74. lp2graph/views/hybrid.py +202 -0
  75. lp2graph/views/schema.py +208 -0
  76. lp2graph-0.3.0.dist-info/METADATA +206 -0
  77. lp2graph-0.3.0.dist-info/RECORD +80 -0
  78. lp2graph-0.3.0.dist-info/WHEEL +4 -0
  79. lp2graph-0.3.0.dist-info/entry_points.txt +2 -0
  80. 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
+ ]