solverpilot 0.1__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.
- solverpilot/__init__.py +250 -0
- solverpilot/_immutability.py +27 -0
- solverpilot/_version.py +3 -0
- solverpilot/applications/__init__.py +3 -0
- solverpilot/applications/routing/__init__.py +19 -0
- solverpilot/applications/routing/compile_milp.py +321 -0
- solverpilot/applications/routing/diagnostics.py +78 -0
- solverpilot/applications/routing/errors.py +14 -0
- solverpilot/applications/routing/heuristics.py +167 -0
- solverpilot/applications/routing/model.py +259 -0
- solverpilot/applications/routing/reference.py +81 -0
- solverpilot/applications/routing/solution.py +89 -0
- solverpilot/applications/routing/timeline.py +79 -0
- solverpilot/applications/routing/validation.py +169 -0
- solverpilot/applications/tsp/__init__.py +41 -0
- solverpilot/applications/tsp/compile_cp.py +121 -0
- solverpilot/applications/tsp/compile_milp.py +139 -0
- solverpilot/applications/tsp/distance.py +60 -0
- solverpilot/applications/tsp/errors.py +17 -0
- solverpilot/applications/tsp/heuristics.py +112 -0
- solverpilot/applications/tsp/model.py +175 -0
- solverpilot/applications/tsp/reference.py +121 -0
- solverpilot/applications/tsp/solution.py +65 -0
- solverpilot/applications/tsp/validation.py +92 -0
- solverpilot/backends/__init__.py +51 -0
- solverpilot/backends/_casadi_highs_worker.py +92 -0
- solverpilot/backends/base.py +28 -0
- solverpilot/backends/bundled_capi.py +910 -0
- solverpilot/backends/casadi_conic.py +303 -0
- solverpilot/backends/health.py +240 -0
- solverpilot/backends/highspy_native.py +217 -0
- solverpilot/backends/nlopt_native.py +237 -0
- solverpilot/backends/osqp_native.py +297 -0
- solverpilot/backends/protocol_v2.py +119 -0
- solverpilot/backends/pyscipopt_native.py +200 -0
- solverpilot/backends/registry.py +38 -0
- solverpilot/backends/scipy_highs.py +138 -0
- solverpilot/backends/scipy_highs_lp.py +195 -0
- solverpilot/backends/scipy_slsqp_qp.py +133 -0
- solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
- solverpilot/benchmark/__init__.py +23 -0
- solverpilot/benchmark/acquire.py +199 -0
- solverpilot/benchmark/baselines.py +70 -0
- solverpilot/benchmark/bundle.py +37 -0
- solverpilot/benchmark/campaign.py +150 -0
- solverpilot/benchmark/environment.py +115 -0
- solverpilot/benchmark/integrity.py +46 -0
- solverpilot/benchmark/model.py +146 -0
- solverpilot/benchmark/policy.py +182 -0
- solverpilot/benchmark/registry.py +38 -0
- solverpilot/benchmark/report.py +44 -0
- solverpilot/benchmark/runner.py +516 -0
- solverpilot/benchmark/slurm.py +55 -0
- solverpilot/benchmark/specs.py +75 -0
- solverpilot/benchmark/splits.py +181 -0
- solverpilot/benchmark/summary.py +384 -0
- solverpilot/benchmark/verify.py +142 -0
- solverpilot/benchmark/worker.py +71 -0
- solverpilot/bridges/__init__.py +19 -0
- solverpilot/bridges/engine.py +44 -0
- solverpilot/bridges/indicator.py +232 -0
- solverpilot/bridges/registry.py +78 -0
- solverpilot/bridges/transformation-tape.schema.json +53 -0
- solverpilot/bridges/types.py +149 -0
- solverpilot/bridges/validation.py +110 -0
- solverpilot/capabilities/__init__.py +32 -0
- solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
- solverpilot/capabilities/conformance.py +151 -0
- solverpilot/capabilities/enums.py +27 -0
- solverpilot/capabilities/manifest.py +19 -0
- solverpilot/capabilities/ontology.py +73 -0
- solverpilot/capabilities/requirements.py +37 -0
- solverpilot/capabilities/resolve.py +30 -0
- solverpilot/capabilities/v2.py +711 -0
- solverpilot/cli/__init__.py +1 -0
- solverpilot/cli/backend_health.py +77 -0
- solverpilot/cli/benchmark.py +331 -0
- solverpilot/cli/benchmark_worker.py +33 -0
- solverpilot/cli/capabilities.py +65 -0
- solverpilot/conic/__init__.py +11 -0
- solverpilot/conic/backend.py +263 -0
- solverpilot/conic/compiler.py +310 -0
- solverpilot/conic/conformance.py +60 -0
- solverpilot/conic/conic-ir-v1.schema.json +24 -0
- solverpilot/conic/ir.py +299 -0
- solverpilot/conic/validation.py +149 -0
- solverpilot/cp/__init__.py +7 -0
- solverpilot/cp/conformance.py +32 -0
- solverpilot/cp/cp-ir-v1.schema.json +13 -0
- solverpilot/cp/ir.py +170 -0
- solverpilot/cp/model.py +143 -0
- solverpilot/cp/ortools_backend.py +423 -0
- solverpilot/cp/ortools_worker.py +75 -0
- solverpilot/cp/reference.py +51 -0
- solverpilot/cp/serialization.py +117 -0
- solverpilot/cp/validate.py +83 -0
- solverpilot/diagnose/__init__.py +33 -0
- solverpilot/diagnose/infeasibility.py +463 -0
- solverpilot/diagnose/model.py +87 -0
- solverpilot/evaluation/__init__.py +53 -0
- solverpilot/evaluation/metrics.py +150 -0
- solverpilot/evaluation/oracle.py +288 -0
- solverpilot/evaluation/proof.py +53 -0
- solverpilot/evaluation/references.py +73 -0
- solverpilot/evaluation/regret.py +281 -0
- solverpilot/exceptions.py +34 -0
- solverpilot/experimental/__init__.py +32 -0
- solverpilot/extensions/__init__.py +56 -0
- solverpilot/extensions/entrypoints.py +34 -0
- solverpilot/extensions/errors.py +35 -0
- solverpilot/extensions/manager.py +651 -0
- solverpilot/extensions/manifest.py +232 -0
- solverpilot/extensions/model.py +101 -0
- solverpilot/extensions/versioning.py +136 -0
- solverpilot/history/__init__.py +22 -0
- solverpilot/history/errors.py +15 -0
- solverpilot/history/model.py +129 -0
- solverpilot/history/sqlite.py +651 -0
- solverpilot/inspect/__init__.py +3 -0
- solverpilot/inspect/fingerprint.py +199 -0
- solverpilot/intelligence/__init__.py +36 -0
- solverpilot/intelligence/errors.py +16 -0
- solverpilot/intelligence/fingerprint.py +108 -0
- solverpilot/intelligence/leakage.py +69 -0
- solverpilot/intelligence/schema.py +314 -0
- solverpilot/intelligence/shift.py +208 -0
- solverpilot/io/__init__.py +41 -0
- solverpilot/io/binary64.py +49 -0
- solverpilot/io/csv.py +230 -0
- solverpilot/io/errors.py +25 -0
- solverpilot/io/hashing.py +111 -0
- solverpilot/io/json.py +90 -0
- solverpilot/io/mapping.py +195 -0
- solverpilot/io/model.py +407 -0
- solverpilot/io/numeric.py +108 -0
- solverpilot/io/provenance.py +114 -0
- solverpilot/io/result.py +45 -0
- solverpilot/io/source.py +104 -0
- solverpilot/io/strict_json.py +133 -0
- solverpilot/minlp/__init__.py +6 -0
- solverpilot/minlp/compiler.py +66 -0
- solverpilot/minlp/conformance.py +34 -0
- solverpilot/minlp/convexity.py +85 -0
- solverpilot/minlp/ir.py +35 -0
- solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
- solverpilot/minlp/orchestrator.py +220 -0
- solverpilot/minlp/validation.py +28 -0
- solverpilot/model/__init__.py +27 -0
- solverpilot/model/compiler.py +1263 -0
- solverpilot/model/errors.py +22 -0
- solverpilot/model/expression.py +373 -0
- solverpilot/model/model.py +716 -0
- solverpilot/model/sets.py +60 -0
- solverpilot/model/types.py +36 -0
- solverpilot/nlp/__init__.py +7 -0
- solverpilot/nlp/ad.py +166 -0
- solverpilot/nlp/backend.py +113 -0
- solverpilot/nlp/compiler.py +116 -0
- solverpilot/nlp/conformance.py +58 -0
- solverpilot/nlp/ir.py +70 -0
- solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
- solverpilot/nlp/validation.py +60 -0
- solverpilot/plan/__init__.py +33 -0
- solverpilot/plan/model.py +79 -0
- solverpilot/plan/planner.py +201 -0
- solverpilot/plan/production.py +627 -0
- solverpilot/plan/selective_lp.py +65 -0
- solverpilot/problem/__init__.py +16 -0
- solverpilot/problem/enums.py +20 -0
- solverpilot/problem/hashing.py +88 -0
- solverpilot/problem/linear.py +196 -0
- solverpilot/problem/mps.py +447 -0
- solverpilot/problem/quadratic.py +192 -0
- solverpilot/reporting/__init__.py +27 -0
- solverpilot/reporting/explain.py +327 -0
- solverpilot/reporting/model.py +264 -0
- solverpilot/reporting/render.py +55 -0
- solverpilot/runtime/__init__.py +19 -0
- solverpilot/runtime/auto.py +193 -0
- solverpilot/runtime/budgeting.py +43 -0
- solverpilot/runtime/executor.py +126 -0
- solverpilot/runtime/portfolio.py +197 -0
- solverpilot/runtime/result.py +72 -0
- solverpilot/session/__init__.py +29 -0
- solverpilot/session/conformance_v2.py +239 -0
- solverpilot/session/mutations.py +115 -0
- solverpilot/session/persistent-session-trace.schema.json +45 -0
- solverpilot/session/persistent.py +491 -0
- solverpilot/session/reuse.py +81 -0
- solverpilot/session/session.py +236 -0
- solverpilot/trace/__init__.py +3 -0
- solverpilot/trace/schema.py +47 -0
- solverpilot/validate/__init__.py +10 -0
- solverpilot/validate/core.py +179 -0
- solverpilot/validate/result.py +55 -0
- solverpilot-0.1.dist-info/METADATA +554 -0
- solverpilot-0.1.dist-info/RECORD +201 -0
- solverpilot-0.1.dist-info/WHEEL +5 -0
- solverpilot-0.1.dist-info/entry_points.txt +4 -0
- solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
- solverpilot-0.1.dist-info/top_level.txt +1 -0
solverpilot/io/source.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Fail-closed helpers for local file and declared-bundle ingestion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from .errors import DataIOSourceError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _absolute_without_resolving(path: Path) -> Path:
|
|
12
|
+
return Path(os.path.abspath(os.fspath(path)))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _reject_symlink_components(path: Path) -> None:
|
|
16
|
+
absolute = _absolute_without_resolving(path)
|
|
17
|
+
current = Path(absolute.anchor)
|
|
18
|
+
for part in absolute.parts[1:]:
|
|
19
|
+
current = current / part
|
|
20
|
+
try:
|
|
21
|
+
if current.is_symlink():
|
|
22
|
+
raise DataIOSourceError("Symbolic links are rejected in data source paths.")
|
|
23
|
+
except OSError as exc:
|
|
24
|
+
raise DataIOSourceError("Local source path components cannot be inspected safely.") from exc
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_local_path(
|
|
28
|
+
path: str | Path,
|
|
29
|
+
*,
|
|
30
|
+
expect_directory: bool | None,
|
|
31
|
+
reject_symlinks: bool = True,
|
|
32
|
+
) -> Path:
|
|
33
|
+
if not isinstance(path, (str, os.PathLike)):
|
|
34
|
+
raise DataIOSourceError("Local source path must be path-like.")
|
|
35
|
+
if isinstance(path, str) and not path.strip():
|
|
36
|
+
raise DataIOSourceError("Local source path must be non-empty.")
|
|
37
|
+
candidate = Path(path)
|
|
38
|
+
if reject_symlinks:
|
|
39
|
+
_reject_symlink_components(candidate)
|
|
40
|
+
try:
|
|
41
|
+
exists = candidate.exists()
|
|
42
|
+
except OSError as exc:
|
|
43
|
+
raise DataIOSourceError("Local source path cannot be inspected safely.") from exc
|
|
44
|
+
if not exists:
|
|
45
|
+
raise DataIOSourceError(f"Local source does not exist: {candidate.name or 'source'}.")
|
|
46
|
+
try:
|
|
47
|
+
is_directory = candidate.is_dir()
|
|
48
|
+
is_file = candidate.is_file()
|
|
49
|
+
except OSError as exc:
|
|
50
|
+
raise DataIOSourceError("Local source type cannot be inspected safely.") from exc
|
|
51
|
+
if expect_directory is True and not is_directory:
|
|
52
|
+
raise DataIOSourceError("Data source must be a directory bundle.")
|
|
53
|
+
if expect_directory is False and not is_file:
|
|
54
|
+
raise DataIOSourceError("Data source must be a regular file.")
|
|
55
|
+
if expect_directory is None and not (is_file or is_directory):
|
|
56
|
+
raise DataIOSourceError("Data source must be a regular file or directory bundle.")
|
|
57
|
+
return candidate
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def safe_bundle_child(
|
|
61
|
+
bundle: Path,
|
|
62
|
+
file_name: str,
|
|
63
|
+
*,
|
|
64
|
+
reject_symlinks: bool = True,
|
|
65
|
+
) -> Path:
|
|
66
|
+
if not isinstance(file_name, str) or not file_name.strip():
|
|
67
|
+
raise DataIOSourceError("Bundle file name must be a non-empty string.")
|
|
68
|
+
posix = PurePosixPath(file_name.strip().replace("\\", "/"))
|
|
69
|
+
if posix.is_absolute() or not posix.parts or any(
|
|
70
|
+
part in {"", ".", ".."} or ":" in part for part in posix.parts
|
|
71
|
+
):
|
|
72
|
+
raise DataIOSourceError("Bundle file name must be a safe relative path.")
|
|
73
|
+
child = bundle.joinpath(*posix.parts)
|
|
74
|
+
if reject_symlinks:
|
|
75
|
+
_reject_symlink_components(bundle)
|
|
76
|
+
_reject_symlink_components(child)
|
|
77
|
+
try:
|
|
78
|
+
bundle_resolved = bundle.resolve(strict=True)
|
|
79
|
+
child_resolved = child.resolve(strict=True)
|
|
80
|
+
except FileNotFoundError as exc:
|
|
81
|
+
raise DataIOSourceError(f"Declared bundle file is missing: {posix.as_posix()}.") from exc
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
raise DataIOSourceError("Bundle paths cannot be resolved safely.") from exc
|
|
84
|
+
if bundle_resolved != child_resolved.parent and bundle_resolved not in child_resolved.parents:
|
|
85
|
+
raise DataIOSourceError("Declared bundle file escapes the bundle directory.")
|
|
86
|
+
if not child_resolved.is_file():
|
|
87
|
+
raise DataIOSourceError(f"Declared bundle entry is not a file: {posix.as_posix()}.")
|
|
88
|
+
return child_resolved
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def read_limited_bytes(path: Path, *, max_bytes: int) -> bytes:
|
|
92
|
+
"""Read at most ``max_bytes`` and detect growth without trusting a prior stat."""
|
|
93
|
+
if type(max_bytes) is not int or max_bytes <= 0:
|
|
94
|
+
raise DataIOSourceError("Maximum source size must be a positive integer.")
|
|
95
|
+
try:
|
|
96
|
+
with path.open("rb") as handle:
|
|
97
|
+
data = handle.read(max_bytes + 1)
|
|
98
|
+
except OSError as exc:
|
|
99
|
+
raise DataIOSourceError("Source bytes could not be read.") from exc
|
|
100
|
+
if len(data) > max_bytes:
|
|
101
|
+
raise DataIOSourceError(
|
|
102
|
+
f"Source exceeds the configured maximum size of {max_bytes} bytes."
|
|
103
|
+
)
|
|
104
|
+
return data
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Strict JSON parser shared by file ingestion and JSON-valued mapping fields."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .binary64 import Binary64ConversionError, decimal_token_to_binary64
|
|
11
|
+
from .errors import DataIOParseError
|
|
12
|
+
|
|
13
|
+
DEFAULT_MAX_JSON_DEPTH = 256
|
|
14
|
+
DEFAULT_MAX_INTEGER_DIGITS = 1000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _DuplicateKey(ValueError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _NonFiniteConstant(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _IntegerTooLarge(ValueError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _object_pairs_no_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
30
|
+
result: dict[str, Any] = {}
|
|
31
|
+
for key, value in pairs:
|
|
32
|
+
if key in result:
|
|
33
|
+
raise _DuplicateKey("duplicate object key")
|
|
34
|
+
result[key] = value
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reject_constant(value: str) -> None:
|
|
39
|
+
raise _NonFiniteConstant(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_bounded_int(value: str) -> int:
|
|
43
|
+
if len(value.lstrip("-")) > DEFAULT_MAX_INTEGER_DIGITS:
|
|
44
|
+
raise _IntegerTooLarge("integer digit limit exceeded")
|
|
45
|
+
return int(value, 10)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_tree(value: Any, *, max_depth: int, context: str) -> None:
|
|
49
|
+
stack: list[tuple[Any, int]] = [(value, 0)]
|
|
50
|
+
while stack:
|
|
51
|
+
current, depth = stack.pop()
|
|
52
|
+
if depth > max_depth:
|
|
53
|
+
raise DataIOParseError(f"{context} exceeds the maximum nesting depth of {max_depth}.")
|
|
54
|
+
if current is None or isinstance(current, (bool, int)):
|
|
55
|
+
continue
|
|
56
|
+
if isinstance(current, float):
|
|
57
|
+
if not math.isfinite(current):
|
|
58
|
+
raise DataIOParseError(f"{context} contains a non-finite number.")
|
|
59
|
+
continue
|
|
60
|
+
if isinstance(current, str):
|
|
61
|
+
try:
|
|
62
|
+
current.encode("utf-8")
|
|
63
|
+
except UnicodeEncodeError as exc:
|
|
64
|
+
raise DataIOParseError(f"{context} contains an invalid Unicode surrogate.") from exc
|
|
65
|
+
continue
|
|
66
|
+
if isinstance(current, Mapping):
|
|
67
|
+
for key, item in current.items():
|
|
68
|
+
if not isinstance(key, str):
|
|
69
|
+
raise DataIOParseError(f"{context} object keys must be strings.")
|
|
70
|
+
try:
|
|
71
|
+
key.encode("utf-8")
|
|
72
|
+
except UnicodeEncodeError as exc:
|
|
73
|
+
raise DataIOParseError(
|
|
74
|
+
f"{context} contains an invalid Unicode surrogate in an object key."
|
|
75
|
+
) from exc
|
|
76
|
+
stack.append((item, depth + 1))
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(current, Sequence) and not isinstance(current, (str, bytes, bytearray)):
|
|
79
|
+
stack.extend((item, depth + 1) for item in current)
|
|
80
|
+
continue
|
|
81
|
+
raise DataIOParseError(f"{context} contains an unsupported parsed value type.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def loads_strict_json(
|
|
85
|
+
text: str,
|
|
86
|
+
*,
|
|
87
|
+
context: str = "JSON input",
|
|
88
|
+
max_depth: int = DEFAULT_MAX_JSON_DEPTH,
|
|
89
|
+
strict_numbers: bool = True,
|
|
90
|
+
) -> Any:
|
|
91
|
+
"""Parse standards-compliant JSON with deterministic resource and numeric gates."""
|
|
92
|
+
if not isinstance(text, str):
|
|
93
|
+
raise DataIOParseError(f"{context} must be text.")
|
|
94
|
+
if type(max_depth) is not int or max_depth < 0:
|
|
95
|
+
raise DataIOParseError("max_depth must be a non-negative integer.")
|
|
96
|
+
if type(strict_numbers) is not bool:
|
|
97
|
+
raise DataIOParseError("strict_numbers must be a boolean.")
|
|
98
|
+
|
|
99
|
+
def parse_float(token: str) -> float:
|
|
100
|
+
try:
|
|
101
|
+
return decimal_token_to_binary64(token, exact_decimal=strict_numbers)
|
|
102
|
+
except Binary64ConversionError as exc:
|
|
103
|
+
raise Binary64ConversionError(str(exc)) from exc
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
parsed = json.loads(
|
|
107
|
+
text,
|
|
108
|
+
object_pairs_hook=_object_pairs_no_duplicates,
|
|
109
|
+
parse_constant=_reject_constant,
|
|
110
|
+
parse_int=_parse_bounded_int,
|
|
111
|
+
parse_float=parse_float,
|
|
112
|
+
)
|
|
113
|
+
except _DuplicateKey as exc:
|
|
114
|
+
raise DataIOParseError(f"{context} contains a duplicate object key.") from exc
|
|
115
|
+
except _NonFiniteConstant as exc:
|
|
116
|
+
raise DataIOParseError(f"{context} contains a non-finite JSON constant.") from exc
|
|
117
|
+
except _IntegerTooLarge as exc:
|
|
118
|
+
raise DataIOParseError(
|
|
119
|
+
f"{context} contains an integer exceeding {DEFAULT_MAX_INTEGER_DIGITS} digits."
|
|
120
|
+
) from exc
|
|
121
|
+
except Binary64ConversionError as exc:
|
|
122
|
+
raise DataIOParseError(
|
|
123
|
+
f"{context} contains a decimal number rejected by the binary64 ingestion policy."
|
|
124
|
+
) from exc
|
|
125
|
+
except json.JSONDecodeError as exc:
|
|
126
|
+
raise DataIOParseError(f"{context} is malformed at line {exc.lineno}, column {exc.colno}.") from exc
|
|
127
|
+
except RecursionError as exc:
|
|
128
|
+
raise DataIOParseError(f"{context} exceeds the supported nesting depth.") from exc
|
|
129
|
+
except (ValueError, OverflowError, UnicodeError) as exc:
|
|
130
|
+
raise DataIOParseError(f"{context} cannot be parsed safely.") from exc
|
|
131
|
+
|
|
132
|
+
_validate_tree(parsed, max_depth=max_depth, context=context)
|
|
133
|
+
return parsed
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .ir import MINLPProblem, ConvexityCertificate
|
|
2
|
+
from .compiler import compile_minlp_model
|
|
3
|
+
from .orchestrator import MINLPSolveResult, MINLPIteration, solve_binary_enumeration, solve_outer_approximation, solve_minlp
|
|
4
|
+
from .validation import MINLPValidationReport, validate_minlp_solution
|
|
5
|
+
from .conformance import MINLPConformanceCheck, MINLPConformanceReport, conform_minlp_orchestrator
|
|
6
|
+
__all__=['MINLPProblem','ConvexityCertificate','compile_minlp_model','MINLPSolveResult','MINLPIteration','solve_binary_enumeration','solve_outer_approximation','solve_minlp','MINLPValidationReport','validate_minlp_solution','MINLPConformanceCheck','MINLPConformanceReport','conform_minlp_orchestrator']
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import hashlib, json
|
|
3
|
+
import numpy as np
|
|
4
|
+
from solverpilot.model.errors import CompileError
|
|
5
|
+
from solverpilot.model.sets import LessThan, GreaterThan, EqualTo, Interval
|
|
6
|
+
from solverpilot.problem import ObjectiveSense, VariableDomain
|
|
7
|
+
from solverpilot.model.compiler import CompiledModel, CompilationReport, _variable_layout
|
|
8
|
+
from solverpilot.nlp.ir import NLPConstraintBlock, NLPProblem
|
|
9
|
+
from .ir import MINLPProblem, ConvexityCertificate
|
|
10
|
+
from .convexity import prove_curvature
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _broadcast(value,size):
|
|
14
|
+
try: return np.broadcast_to(np.asarray(value,dtype=float),(size,)).astype(float,copy=True)
|
|
15
|
+
except Exception as e: raise CompileError('MINLP set bound cannot broadcast') from e
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compile_minlp_model(model, *, use_cache=True):
|
|
19
|
+
if model.objective is None: raise CompileError('model has no objective')
|
|
20
|
+
if model.objective.sense is not ObjectiveSense.MINIMIZE: raise CompileError('P8 MINLP v1 supports minimization only')
|
|
21
|
+
if model.objective.expression.shape != (): raise CompileError('P8 MINLP objective must be scalar')
|
|
22
|
+
if any(c.__class__.__name__=='IndicatorConstraint' for c in model.constraints):
|
|
23
|
+
raise CompileError('P8 MINLP v1 does not combine indicator bridges with nonlinear orchestration')
|
|
24
|
+
offsets,n,vl,vu,domains,source_vars=_variable_layout(model)
|
|
25
|
+
dom_objs=tuple(VariableDomain(d) for d in domains)
|
|
26
|
+
integer=tuple(i for i,d in enumerate(dom_objs) if d in {VariableDomain.INTEGER,VariableDomain.BINARY})
|
|
27
|
+
binary=tuple(i for i,d in enumerate(dom_objs) if d is VariableDomain.BINARY)
|
|
28
|
+
if not integer: raise CompileError('compile_minlp_model requires at least one discrete variable')
|
|
29
|
+
if integer != binary:
|
|
30
|
+
raise CompileError('P8 OA v1 supports binary discrete variables only; general integer MINLP is fail-closed')
|
|
31
|
+
layout={vid:(offsets[vid][0],data.shape) for vid,data in model._variables.items()}
|
|
32
|
+
pvals={pid:data.value.copy() for pid,data in model._parameters.items()}
|
|
33
|
+
blocks=[]; source_constraints={}; ccurv=[]
|
|
34
|
+
for c in model.constraints:
|
|
35
|
+
if not isinstance(c.set,(LessThan,GreaterThan,EqualTo,Interval)):
|
|
36
|
+
raise CompileError(f'P8 supports scalar-set algebraic constraints only, got {type(c.set).__name__}')
|
|
37
|
+
size=int(np.prod(c.function.shape)) if c.function.shape else 1
|
|
38
|
+
if size != 1 and (c.function.polynomial_degree is None or c.function.polynomial_degree>1):
|
|
39
|
+
raise CompileError('P8 nonlinear constraints must be scalar in v1')
|
|
40
|
+
if isinstance(c.set,LessThan): lo=np.full(size,-np.inf); hi=_broadcast(c.set.upper,size)
|
|
41
|
+
elif isinstance(c.set,GreaterThan): lo=_broadcast(c.set.lower,size); hi=np.full(size,np.inf)
|
|
42
|
+
elif isinstance(c.set,EqualTo): lo=hi=_broadcast(c.set.value,size)
|
|
43
|
+
else: lo=_broadcast(c.set.lower,size); hi=_broadcast(c.set.upper,size)
|
|
44
|
+
proof=prove_curvature(c.function._node,pvals)
|
|
45
|
+
nonlinear=not (c.function.polynomial_degree is not None and c.function.polynomial_degree<=1)
|
|
46
|
+
if nonlinear:
|
|
47
|
+
if isinstance(c.set,LessThan) and proof.curvature not in {'convex','affine'}:
|
|
48
|
+
raise CompileError(f'P8 cannot certify convex upper-bounded constraint {c.entity_id.value}: {proof.reason}')
|
|
49
|
+
if isinstance(c.set,GreaterThan) and proof.curvature not in {'concave','affine'}:
|
|
50
|
+
raise CompileError(f'P8 cannot certify concave lower-bounded constraint {c.entity_id.value}: {proof.reason}')
|
|
51
|
+
if isinstance(c.set,(EqualTo,Interval)):
|
|
52
|
+
raise CompileError('P8 nonlinear equality/two-sided constraints are outside exact OA v1 scope')
|
|
53
|
+
ccurv.append(proof.curvature)
|
|
54
|
+
blocks.append(NLPConstraintBlock(c.entity_id.value,c.function._node,lo,hi,c.function.shape,tuple(sorted(c.function.parameter_dependencies))))
|
|
55
|
+
source_constraints[c.entity_id.value]={'semantic_kind':'minlp_function_in_set','nlp_rows':size,'shape':list(c.function.shape),'curvature':proof.curvature}
|
|
56
|
+
op=prove_curvature(model.objective.expression._node,pvals)
|
|
57
|
+
if op.curvature not in {'convex','affine'}:
|
|
58
|
+
raise CompileError(f'P8 exact OA requires certified convex objective: {op.reason}')
|
|
59
|
+
relax=NLPProblem(n,vl,vu,layout,pvals,model.objective.expression._node,tuple(blocks),model.objective.sense.value,model.name,{'semantic_hash':model.semantic_hash,'compiled_by':'solverpilot-p8'})
|
|
60
|
+
cert=ConvexityCertificate('solverpilot.minlp.convexity-certificate.v1',op.curvature,tuple(ccurv),'p8-affine-square-exp-v1',model.data_hash,('binary-discrete-only','nonlinear equalities rejected'))
|
|
61
|
+
problem=MINLPProblem(relax,tuple(d.value for d in dom_objs),integer,binary,cert,model.semantic_hash,model.data_hash,{'global_proof_scope':'certified-convex-binary-minlp'})
|
|
62
|
+
payload={'semantic_hash':model.semantic_hash,'data_hash':model.data_hash,'target':'minlp-ir-v1','policy':'p8-certified-convex-binary-oa-v1','convexity':cert.objective_curvature,'constraints':cert.constraint_curvatures}
|
|
63
|
+
ch=hashlib.sha256(json.dumps(payload,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
64
|
+
pmap={pid:{'shape':list(data.shape),'patchable':False,'parameter_compile_class':'p8_full_minlp_recertificate','plan_sensitive':True} for pid,data in model._parameters.items()}
|
|
65
|
+
report=CompilationReport(target='minlp-ir-v1',semantic_analysis_s=0.0,lowering_s=0.0,total_s=0.0,n_semantic_variables=len(model._variables),n_execution_variables=n,n_semantic_constraints=len(model._constraints),n_execution_constraints=relax.n_constraints,cache_status='disabled' if not use_cache else 'p8_rebuild',changed_parameters=(),reused_execution_rows=0,recompiled_execution_rows=relax.n_constraints,objective_recompiled=True,execution_mutations=(),n_transformations=0)
|
|
66
|
+
return CompiledModel(execution_ir=problem,semantic_hash=model.semantic_hash,data_hash=model.data_hash,compilation_hash=ch,parameter_map=pmap,source_map={'variables':source_vars,'constraints':source_constraints,'objective':{model.objective.entity_id.value:{'execution_objective':True,'curvature':op.curvature}}},transformation_tape=(),capability_signature='p8-certified-convex-binary-minlp-v1',mutation_plan={'mode':'p8_full_recertificate','backend_session_patch':False},plan_sensitive_dependencies=tuple(sorted(model._parameters)),precondition_certificate_hashes=(hashlib.sha256(json.dumps({'schema':cert.schema,'objective_curvature':cert.objective_curvature,'constraint_curvatures':cert.constraint_curvatures,'supported_atom_policy':cert.supported_atom_policy,'data_hash':cert.data_hash,'notes':cert.notes},sort_keys=True,default=list).encode()).hexdigest(),),compilation_report=report,reconstruction_contract={'primal':'identity_by_source_map','global_proof':'only_if_orchestrator_proved','original_space_validation':'solverpilot.nlp.validate_nlp_solution'},schema_version='solverpilot.compiled-model.p8.v1')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True, slots=True)
|
|
6
|
+
class MINLPConformanceCheck:
|
|
7
|
+
name: str
|
|
8
|
+
passed: bool
|
|
9
|
+
detail: str
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class MINLPConformanceReport:
|
|
13
|
+
schema: str
|
|
14
|
+
checks: tuple[MINLPConformanceCheck, ...]
|
|
15
|
+
passed: bool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def conform_minlp_orchestrator() -> MINLPConformanceReport:
|
|
19
|
+
import solverpilot as om
|
|
20
|
+
from .orchestrator import solve_binary_enumeration, solve_outer_approximation
|
|
21
|
+
checks=[]
|
|
22
|
+
m=om.Model('p8-conformance')
|
|
23
|
+
x=m.variable(lower=-2,upper=2); z=m.binary()
|
|
24
|
+
m.minimize((x-0.2)**2 + (z-0.4)**2)
|
|
25
|
+
p=m.compile().execution_ir
|
|
26
|
+
enum=solve_binary_enumeration(p); oa=solve_outer_approximation(p)
|
|
27
|
+
checks.append(MINLPConformanceCheck('enumeration_global_proof',bool(enum.globally_proven),str(enum.status)))
|
|
28
|
+
checks.append(MINLPConformanceCheck('oa_global_proof',bool(oa.globally_proven),str(oa.status)))
|
|
29
|
+
agree=bool(enum.objective is not None and oa.objective is not None and abs(enum.objective-oa.objective)<=2e-6)
|
|
30
|
+
checks.append(MINLPConformanceCheck('oa_matches_enumeration',agree,f'{enum.objective} vs {oa.objective}'))
|
|
31
|
+
bound=bool(oa.lower_bound is not None and oa.objective is not None and oa.lower_bound <= oa.objective + 2e-6)
|
|
32
|
+
checks.append(MINLPConformanceCheck('bound_consistency',bound,f'lb={oa.lower_bound}, ub={oa.objective}'))
|
|
33
|
+
checks.append(MINLPConformanceCheck('no_global_claim_from_nlp_status_alone',all('global_subproblem_by_convexity_certificate' not in i.nlp_status for i in oa.iterations), 'global proof comes from convexity+bound closure'))
|
|
34
|
+
return MINLPConformanceReport('solverpilot.minlp.conformance.v1',tuple(checks),all(c.passed for c in checks))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import numpy as np
|
|
4
|
+
from solverpilot.model.expression import ExprNode
|
|
5
|
+
from solverpilot.model.types import Curvature
|
|
6
|
+
|
|
7
|
+
_AFFINE_KINDS={'constant','parameter','variable','index','transpose','sum','concat','matmul'}
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class CurvatureProof:
|
|
11
|
+
curvature: str
|
|
12
|
+
reason: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _fixed_scalar(node: ExprNode, parameter_values: dict[str,np.ndarray]):
|
|
16
|
+
if node.variable_dependencies:
|
|
17
|
+
return None
|
|
18
|
+
k=node.kind
|
|
19
|
+
if k=='constant':
|
|
20
|
+
a=np.asarray(node.payload,dtype=float)
|
|
21
|
+
elif k=='parameter':
|
|
22
|
+
a=np.asarray(parameter_values[str(node.payload)],dtype=float)
|
|
23
|
+
elif k=='neg':
|
|
24
|
+
v=_fixed_scalar(node.args[0],parameter_values); return None if v is None else -v
|
|
25
|
+
elif k in {'add','mul','div'}:
|
|
26
|
+
a0=_fixed_scalar(node.args[0],parameter_values); a1=_fixed_scalar(node.args[1],parameter_values)
|
|
27
|
+
if a0 is None or a1 is None: return None
|
|
28
|
+
if k=='add': return a0+a1
|
|
29
|
+
if k=='mul': return a0*a1
|
|
30
|
+
if np.any(np.asarray(a1)==0): return None
|
|
31
|
+
return a0/a1
|
|
32
|
+
elif k=='pow':
|
|
33
|
+
a0=_fixed_scalar(node.args[0],parameter_values)
|
|
34
|
+
return None if a0 is None else np.power(a0,int(node.payload))
|
|
35
|
+
else:
|
|
36
|
+
return None
|
|
37
|
+
if np.asarray(a).size != 1: return None
|
|
38
|
+
return float(np.asarray(a).reshape(-1)[0])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _same_node(a: ExprNode,b: ExprNode) -> bool:
|
|
42
|
+
return a is b or a == b
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def prove_curvature(node: ExprNode, parameter_values: dict[str,np.ndarray]) -> CurvatureProof:
|
|
46
|
+
if node.degree is not None and node.degree <= 1:
|
|
47
|
+
return CurvatureProof('affine','polynomial degree <= 1')
|
|
48
|
+
k=node.kind
|
|
49
|
+
if k=='neg':
|
|
50
|
+
p=prove_curvature(node.args[0],parameter_values)
|
|
51
|
+
return CurvatureProof({'convex':'concave','concave':'convex'}.get(p.curvature,p.curvature),f'neg({p.reason})')
|
|
52
|
+
if k=='add':
|
|
53
|
+
a=prove_curvature(node.args[0],parameter_values); b=prove_curvature(node.args[1],parameter_values)
|
|
54
|
+
if a.curvature=='affine': return CurvatureProof(b.curvature,f'affine + {b.reason}')
|
|
55
|
+
if b.curvature=='affine': return CurvatureProof(a.curvature,f'{a.reason} + affine')
|
|
56
|
+
if a.curvature==b.curvature and a.curvature in {'convex','concave'}:
|
|
57
|
+
return CurvatureProof(a.curvature,f'sum of {a.curvature} terms')
|
|
58
|
+
return CurvatureProof('unknown','sum composition not certified')
|
|
59
|
+
if k=='mul':
|
|
60
|
+
a,b=node.args
|
|
61
|
+
ca=_fixed_scalar(a,parameter_values); cb=_fixed_scalar(b,parameter_values)
|
|
62
|
+
if ca is not None:
|
|
63
|
+
p=prove_curvature(b,parameter_values)
|
|
64
|
+
if ca==0: return CurvatureProof('affine','zero scaling')
|
|
65
|
+
if ca>0: return CurvatureProof(p.curvature,f'positive scaling {ca:g}')
|
|
66
|
+
return CurvatureProof({'convex':'concave','concave':'convex'}.get(p.curvature,p.curvature),f'negative scaling {ca:g}')
|
|
67
|
+
if cb is not None:
|
|
68
|
+
p=prove_curvature(a,parameter_values)
|
|
69
|
+
if cb==0: return CurvatureProof('affine','zero scaling')
|
|
70
|
+
if cb>0: return CurvatureProof(p.curvature,f'positive scaling {cb:g}')
|
|
71
|
+
return CurvatureProof({'convex':'concave','concave':'convex'}.get(p.curvature,p.curvature),f'negative scaling {cb:g}')
|
|
72
|
+
pa=prove_curvature(a,parameter_values)
|
|
73
|
+
if _same_node(a,b) and pa.curvature=='affine' and a.shape==():
|
|
74
|
+
return CurvatureProof('convex','square of scalar affine expression')
|
|
75
|
+
return CurvatureProof('unknown','bilinear/product term not certified')
|
|
76
|
+
if k=='exp':
|
|
77
|
+
p=prove_curvature(node.args[0],parameter_values)
|
|
78
|
+
if p.curvature=='affine': return CurvatureProof('convex','exp of affine')
|
|
79
|
+
return CurvatureProof('unknown','exp composition outside P8 certificate policy')
|
|
80
|
+
if k=='pow':
|
|
81
|
+
p=prove_curvature(node.args[0],parameter_values); e=int(node.payload)
|
|
82
|
+
if p.curvature=='affine' and e>=2 and e%2==0 and node.shape==():
|
|
83
|
+
return CurvatureProof('convex',f'even power {e} of scalar affine')
|
|
84
|
+
return CurvatureProof('unknown','power outside P8 certificate policy')
|
|
85
|
+
return CurvatureProof('unknown',f'atom {k!r} outside P8 convexity certificate policy')
|
solverpilot/minlp/ir.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import MappingProxyType
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
import numpy as np
|
|
6
|
+
from solverpilot.nlp.ir import NLPProblem
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class ConvexityCertificate:
|
|
10
|
+
schema: str
|
|
11
|
+
objective_curvature: str
|
|
12
|
+
constraint_curvatures: tuple[str, ...]
|
|
13
|
+
supported_atom_policy: str
|
|
14
|
+
data_hash: str
|
|
15
|
+
notes: tuple[str, ...] = ()
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class MINLPProblem:
|
|
19
|
+
relaxation: NLPProblem
|
|
20
|
+
domains: tuple[str, ...]
|
|
21
|
+
integer_indices: tuple[int, ...]
|
|
22
|
+
binary_indices: tuple[int, ...]
|
|
23
|
+
convexity_certificate: ConvexityCertificate
|
|
24
|
+
semantic_hash: str
|
|
25
|
+
data_hash: str
|
|
26
|
+
metadata: Mapping[str, Any] = MappingProxyType({})
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
if len(self.domains) != self.relaxation.n_variables:
|
|
30
|
+
raise ValueError('domain vector length mismatch')
|
|
31
|
+
object.__setattr__(self, 'metadata', MappingProxyType(dict(self.metadata)))
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def n_variables(self) -> int:
|
|
35
|
+
return self.relaxation.n_variables
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://solverpilot.dev/schema/minlp-ir-v1.json","title":"SolverPilot MINLP IR v1","type":"object","required":["schema","semantic_hash","data_hash","domains","binary_indices","convexity_certificate"],"properties":{"schema":{"const":"solverpilot.minlp-ir.v1"},"semantic_hash":{"type":"string"},"data_hash":{"type":"string"},"domains":{"type":"array","items":{"type":"string"}},"binary_indices":{"type":"array","items":{"type":"integer","minimum":0}},"convexity_certificate":{"type":"object"}}}
|