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,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
+ )