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
|
@@ -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()
|