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,220 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+ import itertools
5
+ import numpy as np
6
+ from scipy import sparse
7
+ from solverpilot.backends import ScipyHighsBackend
8
+ from solverpilot.runtime.executor import execute
9
+ from solverpilot.problem import LinearProblem, ObjectiveSense, VariableDomain
10
+ from solverpilot.nlp import CasadiIpoptBackend, NLPDerivativeEngine, validate_nlp_solution
11
+ from solverpilot.nlp.ir import NLPProblem
12
+ from solverpilot.validate import PublicStatus
13
+ from .ir import MINLPProblem
14
+ from .validation import validate_minlp_solution
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class MINLPIteration:
18
+ iteration:int
19
+ binary_assignment:tuple[int,...]
20
+ master_lower_bound:float|None
21
+ nlp_objective:float|None
22
+ nlp_status:str
23
+ cuts_added:int
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class MINLPSolveResult:
27
+ status:str
28
+ x:np.ndarray|None
29
+ objective:float|None
30
+ lower_bound:float|None
31
+ gap:float|None
32
+ globally_proven:bool
33
+ validation_valid:bool
34
+ algorithm:str
35
+ iterations:tuple[MINLPIteration,...]
36
+ raw_statistics:dict[str,Any]
37
+ proof_scope:str='none'
38
+ independently_verified_global:bool=False
39
+
40
+
41
+
42
+ def _bound_closure(ub: float, lb: float, atol: float, rtol: float | None = None):
43
+ rtol=float(atol if rtol is None else rtol)
44
+ tol=float(atol)+rtol*max(abs(float(ub)),abs(float(lb)))
45
+ primal_dual_gap=max(0.0,float(ub)-float(lb))
46
+ bound_inversion=max(0.0,float(lb)-float(ub))
47
+ return (max(primal_dual_gap,bound_inversion) <= tol, primal_dual_gap, bound_inversion, tol)
48
+
49
+ def _fixed_problem(base:NLPProblem, indices, values):
50
+ lo=base.variable_lower.copy(); hi=base.variable_upper.copy()
51
+ for i,v in zip(indices,values): lo[i]=hi[i]=float(v)
52
+ return NLPProblem(base.n_variables,lo,hi,base.variable_layout,base.parameter_values,base.objective_node,base.constraints,base.objective_sense,base.name,{**dict(base.metadata),'fixed_binary_assignment':tuple(map(int,values))})
53
+
54
+
55
+ def solve_binary_enumeration(problem:MINLPProblem, *, nlp_backend=None, atol=1e-7, max_assignments=1<<18):
56
+ if tuple(problem.integer_indices)!=tuple(problem.binary_indices): raise ValueError('binary enumeration supports binary discrete variables only')
57
+ k=len(problem.binary_indices)
58
+ if 2**k > max_assignments: raise ValueError('binary assignment count exceeds max_assignments')
59
+ backend=nlp_backend or CasadiIpoptBackend()
60
+ best=None; best_obj=np.inf; it=[]; unresolved=0
61
+ for j,bits in enumerate(itertools.product((0,1), repeat=k),1):
62
+ sub=_fixed_problem(problem.relaxation,problem.binary_indices,bits)
63
+ r=backend.solve(sub)
64
+ if r.local_optimal_candidate and r.validation.valid and r.x is not None:
65
+ obj=float(r.validation.objective_recomputed)
66
+ if obj < best_obj-atol: best_obj=obj; best=r.x.copy()
67
+ st='global_subproblem_by_convexity_certificate'
68
+ else:
69
+ # Without an NLP infeasibility certificate a failed local solve cannot
70
+ # safely eliminate an assignment from a global proof.
71
+ unresolved += 1; st='unresolved'
72
+ it.append(MINLPIteration(j,tuple(map(int,bits)),None,None if r.x is None else r.validation.objective_recomputed,r.backend_status,0))
73
+ valid=False
74
+ if best is not None: valid=validate_minlp_solution(problem,best,objective_reported=best_obj,atol=atol,rtol=atol).valid
75
+ proven=best is not None and unresolved==0 and valid
76
+ return MINLPSolveResult('proven_optimal' if proven else ('valid_feasible' if best is not None and valid else 'unknown'),best,None if best is None else best_obj,None,0.0 if proven else None,proven,valid,'binary_enumeration_convex_subproblems',tuple(it),{'assignments':2**k,'unresolved_assignments':unresolved,'convexity_certificate':problem.convexity_certificate.schema},'solver_certified_under_convexity_assumptions' if proven else 'none',False)
77
+
78
+
79
+ def _build_master(problem:MINLPProblem, cuts, eta_lower, reference_x):
80
+ base=problem.relaxation; n=problem.n_variables
81
+ eng=NLPDerivativeEngine(base); f,g,grad,jac=eng.evaluate(reference_x)
82
+ has_nl_obj=problem.convexity_certificate.objective_curvature != 'affine'
83
+ nmaster=n+1 if has_nl_obj else n
84
+ rows=[]; cl=[]; cu=[]
85
+ row_cursor=0
86
+ for curvature,block in zip(problem.convexity_certificate.constraint_curvatures,base.constraints):
87
+ size=block.lower.size
88
+ if curvature=='affine':
89
+ for local in range(size):
90
+ a=jac[row_cursor+local].copy(); val=float(g[row_cursor+local]); intercept=val-float(a@reference_x)
91
+ rows.append({i:float(v) for i,v in enumerate(a) if v!=0})
92
+ lo=float(block.lower[local]); hi=float(block.upper[local])
93
+ cl.append(lo-intercept if np.isfinite(lo) else -np.inf)
94
+ cu.append(hi-intercept if np.isfinite(hi) else np.inf)
95
+ row_cursor += size
96
+ for row,lo,hi in cuts: rows.append(dict(row)); cl.append(float(lo)); cu.append(float(hi))
97
+ data=[]; rr=[]; cc=[]
98
+ for r,row in enumerate(rows):
99
+ for j,v in row.items():
100
+ if v!=0: rr.append(r); cc.append(j); data.append(float(v))
101
+ A=sparse.csr_matrix((data,(rr,cc)),shape=(len(rows),nmaster))
102
+ cvec=np.zeros(nmaster)
103
+ if has_nl_obj:
104
+ cvec[n]=1.0; vlm=np.r_[base.variable_lower,float(eta_lower)]; vum=np.r_[base.variable_upper,np.inf]; domm=tuple(problem.domains)+('continuous',); offset=0.0
105
+ else:
106
+ cvec[:n]=grad; offset=float(f-grad@reference_x); vlm=base.variable_lower; vum=base.variable_upper; domm=tuple(problem.domains)
107
+ return LinearProblem(A=A,c=cvec,variable_lower=vlm,variable_upper=vum,constraint_lower=np.asarray(cl),constraint_upper=np.asarray(cu),domains=domm,objective_sense=ObjectiveSense.MINIMIZE,objective_offset=offset,name='p8-oa-master'),has_nl_obj
108
+
109
+
110
+ def _oa_cuts_at(problem:MINLPProblem, x, *, include_objective=True):
111
+ eng=NLPDerivativeEngine(problem.relaxation); f,g,grad,jac=eng.evaluate(x)
112
+ cuts=[]; n=problem.n_variables; row_cursor=0
113
+ for curvature,block in zip(problem.convexity_certificate.constraint_curvatures,problem.relaxation.constraints):
114
+ size=block.lower.size
115
+ if curvature=='affine': row_cursor += size; continue
116
+ if size!=1: raise ValueError('P8 nonlinear OA cuts require scalar constraints')
117
+ a=jac[row_cursor].copy(); gx=float(g[row_cursor]); intercept=gx-float(a@x)
118
+ lo=float(block.lower[0]); hi=float(block.upper[0])
119
+ if np.isfinite(hi) and not np.isfinite(lo): cuts.append(({i:float(v) for i,v in enumerate(a) if v!=0},-np.inf,hi-intercept))
120
+ elif np.isfinite(lo) and not np.isfinite(hi): cuts.append(({i:float(v) for i,v in enumerate(a) if v!=0},lo-intercept,np.inf))
121
+ else: raise ValueError('nonlinear equality/two-sided cut unsupported')
122
+ row_cursor += size
123
+ if include_objective and problem.convexity_certificate.objective_curvature != 'affine':
124
+ row={i:float(-v) for i,v in enumerate(grad) if v!=0}; row[n]=1.0
125
+ rhs=float(f-grad@x); cuts.append((row,rhs,np.inf))
126
+ return cuts
127
+
128
+
129
+ def _binary_nogood_cut(problem:MINLPProblem, bits):
130
+ row={}; ones=0
131
+ for idx,bit in zip(problem.binary_indices,bits):
132
+ if int(bit)==1:
133
+ row[int(idx)]=-1.0; ones+=1
134
+ else:
135
+ row[int(idx)]=1.0
136
+ return (row,1.0-float(ones),np.inf)
137
+
138
+
139
+ def solve_outer_approximation(problem:MINLPProblem, *, nlp_backend=None, milp_backend=None, max_iter=None, atol=1e-7):
140
+ backend=nlp_backend or CasadiIpoptBackend(); mip=milp_backend or ScipyHighsBackend()
141
+ iteration_budget = min((1 << len(problem.binary_indices)) + 5, 10000) if max_iter is None else int(max_iter)
142
+ if iteration_budget <= 0: raise ValueError("max_iter must be positive")
143
+ # Convex continuous relaxation supplies the initial global lower bound and first cuts.
144
+ relax=backend.solve(problem.relaxation)
145
+ if not (relax.local_optimal_candidate and relax.validation.valid and relax.x is not None):
146
+ return MINLPSolveResult('unknown',None,None,None,None,False,False,'outer_approximation',(),{'reason':'continuous relaxation unresolved'})
147
+ relax_lb=float(relax.validation.objective_recomputed)
148
+ relax_minlp=validate_minlp_solution(problem,relax.x,objective_reported=relax_lb,atol=atol,rtol=atol)
149
+ if relax_minlp.valid:
150
+ return MINLPSolveResult('proven_optimal',relax.x.copy(),relax_lb,relax_lb,0.0,True,True,'outer_approximation',(),{'continuous_relaxation_lb':relax_lb,'integral_relaxation':True,'cuts':0,'convexity_certificate':problem.convexity_certificate.schema},'solver_certified_under_convexity_assumptions',False)
151
+ # Ipopt may return an almost-integral relaxation point (e.g. 0.9999998).
152
+ # Rounding alone is never a proof; fix the rounded assignment and re-solve.
153
+ dist=max((min(abs(float(relax.x[i])),abs(float(relax.x[i])-1.0)) for i in problem.binary_indices),default=0.0)
154
+ if dist <= max(1e-5,100*atol):
155
+ bits=tuple(int(round(float(relax.x[i]))) for i in problem.binary_indices)
156
+ sub=_fixed_problem(problem.relaxation,problem.binary_indices,bits)
157
+ near=backend.solve(sub,x0=relax.x)
158
+ if near.local_optimal_candidate and near.validation.valid and near.x is not None:
159
+ ub0=float(near.validation.objective_recomputed)
160
+ v0=validate_minlp_solution(problem,near.x,objective_reported=ub0,atol=atol,rtol=atol)
161
+ if v0.valid and ub0-relax_lb <= atol*(1+abs(ub0)):
162
+ return MINLPSolveResult('proven_optimal',near.x.copy(),ub0,relax_lb,max(0.0,ub0-relax_lb),True,True,'outer_approximation',(),{'continuous_relaxation_lb':relax_lb,'near_integral_relaxation_certified':True,'integrality_distance':dist,'cuts':0,'convexity_certificate':problem.convexity_certificate.schema},'solver_certified_under_convexity_assumptions',False)
163
+ cuts=_oa_cuts_at(problem,relax.x)
164
+ incumbent=None; ub=np.inf; lb=relax_lb; hist=[]; seen=set(); unresolved=False; evaluated_values=[]
165
+ for itn in range(1,iteration_budget+1):
166
+ master,has_eta=_build_master(problem,cuts,relax_lb,relax.x)
167
+ mr=execute(master,mip)
168
+ if mr.status is PublicStatus.INFEASIBLE:
169
+ if incumbent is not None and evaluated_values:
170
+ lb=ub
171
+ valid=validate_minlp_solution(problem,incumbent,objective_reported=ub,atol=atol,rtol=atol).valid
172
+ return MINLPSolveResult('proven_optimal' if valid else 'unknown',incumbent,ub,lb,0.0,valid,valid,'outer_approximation',tuple(hist),{'continuous_relaxation_lb':relax_lb,'cuts':len(cuts),'master_exhausted':True,'master_infeasibility_backend_reported':True,'master_infeasibility_certificate_verified':False,'evaluated_assignments':len(evaluated_values),'convexity_certificate':problem.convexity_certificate.schema},'solver_certified_under_convexity_assumptions' if valid else 'none',False)
173
+ unresolved=True; break
174
+ if mr.status is not PublicStatus.VALID_OPTIMAL or mr.x is None or mr.objective is None:
175
+ unresolved=True; break
176
+ remaining_lb=max(relax_lb,float(mr.objective))
177
+ lb=min(remaining_lb,min(evaluated_values)) if evaluated_values else remaining_lb
178
+ xmaster=np.asarray(mr.x[:problem.n_variables],float)
179
+ bits=tuple(int(round(xmaster[i])) for i in problem.binary_indices)
180
+ sub=_fixed_problem(problem.relaxation,problem.binary_indices,bits)
181
+ nr=backend.solve(sub,x0=xmaster)
182
+ added=0
183
+ if nr.local_optimal_candidate and nr.validation.valid and nr.x is not None:
184
+ obj=float(nr.validation.objective_recomputed)
185
+ if obj < ub-atol: ub=obj; incumbent=nr.x.copy()
186
+ evaluated_values.append(obj)
187
+ newcuts=_oa_cuts_at(problem,nr.x)
188
+ newcuts.append(_binary_nogood_cut(problem,bits))
189
+ cuts.extend(newcuts); added=len(newcuts)
190
+ else:
191
+ # No infeasibility certificate: do not add a no-good cut and do not claim a proof.
192
+ unresolved=True
193
+ hist.append(MINLPIteration(itn,bits,lb,None if nr.x is None else nr.validation.objective_recomputed,nr.backend_status,added))
194
+ if unresolved: break
195
+ if np.isfinite(ub):
196
+ closed,gap,inversion,proof_tol=_bound_closure(ub,lb,atol)
197
+ else:
198
+ closed,gap,inversion,proof_tol=False,np.inf,0.0,np.inf
199
+ if np.isfinite(ub) and closed:
200
+ valid=validate_minlp_solution(problem,incumbent,objective_reported=ub,atol=atol,rtol=atol).valid
201
+ return MINLPSolveResult('proven_optimal' if valid else 'unknown',incumbent,ub,lb,gap,valid,valid,'outer_approximation',tuple(hist),{'continuous_relaxation_lb':relax_lb,'cuts':len(cuts),'evaluated_assignments':len(evaluated_values),'bound_inversion':inversion,'proof_tolerance':proof_tol,'convexity_certificate':problem.convexity_certificate.schema},'solver_certified_under_convexity_assumptions' if valid else 'none',False)
202
+ key=(bits,round(lb,10),round(ub,10) if np.isfinite(ub) else None)
203
+ if key in seen:
204
+ unresolved=True; break
205
+ seen.add(key)
206
+ valid=False
207
+ if incumbent is not None: valid=validate_minlp_solution(problem,incumbent,objective_reported=ub,atol=atol,rtol=atol).valid
208
+ if not np.isfinite(ub): gap=None; inversion=0.0; proof_tol=None
209
+ else:
210
+ _,gap,inversion,proof_tol=_bound_closure(ub,lb,atol)
211
+ return MINLPSolveResult('valid_feasible' if valid else 'unknown',incumbent,None if incumbent is None else ub,lb,gap,False,valid,'outer_approximation',tuple(hist),{'continuous_relaxation_lb':relax_lb,'cuts':len(cuts),'evaluated_assignments':len(evaluated_values),'unresolved':unresolved,'iteration_budget':iteration_budget,'bound_inversion':inversion,'proof_tolerance':proof_tol,'convexity_certificate':problem.convexity_certificate.schema})
212
+
213
+
214
+ def solve_minlp(problem:MINLPProblem, *, algorithm="oa", **kwargs):
215
+ key=str(algorithm).lower().replace("-","_")
216
+ if key in {"oa","outer_approximation"}:
217
+ return solve_outer_approximation(problem, **kwargs)
218
+ if key in {"enumeration","binary_enumeration"}:
219
+ return solve_binary_enumeration(problem, **kwargs)
220
+ raise ValueError(f"unsupported P8 MINLP algorithm {algorithm!r}")
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ import numpy as np
4
+ from solverpilot.nlp.validation import NLPValidationReport, validate_nlp_solution
5
+ from .ir import MINLPProblem
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class MINLPValidationReport:
9
+ valid: bool
10
+ nlp: NLPValidationReport
11
+ max_integrality_violation: float
12
+ binary_violations: tuple[int, ...]
13
+
14
+
15
+ def validate_minlp_solution(problem:MINLPProblem, x, *, objective_reported=None, atol=1e-7, rtol=1e-7):
16
+ nlp=validate_nlp_solution(problem.relaxation,x,objective_reported=objective_reported,atol=atol,rtol=rtol)
17
+ if x is None:
18
+ return MINLPValidationReport(False,nlp,float('inf'),())
19
+ a=np.asarray(x,dtype=float).reshape(-1)
20
+ bad=[]; maxv=0.0
21
+ for i in problem.binary_indices:
22
+ v=float(a[i]); d=min(abs(v),abs(v-1.0)); maxv=max(maxv,d)
23
+ if d>atol: bad.append(int(i))
24
+ for i in problem.integer_indices:
25
+ if i in problem.binary_indices: continue
26
+ v=float(a[i]); d=abs(v-round(v)); maxv=max(maxv,d)
27
+ if d>atol: bad.append(int(i))
28
+ return MINLPValidationReport(bool(nlp.valid and not bad),nlp,float(maxv),tuple(sorted(set(bad))))
@@ -0,0 +1,27 @@
1
+ from .compiler import CompiledModel, CompilationReport, CompilerCacheInfo, compile_model, compiler_cache_info
2
+ from .errors import CompileError, DomainError, ModelingError, OwnershipError, ShapeError, SymbolicTruthValueError
3
+ from .expression import Expression
4
+ from .model import Constraint, IndicatorConstraint, Model, Objective, Parameter, PendingConstraint, Variable
5
+ from .sets import EqualTo, GreaterThan, Interval, LessThan, PositiveSemidefiniteCone, RotatedSecondOrderCone, SecondOrderCone
6
+ from .types import Curvature, EntityId, NumericType, SignDomain
7
+
8
+
9
+ def sum(expression, axis=None):
10
+ return expression.sum(axis=axis)
11
+
12
+
13
+ __all__ = [
14
+ "CompileError", "CompiledModel", "CompilationReport", "CompilerCacheInfo", "Constraint", "Curvature",
15
+ "DomainError", "EntityId", "EqualTo", "Expression", "GreaterThan", "IndicatorConstraint", "Interval",
16
+ "LessThan", "PositiveSemidefiniteCone", "RotatedSecondOrderCone", "SecondOrderCone", "Model", "ModelingError", "NumericType", "Objective", "OwnershipError",
17
+ "Parameter", "PendingConstraint", "ShapeError", "SignDomain", "SymbolicTruthValueError",
18
+ "Variable", "compile_model", "compiler_cache_info", "sum", "sin", "cos", "exp", "log", "sqrt", "tanh",
19
+ ]
20
+
21
+
22
+ def sin(x): return x.sin()
23
+ def cos(x): return x.cos()
24
+ def exp(x): return x.exp()
25
+ def log(x): return x.log()
26
+ def sqrt(x): return x.sqrt()
27
+ def tanh(x): return x.tanh()