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,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import math
|
|
4
|
+
from typing import Mapping, Any
|
|
5
|
+
from solverpilot.inspect import ProblemFingerprint
|
|
6
|
+
|
|
7
|
+
FEATURE_NAMES=('log1p_n','log1p_m','log1p_nnz','log_density','log1p_m_over_n')
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class SelectiveLPDecision:
|
|
11
|
+
backend: str
|
|
12
|
+
default_backend: str
|
|
13
|
+
alternate_backend: str
|
|
14
|
+
switched: bool
|
|
15
|
+
leaf: int
|
|
16
|
+
predicted_saving_s: float
|
|
17
|
+
lower_saving_s: float
|
|
18
|
+
support_passed: bool
|
|
19
|
+
reason: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _features(fp: ProblemFingerprint) -> tuple[float,...]:
|
|
23
|
+
if fp.problem_class != 'lp':
|
|
24
|
+
raise ValueError(f'M27 LP selector only supports continuous LP fingerprints, got {fp.problem_class!r}')
|
|
25
|
+
n=max(int(fp.n_variables),0); m=max(int(fp.n_constraints),0); nnz=max(int(fp.nnz_a),0)
|
|
26
|
+
return (
|
|
27
|
+
math.log1p(n), math.log1p(m), math.log1p(nnz),
|
|
28
|
+
math.log(max(float(fp.density_a),1e-12)), math.log1p(m/max(n,1)),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _traverse(nodes: list[Mapping[str,Any]], x: tuple[float,...]) -> int:
|
|
33
|
+
by_id={int(n['node']):n for n in nodes}; node=0
|
|
34
|
+
while True:
|
|
35
|
+
row=by_id[node]
|
|
36
|
+
if bool(row['is_leaf']): return node
|
|
37
|
+
f=int(row['feature']); thr=float(row['threshold'])
|
|
38
|
+
node=int(row['left'] if x[f] <= thr else row['right'])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decide_selective_lp_backend(fingerprint: ProblemFingerprint, model: Mapping[str,Any]) -> SelectiveLPDecision:
|
|
42
|
+
if str(model.get('schema')) != 'solverpilot.m27.selective_absolute_saving_tree.v1':
|
|
43
|
+
raise ValueError('unsupported M27 selector schema')
|
|
44
|
+
if bool(model.get('test_outcomes_seen_during_fit')):
|
|
45
|
+
raise ValueError('selector artifact is contaminated: test outcomes seen during fit')
|
|
46
|
+
if not bool(model.get('development_gate_passed')):
|
|
47
|
+
raise ValueError('selector development gate did not pass')
|
|
48
|
+
x=_features(fingerprint); nodes=list(model['tree_nodes']); leaf=_traverse(nodes,x)
|
|
49
|
+
stats=model['leaf_stats'][str(leaf)]; rule=model['selective_rule']; margin=float(rule['switch_margin_s'])
|
|
50
|
+
mean=float(stats['mean_saving_s']); lower=float(stats['bootstrap_one_sided_90_lower_mean_s']); samples=int(stats['samples'])
|
|
51
|
+
support=True
|
|
52
|
+
for key,bounds in stats.get('support_used_features',{}).items():
|
|
53
|
+
f=int(key); v=x[f]
|
|
54
|
+
if v < float(bounds['min'])-1e-12 or v > float(bounds['max'])+1e-12:
|
|
55
|
+
support=False; break
|
|
56
|
+
switch=bool(mean>margin and lower>margin and samples>=int(rule['require_leaf_samples_at_least']) and support)
|
|
57
|
+
default=str(model['default_backend']); alternate=str(model['alternate_backend']); backend=alternate if switch else default
|
|
58
|
+
reasons=[]
|
|
59
|
+
if switch: reasons.append('alternate selected: leaf mean and one-sided empirical lower saving exceed margin within observed leaf support')
|
|
60
|
+
else:
|
|
61
|
+
if mean<=margin: reasons.append('fallback: predicted leaf mean saving does not exceed margin')
|
|
62
|
+
if lower<=margin: reasons.append('fallback: empirical lower saving does not exceed margin')
|
|
63
|
+
if not support: reasons.append('fallback: query extrapolates outside observed leaf support')
|
|
64
|
+
if samples<int(rule['require_leaf_samples_at_least']): reasons.append('fallback: insufficient leaf support count')
|
|
65
|
+
return SelectiveLPDecision(backend,default,alternate,switch,leaf,mean,lower,support,'; '.join(reasons))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .enums import ConvexityStatus, ObjectiveSense, VariableDomain
|
|
2
|
+
from .linear import LinearProblem
|
|
3
|
+
from .mps import MPSParseError, MPSUnsupportedFeatureError, parse_mps, read_mps
|
|
4
|
+
from .quadratic import QuadraticProblem
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ConvexityStatus",
|
|
8
|
+
"LinearProblem",
|
|
9
|
+
"MPSParseError",
|
|
10
|
+
"MPSUnsupportedFeatureError",
|
|
11
|
+
"ObjectiveSense",
|
|
12
|
+
"parse_mps",
|
|
13
|
+
"read_mps",
|
|
14
|
+
"QuadraticProblem",
|
|
15
|
+
"VariableDomain",
|
|
16
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ObjectiveSense(str, Enum):
|
|
7
|
+
MINIMIZE = "minimize"
|
|
8
|
+
MAXIMIZE = "maximize"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VariableDomain(str, Enum):
|
|
12
|
+
CONTINUOUS = "continuous"
|
|
13
|
+
INTEGER = "integer"
|
|
14
|
+
BINARY = "binary"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConvexityStatus(str, Enum):
|
|
18
|
+
CONFIRMED = "confirmed"
|
|
19
|
+
REJECTED = "rejected"
|
|
20
|
+
UNKNOWN = "unknown"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _hash_parts(parts: Iterable[bytes]) -> str:
|
|
11
|
+
h = hashlib.sha256()
|
|
12
|
+
for part in parts:
|
|
13
|
+
h.update(len(part).to_bytes(8, byteorder="little", signed=False))
|
|
14
|
+
h.update(part)
|
|
15
|
+
return h.hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _array_bytes(arr: np.ndarray) -> bytes:
|
|
19
|
+
c = np.ascontiguousarray(arr)
|
|
20
|
+
return b"|".join(
|
|
21
|
+
[
|
|
22
|
+
str(c.dtype).encode(),
|
|
23
|
+
repr(tuple(c.shape)).encode(),
|
|
24
|
+
c.tobytes(order="C"),
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def hash_array(arr: np.ndarray) -> str:
|
|
30
|
+
return _hash_parts([_array_bytes(arr)])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def hash_sparse_matrix(matrix: sparse.spmatrix) -> str:
|
|
34
|
+
csr = sparse.csr_matrix(matrix, dtype=np.float64, copy=True)
|
|
35
|
+
csr.sum_duplicates()
|
|
36
|
+
csr.sort_indices()
|
|
37
|
+
return _hash_parts(
|
|
38
|
+
[
|
|
39
|
+
repr(csr.shape).encode(),
|
|
40
|
+
_array_bytes(csr.indptr.astype(np.int64, copy=False)),
|
|
41
|
+
_array_bytes(csr.indices.astype(np.int64, copy=False)),
|
|
42
|
+
_array_bytes(csr.data.astype(np.float64, copy=False)),
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def structural_hash_linear(
|
|
48
|
+
A: sparse.spmatrix,
|
|
49
|
+
domains: np.ndarray,
|
|
50
|
+
) -> str:
|
|
51
|
+
csr = sparse.csr_matrix(A, dtype=np.float64, copy=True)
|
|
52
|
+
csr.sum_duplicates()
|
|
53
|
+
csr.sort_indices()
|
|
54
|
+
# Structural hash intentionally excludes numerical matrix values.
|
|
55
|
+
return _hash_parts(
|
|
56
|
+
[
|
|
57
|
+
repr(csr.shape).encode(),
|
|
58
|
+
_array_bytes(csr.indptr.astype(np.int64, copy=False)),
|
|
59
|
+
_array_bytes(csr.indices.astype(np.int64, copy=False)),
|
|
60
|
+
_array_bytes(np.asarray(domains, dtype="U16")),
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def data_hash_linear(
|
|
66
|
+
A: sparse.spmatrix,
|
|
67
|
+
c: np.ndarray,
|
|
68
|
+
variable_lower: np.ndarray,
|
|
69
|
+
variable_upper: np.ndarray,
|
|
70
|
+
constraint_lower: np.ndarray,
|
|
71
|
+
constraint_upper: np.ndarray,
|
|
72
|
+
domains: np.ndarray,
|
|
73
|
+
objective_sense: str,
|
|
74
|
+
objective_offset: float,
|
|
75
|
+
) -> str:
|
|
76
|
+
return _hash_parts(
|
|
77
|
+
[
|
|
78
|
+
hash_sparse_matrix(A).encode(),
|
|
79
|
+
_array_bytes(c),
|
|
80
|
+
_array_bytes(variable_lower),
|
|
81
|
+
_array_bytes(variable_upper),
|
|
82
|
+
_array_bytes(constraint_lower),
|
|
83
|
+
_array_bytes(constraint_upper),
|
|
84
|
+
_array_bytes(np.asarray(domains, dtype="U16")),
|
|
85
|
+
objective_sense.encode(),
|
|
86
|
+
np.float64(objective_offset).tobytes(),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Iterable, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
|
|
9
|
+
from .enums import ObjectiveSense, VariableDomain
|
|
10
|
+
from .hashing import data_hash_linear, structural_hash_linear
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _readonly_f64(values: Sequence[float] | np.ndarray, *, name: str) -> np.ndarray:
|
|
14
|
+
arr = np.array(values, dtype=np.float64, copy=True)
|
|
15
|
+
if arr.ndim != 1:
|
|
16
|
+
raise ValueError(f"{name} must be one-dimensional")
|
|
17
|
+
if np.isnan(arr).any():
|
|
18
|
+
raise ValueError(f"{name} must not contain NaN")
|
|
19
|
+
arr.flags.writeable = False
|
|
20
|
+
return arr
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _require_finite(values: np.ndarray, *, name: str) -> None:
|
|
24
|
+
if not np.all(np.isfinite(values)):
|
|
25
|
+
raise ValueError(f"{name} must contain only finite values")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _require_bound_directions(lower: np.ndarray, upper: np.ndarray, *, label: str) -> None:
|
|
29
|
+
if np.isposinf(lower).any():
|
|
30
|
+
idx = int(np.flatnonzero(np.isposinf(lower))[0])
|
|
31
|
+
raise ValueError(f"{label} lower bound at index {idx} cannot be +infinity")
|
|
32
|
+
if np.isneginf(upper).any():
|
|
33
|
+
idx = int(np.flatnonzero(np.isneginf(upper))[0])
|
|
34
|
+
raise ValueError(f"{label} upper bound at index {idx} cannot be -infinity")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_domains(domains: Iterable[VariableDomain | str], n: int) -> np.ndarray:
|
|
38
|
+
vals = []
|
|
39
|
+
for value in domains:
|
|
40
|
+
try:
|
|
41
|
+
vals.append(VariableDomain(value).value)
|
|
42
|
+
except ValueError as exc:
|
|
43
|
+
raise ValueError(f"unknown variable domain: {value!r}") from exc
|
|
44
|
+
if len(vals) != n:
|
|
45
|
+
raise ValueError(f"domains must have length {n}, got {len(vals)}")
|
|
46
|
+
arr = np.asarray(vals, dtype="U16")
|
|
47
|
+
arr.flags.writeable = False
|
|
48
|
+
return arr
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _readonly_csr(A: sparse.spmatrix | np.ndarray) -> sparse.csr_matrix:
|
|
52
|
+
matrix = sparse.csr_matrix(A, dtype=np.float64, copy=True)
|
|
53
|
+
if matrix.ndim != 2:
|
|
54
|
+
raise ValueError("A must be two-dimensional")
|
|
55
|
+
if np.isnan(matrix.data).any() or np.isinf(matrix.data).any():
|
|
56
|
+
raise ValueError("A coefficients must be finite")
|
|
57
|
+
matrix.sum_duplicates()
|
|
58
|
+
if not np.isfinite(matrix.data).all():
|
|
59
|
+
raise ValueError("A coefficients must be finite after summing duplicates")
|
|
60
|
+
matrix.eliminate_zeros()
|
|
61
|
+
matrix.sort_indices()
|
|
62
|
+
matrix.data.flags.writeable = False
|
|
63
|
+
matrix.indices.flags.writeable = False
|
|
64
|
+
matrix.indptr.flags.writeable = False
|
|
65
|
+
return matrix
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class LinearProblem:
|
|
70
|
+
A: sparse.csr_matrix
|
|
71
|
+
c: np.ndarray
|
|
72
|
+
variable_lower: np.ndarray
|
|
73
|
+
variable_upper: np.ndarray
|
|
74
|
+
constraint_lower: np.ndarray
|
|
75
|
+
constraint_upper: np.ndarray
|
|
76
|
+
domains: np.ndarray
|
|
77
|
+
objective_sense: ObjectiveSense = ObjectiveSense.MINIMIZE
|
|
78
|
+
objective_offset: float = 0.0
|
|
79
|
+
name: str | None = None
|
|
80
|
+
metadata: dict[str, object] = field(default_factory=dict, compare=False, repr=False)
|
|
81
|
+
structural_hash: str = field(init=False)
|
|
82
|
+
data_hash: str = field(init=False)
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
A = _readonly_csr(self.A)
|
|
86
|
+
c = _readonly_f64(self.c, name="c")
|
|
87
|
+
vl = _readonly_f64(self.variable_lower, name="variable_lower")
|
|
88
|
+
vu = _readonly_f64(self.variable_upper, name="variable_upper")
|
|
89
|
+
cl = _readonly_f64(self.constraint_lower, name="constraint_lower")
|
|
90
|
+
cu = _readonly_f64(self.constraint_upper, name="constraint_upper")
|
|
91
|
+
sense = ObjectiveSense(self.objective_sense)
|
|
92
|
+
|
|
93
|
+
m, n = A.shape
|
|
94
|
+
if c.shape != (n,):
|
|
95
|
+
raise ValueError(f"c must have shape ({n},), got {c.shape}")
|
|
96
|
+
_require_finite(c, name="c")
|
|
97
|
+
if vl.shape != (n,) or vu.shape != (n,):
|
|
98
|
+
raise ValueError("variable bounds must match number of variables")
|
|
99
|
+
if cl.shape != (m,) or cu.shape != (m,):
|
|
100
|
+
raise ValueError("constraint bounds must match number of constraints")
|
|
101
|
+
_require_bound_directions(vl, vu, label="variable")
|
|
102
|
+
_require_bound_directions(cl, cu, label="constraint")
|
|
103
|
+
if np.any(vl > vu):
|
|
104
|
+
idx = int(np.flatnonzero(vl > vu)[0])
|
|
105
|
+
raise ValueError(f"inconsistent variable bounds at index {idx}: lower > upper")
|
|
106
|
+
if np.any(cl > cu):
|
|
107
|
+
idx = int(np.flatnonzero(cl > cu)[0])
|
|
108
|
+
raise ValueError(f"inconsistent constraint bounds at index {idx}: lower > upper")
|
|
109
|
+
|
|
110
|
+
domains = _normalize_domains(self.domains, n)
|
|
111
|
+
for j, domain in enumerate(domains):
|
|
112
|
+
if domain == VariableDomain.BINARY.value:
|
|
113
|
+
if vl[j] < 0.0 or vu[j] > 1.0:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"binary variable {j} must have bounds inside [0, 1], "
|
|
116
|
+
f"got [{vl[j]}, {vu[j]}]"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not np.isfinite(float(self.objective_offset)):
|
|
120
|
+
raise ValueError("objective_offset must be finite")
|
|
121
|
+
|
|
122
|
+
object.__setattr__(self, "A", A)
|
|
123
|
+
object.__setattr__(self, "c", c)
|
|
124
|
+
object.__setattr__(self, "variable_lower", vl)
|
|
125
|
+
object.__setattr__(self, "variable_upper", vu)
|
|
126
|
+
object.__setattr__(self, "constraint_lower", cl)
|
|
127
|
+
object.__setattr__(self, "constraint_upper", cu)
|
|
128
|
+
object.__setattr__(self, "domains", domains)
|
|
129
|
+
object.__setattr__(self, "objective_sense", sense)
|
|
130
|
+
object.__setattr__(self, "objective_offset", float(self.objective_offset))
|
|
131
|
+
object.__setattr__(self, "metadata", dict(self.metadata))
|
|
132
|
+
object.__setattr__(self, "structural_hash", structural_hash_linear(A, domains))
|
|
133
|
+
object.__setattr__(
|
|
134
|
+
self,
|
|
135
|
+
"data_hash",
|
|
136
|
+
data_hash_linear(
|
|
137
|
+
A,
|
|
138
|
+
c,
|
|
139
|
+
vl,
|
|
140
|
+
vu,
|
|
141
|
+
cl,
|
|
142
|
+
cu,
|
|
143
|
+
domains,
|
|
144
|
+
sense.value,
|
|
145
|
+
float(self.objective_offset),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def n_variables(self) -> int:
|
|
151
|
+
return self.A.shape[1]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def n_constraints(self) -> int:
|
|
155
|
+
return self.A.shape[0]
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def nnz(self) -> int:
|
|
159
|
+
return int(self.A.nnz)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def has_integer_variables(self) -> bool:
|
|
163
|
+
return bool(np.any(self.domains != VariableDomain.CONTINUOUS.value))
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_data(
|
|
167
|
+
cls,
|
|
168
|
+
*,
|
|
169
|
+
A: sparse.spmatrix | np.ndarray,
|
|
170
|
+
c: Sequence[float] | np.ndarray,
|
|
171
|
+
variable_lower: Sequence[float] | np.ndarray,
|
|
172
|
+
variable_upper: Sequence[float] | np.ndarray,
|
|
173
|
+
constraint_lower: Sequence[float] | np.ndarray,
|
|
174
|
+
constraint_upper: Sequence[float] | np.ndarray,
|
|
175
|
+
domains: Iterable[VariableDomain | str] | None = None,
|
|
176
|
+
objective_sense: ObjectiveSense | str = ObjectiveSense.MINIMIZE,
|
|
177
|
+
objective_offset: float = 0.0,
|
|
178
|
+
name: str | None = None,
|
|
179
|
+
metadata: dict[str, object] | None = None,
|
|
180
|
+
) -> "LinearProblem":
|
|
181
|
+
n = sparse.csr_matrix(A).shape[1]
|
|
182
|
+
if domains is None:
|
|
183
|
+
domains = [VariableDomain.CONTINUOUS] * n
|
|
184
|
+
return cls(
|
|
185
|
+
A=A,
|
|
186
|
+
c=np.asarray(c),
|
|
187
|
+
variable_lower=np.asarray(variable_lower),
|
|
188
|
+
variable_upper=np.asarray(variable_upper),
|
|
189
|
+
constraint_lower=np.asarray(constraint_lower),
|
|
190
|
+
constraint_upper=np.asarray(constraint_upper),
|
|
191
|
+
domains=list(domains),
|
|
192
|
+
objective_sense=ObjectiveSense(objective_sense),
|
|
193
|
+
objective_offset=objective_offset,
|
|
194
|
+
name=name,
|
|
195
|
+
metadata={} if metadata is None else metadata,
|
|
196
|
+
)
|