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.
Files changed (201) hide show
  1. solverpilot/__init__.py +250 -0
  2. solverpilot/_immutability.py +27 -0
  3. solverpilot/_version.py +3 -0
  4. solverpilot/applications/__init__.py +3 -0
  5. solverpilot/applications/routing/__init__.py +19 -0
  6. solverpilot/applications/routing/compile_milp.py +321 -0
  7. solverpilot/applications/routing/diagnostics.py +78 -0
  8. solverpilot/applications/routing/errors.py +14 -0
  9. solverpilot/applications/routing/heuristics.py +167 -0
  10. solverpilot/applications/routing/model.py +259 -0
  11. solverpilot/applications/routing/reference.py +81 -0
  12. solverpilot/applications/routing/solution.py +89 -0
  13. solverpilot/applications/routing/timeline.py +79 -0
  14. solverpilot/applications/routing/validation.py +169 -0
  15. solverpilot/applications/tsp/__init__.py +41 -0
  16. solverpilot/applications/tsp/compile_cp.py +121 -0
  17. solverpilot/applications/tsp/compile_milp.py +139 -0
  18. solverpilot/applications/tsp/distance.py +60 -0
  19. solverpilot/applications/tsp/errors.py +17 -0
  20. solverpilot/applications/tsp/heuristics.py +112 -0
  21. solverpilot/applications/tsp/model.py +175 -0
  22. solverpilot/applications/tsp/reference.py +121 -0
  23. solverpilot/applications/tsp/solution.py +65 -0
  24. solverpilot/applications/tsp/validation.py +92 -0
  25. solverpilot/backends/__init__.py +51 -0
  26. solverpilot/backends/_casadi_highs_worker.py +92 -0
  27. solverpilot/backends/base.py +28 -0
  28. solverpilot/backends/bundled_capi.py +910 -0
  29. solverpilot/backends/casadi_conic.py +303 -0
  30. solverpilot/backends/health.py +240 -0
  31. solverpilot/backends/highspy_native.py +217 -0
  32. solverpilot/backends/nlopt_native.py +237 -0
  33. solverpilot/backends/osqp_native.py +297 -0
  34. solverpilot/backends/protocol_v2.py +119 -0
  35. solverpilot/backends/pyscipopt_native.py +200 -0
  36. solverpilot/backends/registry.py +38 -0
  37. solverpilot/backends/scipy_highs.py +138 -0
  38. solverpilot/backends/scipy_highs_lp.py +195 -0
  39. solverpilot/backends/scipy_slsqp_qp.py +133 -0
  40. solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
  41. solverpilot/benchmark/__init__.py +23 -0
  42. solverpilot/benchmark/acquire.py +199 -0
  43. solverpilot/benchmark/baselines.py +70 -0
  44. solverpilot/benchmark/bundle.py +37 -0
  45. solverpilot/benchmark/campaign.py +150 -0
  46. solverpilot/benchmark/environment.py +115 -0
  47. solverpilot/benchmark/integrity.py +46 -0
  48. solverpilot/benchmark/model.py +146 -0
  49. solverpilot/benchmark/policy.py +182 -0
  50. solverpilot/benchmark/registry.py +38 -0
  51. solverpilot/benchmark/report.py +44 -0
  52. solverpilot/benchmark/runner.py +516 -0
  53. solverpilot/benchmark/slurm.py +55 -0
  54. solverpilot/benchmark/specs.py +75 -0
  55. solverpilot/benchmark/splits.py +181 -0
  56. solverpilot/benchmark/summary.py +384 -0
  57. solverpilot/benchmark/verify.py +142 -0
  58. solverpilot/benchmark/worker.py +71 -0
  59. solverpilot/bridges/__init__.py +19 -0
  60. solverpilot/bridges/engine.py +44 -0
  61. solverpilot/bridges/indicator.py +232 -0
  62. solverpilot/bridges/registry.py +78 -0
  63. solverpilot/bridges/transformation-tape.schema.json +53 -0
  64. solverpilot/bridges/types.py +149 -0
  65. solverpilot/bridges/validation.py +110 -0
  66. solverpilot/capabilities/__init__.py +32 -0
  67. solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
  68. solverpilot/capabilities/conformance.py +151 -0
  69. solverpilot/capabilities/enums.py +27 -0
  70. solverpilot/capabilities/manifest.py +19 -0
  71. solverpilot/capabilities/ontology.py +73 -0
  72. solverpilot/capabilities/requirements.py +37 -0
  73. solverpilot/capabilities/resolve.py +30 -0
  74. solverpilot/capabilities/v2.py +711 -0
  75. solverpilot/cli/__init__.py +1 -0
  76. solverpilot/cli/backend_health.py +77 -0
  77. solverpilot/cli/benchmark.py +331 -0
  78. solverpilot/cli/benchmark_worker.py +33 -0
  79. solverpilot/cli/capabilities.py +65 -0
  80. solverpilot/conic/__init__.py +11 -0
  81. solverpilot/conic/backend.py +263 -0
  82. solverpilot/conic/compiler.py +310 -0
  83. solverpilot/conic/conformance.py +60 -0
  84. solverpilot/conic/conic-ir-v1.schema.json +24 -0
  85. solverpilot/conic/ir.py +299 -0
  86. solverpilot/conic/validation.py +149 -0
  87. solverpilot/cp/__init__.py +7 -0
  88. solverpilot/cp/conformance.py +32 -0
  89. solverpilot/cp/cp-ir-v1.schema.json +13 -0
  90. solverpilot/cp/ir.py +170 -0
  91. solverpilot/cp/model.py +143 -0
  92. solverpilot/cp/ortools_backend.py +423 -0
  93. solverpilot/cp/ortools_worker.py +75 -0
  94. solverpilot/cp/reference.py +51 -0
  95. solverpilot/cp/serialization.py +117 -0
  96. solverpilot/cp/validate.py +83 -0
  97. solverpilot/diagnose/__init__.py +33 -0
  98. solverpilot/diagnose/infeasibility.py +463 -0
  99. solverpilot/diagnose/model.py +87 -0
  100. solverpilot/evaluation/__init__.py +53 -0
  101. solverpilot/evaluation/metrics.py +150 -0
  102. solverpilot/evaluation/oracle.py +288 -0
  103. solverpilot/evaluation/proof.py +53 -0
  104. solverpilot/evaluation/references.py +73 -0
  105. solverpilot/evaluation/regret.py +281 -0
  106. solverpilot/exceptions.py +34 -0
  107. solverpilot/experimental/__init__.py +32 -0
  108. solverpilot/extensions/__init__.py +56 -0
  109. solverpilot/extensions/entrypoints.py +34 -0
  110. solverpilot/extensions/errors.py +35 -0
  111. solverpilot/extensions/manager.py +651 -0
  112. solverpilot/extensions/manifest.py +232 -0
  113. solverpilot/extensions/model.py +101 -0
  114. solverpilot/extensions/versioning.py +136 -0
  115. solverpilot/history/__init__.py +22 -0
  116. solverpilot/history/errors.py +15 -0
  117. solverpilot/history/model.py +129 -0
  118. solverpilot/history/sqlite.py +651 -0
  119. solverpilot/inspect/__init__.py +3 -0
  120. solverpilot/inspect/fingerprint.py +199 -0
  121. solverpilot/intelligence/__init__.py +36 -0
  122. solverpilot/intelligence/errors.py +16 -0
  123. solverpilot/intelligence/fingerprint.py +108 -0
  124. solverpilot/intelligence/leakage.py +69 -0
  125. solverpilot/intelligence/schema.py +314 -0
  126. solverpilot/intelligence/shift.py +208 -0
  127. solverpilot/io/__init__.py +41 -0
  128. solverpilot/io/binary64.py +49 -0
  129. solverpilot/io/csv.py +230 -0
  130. solverpilot/io/errors.py +25 -0
  131. solverpilot/io/hashing.py +111 -0
  132. solverpilot/io/json.py +90 -0
  133. solverpilot/io/mapping.py +195 -0
  134. solverpilot/io/model.py +407 -0
  135. solverpilot/io/numeric.py +108 -0
  136. solverpilot/io/provenance.py +114 -0
  137. solverpilot/io/result.py +45 -0
  138. solverpilot/io/source.py +104 -0
  139. solverpilot/io/strict_json.py +133 -0
  140. solverpilot/minlp/__init__.py +6 -0
  141. solverpilot/minlp/compiler.py +66 -0
  142. solverpilot/minlp/conformance.py +34 -0
  143. solverpilot/minlp/convexity.py +85 -0
  144. solverpilot/minlp/ir.py +35 -0
  145. solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
  146. solverpilot/minlp/orchestrator.py +220 -0
  147. solverpilot/minlp/validation.py +28 -0
  148. solverpilot/model/__init__.py +27 -0
  149. solverpilot/model/compiler.py +1263 -0
  150. solverpilot/model/errors.py +22 -0
  151. solverpilot/model/expression.py +373 -0
  152. solverpilot/model/model.py +716 -0
  153. solverpilot/model/sets.py +60 -0
  154. solverpilot/model/types.py +36 -0
  155. solverpilot/nlp/__init__.py +7 -0
  156. solverpilot/nlp/ad.py +166 -0
  157. solverpilot/nlp/backend.py +113 -0
  158. solverpilot/nlp/compiler.py +116 -0
  159. solverpilot/nlp/conformance.py +58 -0
  160. solverpilot/nlp/ir.py +70 -0
  161. solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
  162. solverpilot/nlp/validation.py +60 -0
  163. solverpilot/plan/__init__.py +33 -0
  164. solverpilot/plan/model.py +79 -0
  165. solverpilot/plan/planner.py +201 -0
  166. solverpilot/plan/production.py +627 -0
  167. solverpilot/plan/selective_lp.py +65 -0
  168. solverpilot/problem/__init__.py +16 -0
  169. solverpilot/problem/enums.py +20 -0
  170. solverpilot/problem/hashing.py +88 -0
  171. solverpilot/problem/linear.py +196 -0
  172. solverpilot/problem/mps.py +447 -0
  173. solverpilot/problem/quadratic.py +192 -0
  174. solverpilot/reporting/__init__.py +27 -0
  175. solverpilot/reporting/explain.py +327 -0
  176. solverpilot/reporting/model.py +264 -0
  177. solverpilot/reporting/render.py +55 -0
  178. solverpilot/runtime/__init__.py +19 -0
  179. solverpilot/runtime/auto.py +193 -0
  180. solverpilot/runtime/budgeting.py +43 -0
  181. solverpilot/runtime/executor.py +126 -0
  182. solverpilot/runtime/portfolio.py +197 -0
  183. solverpilot/runtime/result.py +72 -0
  184. solverpilot/session/__init__.py +29 -0
  185. solverpilot/session/conformance_v2.py +239 -0
  186. solverpilot/session/mutations.py +115 -0
  187. solverpilot/session/persistent-session-trace.schema.json +45 -0
  188. solverpilot/session/persistent.py +491 -0
  189. solverpilot/session/reuse.py +81 -0
  190. solverpilot/session/session.py +236 -0
  191. solverpilot/trace/__init__.py +3 -0
  192. solverpilot/trace/schema.py +47 -0
  193. solverpilot/validate/__init__.py +10 -0
  194. solverpilot/validate/core.py +179 -0
  195. solverpilot/validate/result.py +55 -0
  196. solverpilot-0.1.dist-info/METADATA +554 -0
  197. solverpilot-0.1.dist-info/RECORD +201 -0
  198. solverpilot-0.1.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
  201. solverpilot-0.1.dist-info/top_level.txt +1 -0
@@ -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')
@@ -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"}}}