operange 0.1.0__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.
- operange/__init__.py +136 -0
- operange/_affine_distance.py +341 -0
- operange/_affine_distance_results.py +282 -0
- operange/_basic_support.py +75 -0
- operange/_change_contracts.py +271 -0
- operange/_finite_audit.py +91 -0
- operange/_geometry.py +226 -0
- operange/_heat_cascade.py +128 -0
- operange/_linear_process_results.py +223 -0
- operange/_linear_recourse.py +213 -0
- operange/_numeric.py +65 -0
- operange/_pinch_results.py +198 -0
- operange/_reference.py +65 -0
- operange/_startup_results.py +307 -0
- operange/_storage_reference.py +73 -0
- operange/adapters.py +246 -0
- operange/affine.py +332 -0
- operange/affine_results.py +599 -0
- operange/changes.py +373 -0
- operange/claim.py +234 -0
- operange/composition.py +179 -0
- operange/contract_types.py +583 -0
- operange/distance.py +51 -0
- operange/domain_io.py +68 -0
- operange/domains.py +331 -0
- operange/engineering_results.py +494 -0
- operange/experimental/__init__.py +1 -0
- operange/experimental/_quadratic_bounds.py +161 -0
- operange/experimental/_residopt_quadratic.py +181 -0
- operange/experimental/quadratic.py +563 -0
- operange/geometries.py +270 -0
- operange/heat_recovery.py +354 -0
- operange/heat_results.py +542 -0
- operange/heat_sensitivity.py +309 -0
- operange/linear.py +100 -0
- operange/linear_process.py +230 -0
- operange/pinch.py +272 -0
- operange/policy.py +115 -0
- operange/polytope.py +212 -0
- operange/primitives.py +247 -0
- operange/profiles.py +53 -0
- operange/recourse.py +66 -0
- operange/reference.py +102 -0
- operange/results.py +87 -0
- operange/startup.py +291 -0
- operange/storage.py +459 -0
- operange/storage_contract.py +488 -0
- operange/storage_results.py +58 -0
- operange/storage_types.py +154 -0
- operange/vector.py +101 -0
- operange-0.1.0.dist-info/METADATA +247 -0
- operange-0.1.0.dist-info/RECORD +55 -0
- operange-0.1.0.dist-info/WHEEL +5 -0
- operange-0.1.0.dist-info/licenses/LICENSE +21 -0
- operange-0.1.0.dist-info/top_level.txt +1 -0
operange/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Process-engineering sensitivity and robustness DSL with portable evidence."""
|
|
2
|
+
|
|
3
|
+
from .affine import AffineOutput, AffineProcessAdapter, AffineRequirement, AffineTerm
|
|
4
|
+
from .linear_process import LinearControl, LinearProcessAdapter
|
|
5
|
+
from .vector import VectorParameter
|
|
6
|
+
from .profiles import PiecewiseLinearProfile
|
|
7
|
+
from .geometries import BudgetSet, EllipsoidSet, SimplexSet
|
|
8
|
+
from .polytope import LinearConstraint, PolytopeSet
|
|
9
|
+
from .composition import Intersection, Product, Union
|
|
10
|
+
from .domain_io import domain_from_json, domain_from_manifest
|
|
11
|
+
from ._geometry import LinearSupport
|
|
12
|
+
from .claim import AdapterCapabilities, Capability, Claim, ModelAdapter
|
|
13
|
+
from .changes import (
|
|
14
|
+
ChangeComparison,
|
|
15
|
+
ChangeResult,
|
|
16
|
+
ContractComparison,
|
|
17
|
+
EngineeringChange,
|
|
18
|
+
FieldChange,
|
|
19
|
+
RequirementSummary,
|
|
20
|
+
compare_changes,
|
|
21
|
+
)
|
|
22
|
+
from .distance import Distance, NormalizedLInf
|
|
23
|
+
from .domains import (
|
|
24
|
+
Coordinate,
|
|
25
|
+
DomainCapabilities,
|
|
26
|
+
FiniteSet,
|
|
27
|
+
MembershipCheck,
|
|
28
|
+
ParameterSpace,
|
|
29
|
+
Scenario,
|
|
30
|
+
UncertaintySet,
|
|
31
|
+
)
|
|
32
|
+
from .recourse import DecisionRule, RecoursePolicy
|
|
33
|
+
from .contract_types import (
|
|
34
|
+
ArtifactRef,
|
|
35
|
+
Axis,
|
|
36
|
+
Bound,
|
|
37
|
+
Cardinality,
|
|
38
|
+
ConstraintCheck,
|
|
39
|
+
ConstraintSpec,
|
|
40
|
+
Coverage,
|
|
41
|
+
Derivative,
|
|
42
|
+
Diagnostic,
|
|
43
|
+
Evidence,
|
|
44
|
+
EvaluationPayload,
|
|
45
|
+
Measurement,
|
|
46
|
+
Membership,
|
|
47
|
+
Objective,
|
|
48
|
+
ProcessContract,
|
|
49
|
+
QuantitySpec,
|
|
50
|
+
QuantityValue,
|
|
51
|
+
RobustnessPayload,
|
|
52
|
+
Search,
|
|
53
|
+
SensitivityPayload,
|
|
54
|
+
Witness,
|
|
55
|
+
)
|
|
56
|
+
from .engineering_results import (
|
|
57
|
+
EvaluationResult,
|
|
58
|
+
RobustnessResult,
|
|
59
|
+
SensitivityResult,
|
|
60
|
+
result_from_json,
|
|
61
|
+
)
|
|
62
|
+
from .primitives import BoxSet, Parameter
|
|
63
|
+
from .claim import bind_contract, rejected_result
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"AffineOutput",
|
|
67
|
+
"AffineProcessAdapter",
|
|
68
|
+
"AffineRequirement",
|
|
69
|
+
"AffineTerm",
|
|
70
|
+
"LinearControl",
|
|
71
|
+
"LinearProcessAdapter",
|
|
72
|
+
"VectorParameter",
|
|
73
|
+
"PiecewiseLinearProfile",
|
|
74
|
+
"BudgetSet",
|
|
75
|
+
"EllipsoidSet",
|
|
76
|
+
"SimplexSet",
|
|
77
|
+
"LinearConstraint",
|
|
78
|
+
"PolytopeSet",
|
|
79
|
+
"Intersection",
|
|
80
|
+
"Product",
|
|
81
|
+
"Union",
|
|
82
|
+
"domain_from_json",
|
|
83
|
+
"domain_from_manifest",
|
|
84
|
+
"LinearSupport",
|
|
85
|
+
"AdapterCapabilities",
|
|
86
|
+
"Capability",
|
|
87
|
+
"Claim",
|
|
88
|
+
"EngineeringChange",
|
|
89
|
+
"ChangeResult",
|
|
90
|
+
"ChangeComparison",
|
|
91
|
+
"ContractComparison",
|
|
92
|
+
"FieldChange",
|
|
93
|
+
"RequirementSummary",
|
|
94
|
+
"compare_changes",
|
|
95
|
+
"ModelAdapter",
|
|
96
|
+
"Distance",
|
|
97
|
+
"NormalizedLInf",
|
|
98
|
+
"Coordinate",
|
|
99
|
+
"DomainCapabilities",
|
|
100
|
+
"FiniteSet",
|
|
101
|
+
"MembershipCheck",
|
|
102
|
+
"ParameterSpace",
|
|
103
|
+
"Scenario",
|
|
104
|
+
"UncertaintySet",
|
|
105
|
+
"DecisionRule",
|
|
106
|
+
"RecoursePolicy",
|
|
107
|
+
"ArtifactRef",
|
|
108
|
+
"Axis",
|
|
109
|
+
"Bound",
|
|
110
|
+
"Cardinality",
|
|
111
|
+
"ConstraintCheck",
|
|
112
|
+
"ConstraintSpec",
|
|
113
|
+
"Coverage",
|
|
114
|
+
"Derivative",
|
|
115
|
+
"Diagnostic",
|
|
116
|
+
"Evidence",
|
|
117
|
+
"EvaluationPayload",
|
|
118
|
+
"EvaluationResult",
|
|
119
|
+
"Measurement",
|
|
120
|
+
"Membership",
|
|
121
|
+
"Objective",
|
|
122
|
+
"ProcessContract",
|
|
123
|
+
"QuantitySpec",
|
|
124
|
+
"QuantityValue",
|
|
125
|
+
"RobustnessPayload",
|
|
126
|
+
"RobustnessResult",
|
|
127
|
+
"Search",
|
|
128
|
+
"SensitivityPayload",
|
|
129
|
+
"SensitivityResult",
|
|
130
|
+
"Witness",
|
|
131
|
+
"result_from_json",
|
|
132
|
+
"BoxSet",
|
|
133
|
+
"Parameter",
|
|
134
|
+
"bind_contract",
|
|
135
|
+
"rejected_result",
|
|
136
|
+
]
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Affine threshold LPs with exact bounded-box certificates.
|
|
2
|
+
|
|
3
|
+
The solver proposes points and nonnegative multipliers. All physical targets,
|
|
4
|
+
domain rows, distances, lower bounds and infeasibility proofs are checked on
|
|
5
|
+
the exact rational values of the declarations. Solver status is not a proof.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import asdict, dataclass, field
|
|
9
|
+
from fractions import Fraction
|
|
10
|
+
from math import inf, nextafter
|
|
11
|
+
|
|
12
|
+
from . import linear
|
|
13
|
+
from ._numeric import exact_dot, exact_normalized_bounds, round_down, round_up
|
|
14
|
+
from .polytope import PolytopeSet
|
|
15
|
+
from .primitives import finite
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def box_minimum(coefficients, bounds):
|
|
19
|
+
return sum(
|
|
20
|
+
(min(a * lo, a * hi) for a, (lo, hi) in zip(coefficients, bounds)), Fraction(0)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ThresholdProblem:
|
|
26
|
+
claim: object
|
|
27
|
+
requirement: object
|
|
28
|
+
threshold: float
|
|
29
|
+
parameters: tuple
|
|
30
|
+
bounds: tuple
|
|
31
|
+
domain_rows: list
|
|
32
|
+
domain_upper: list
|
|
33
|
+
rows: list
|
|
34
|
+
upper: list
|
|
35
|
+
row_labels: list
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def objective(self):
|
|
39
|
+
return (Fraction(0),) * len(self.parameters) + (Fraction(1),)
|
|
40
|
+
|
|
41
|
+
def distance(self, point):
|
|
42
|
+
return max(
|
|
43
|
+
abs(Fraction(point[c.name]) - Fraction(c.nominal)) / Fraction(c.scale)
|
|
44
|
+
for c in self.claim.distance.space.coordinates
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def accepts(self, point):
|
|
48
|
+
if any(not p.lower <= point[p.name] <= p.upper for p in self.parameters):
|
|
49
|
+
return False
|
|
50
|
+
z = [
|
|
51
|
+
(Fraction(point[p.name]) - Fraction(p.nominal)) / Fraction(p.scale)
|
|
52
|
+
for p in self.parameters
|
|
53
|
+
]
|
|
54
|
+
if any(
|
|
55
|
+
exact_dot(row, z) > b for row, b in zip(self.domain_rows, self.domain_upper)
|
|
56
|
+
):
|
|
57
|
+
return False
|
|
58
|
+
model = self.claim.adapter
|
|
59
|
+
residual = self.requirement.sign * (
|
|
60
|
+
model.output(self.requirement.output)._exact_value(
|
|
61
|
+
{**point, **model._fixed_values(self.claim)}
|
|
62
|
+
)
|
|
63
|
+
- Fraction(self.requirement.limit)
|
|
64
|
+
)
|
|
65
|
+
return residual >= Fraction(self.threshold)
|
|
66
|
+
|
|
67
|
+
def physical_candidates(self, coordinates):
|
|
68
|
+
if len(coordinates) != len(self.parameters):
|
|
69
|
+
raise ValueError("wrong number of proposed search coordinates")
|
|
70
|
+
exact = [
|
|
71
|
+
Fraction(p.nominal)
|
|
72
|
+
+ Fraction(p.scale) * Fraction(finite(z, "search coordinate"))
|
|
73
|
+
for p, z in zip(self.parameters, coordinates)
|
|
74
|
+
]
|
|
75
|
+
coefficients = {
|
|
76
|
+
t.variable: self.requirement.sign * t.coefficient
|
|
77
|
+
for t in self.claim.adapter.output(self.requirement.output).terms
|
|
78
|
+
}
|
|
79
|
+
bases = []
|
|
80
|
+
for directed in (False, True):
|
|
81
|
+
values = []
|
|
82
|
+
for p, value in zip(self.parameters, exact):
|
|
83
|
+
value = min(Fraction(p.upper), max(Fraction(p.lower), value))
|
|
84
|
+
coefficient = coefficients.get(p.name, 0)
|
|
85
|
+
rounded = (
|
|
86
|
+
(round_up(value) if coefficient > 0 else round_down(value))
|
|
87
|
+
if directed and coefficient
|
|
88
|
+
else float(value)
|
|
89
|
+
)
|
|
90
|
+
values.append(min(p.upper, max(p.lower, rounded)))
|
|
91
|
+
bases.append(dict(zip((p.name for p in self.parameters), values)))
|
|
92
|
+
# Small rounding repairs are candidates only. Every variant must still
|
|
93
|
+
# satisfy the exact polytope and physical target; no tolerance widens them.
|
|
94
|
+
seen = set()
|
|
95
|
+
for point in bases:
|
|
96
|
+
variants = [point]
|
|
97
|
+
for p in self.parameters:
|
|
98
|
+
for direction in (-inf, inf):
|
|
99
|
+
value = nextafter(point[p.name], direction)
|
|
100
|
+
if p.lower <= value <= p.upper:
|
|
101
|
+
variants.append({**point, p.name: value})
|
|
102
|
+
for variant in variants:
|
|
103
|
+
key = tuple(variant[p.name] for p in self.parameters)
|
|
104
|
+
if key not in seen:
|
|
105
|
+
seen.add(key)
|
|
106
|
+
yield variant
|
|
107
|
+
|
|
108
|
+
def certificate(self, multipliers, *, infeasibility=False):
|
|
109
|
+
if len(multipliers) != len(self.rows):
|
|
110
|
+
raise ValueError("wrong number of proposed dual multipliers")
|
|
111
|
+
multipliers = [max(0.0, finite(v, "dual multiplier")) for v in multipliers]
|
|
112
|
+
weighted = [exact_dot(column, multipliers) for column in zip(*self.rows)]
|
|
113
|
+
objective = (
|
|
114
|
+
(Fraction(0),) * len(self.bounds) if infeasibility else self.objective
|
|
115
|
+
)
|
|
116
|
+
residual = [a + c for a, c in zip(weighted, objective)]
|
|
117
|
+
minimum = box_minimum(residual, self.bounds)
|
|
118
|
+
rhs = exact_dot(self.upper, multipliers)
|
|
119
|
+
bound = minimum - rhs
|
|
120
|
+
return bound, {
|
|
121
|
+
"method": "bounded_box_contradiction"
|
|
122
|
+
if infeasibility
|
|
123
|
+
else "bounded_box_weak_duality",
|
|
124
|
+
"formula": "min_box((lambda A) v) - lambda b > 0"
|
|
125
|
+
if infeasibility
|
|
126
|
+
else "min_box((c + lambda A) v) - lambda b <= minimum_distance",
|
|
127
|
+
"multipliers": multipliers,
|
|
128
|
+
"weighted_residual_exact": list(map(str, residual)),
|
|
129
|
+
"box_minimum_exact": str(minimum),
|
|
130
|
+
"weighted_rhs_exact": str(rhs),
|
|
131
|
+
"bound_exact": str(bound),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def manifest(self):
|
|
135
|
+
return {
|
|
136
|
+
"requirement": self.requirement.name,
|
|
137
|
+
"threshold": self.threshold,
|
|
138
|
+
"unit": self.claim.adapter.output(self.requirement.output).unit,
|
|
139
|
+
"coordinates": [p.name for p in self.parameters] + ["distance_radius"],
|
|
140
|
+
"coordinate_space": "domain_normalized_inputs_and_normalized_linf_radius",
|
|
141
|
+
"origins": [p.nominal for p in self.parameters],
|
|
142
|
+
"scales": [p.scale for p in self.parameters],
|
|
143
|
+
"row_labels": self.row_labels,
|
|
144
|
+
"rows_exact": [[str(a) for a in row] for row in self.rows],
|
|
145
|
+
"rhs_exact": list(map(str, self.upper)),
|
|
146
|
+
"bounds_exact": [[str(lo), str(hi)] for lo, hi in self.bounds],
|
|
147
|
+
"objective_exact": list(map(str, self.objective)),
|
|
148
|
+
"arithmetic": "exact_rationals_of_declared_floats",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def compile_problem(claim, requirement, threshold):
|
|
153
|
+
polytope = claim.domain if type(claim.domain) is PolytopeSet else None
|
|
154
|
+
box = polytope.envelope if polytope is not None else claim.domain
|
|
155
|
+
parameters = box.scalar_parameters
|
|
156
|
+
n = len(parameters)
|
|
157
|
+
domain_rows, domain_upper, labels = [], [], []
|
|
158
|
+
if polytope is not None:
|
|
159
|
+
for constraint in polytope.constraints:
|
|
160
|
+
for sign in (1, -1) if constraint.relation == "eq" else (1,):
|
|
161
|
+
domain_rows.append(
|
|
162
|
+
tuple(
|
|
163
|
+
sign * Fraction(constraint.coefficients.get(p.name, 0))
|
|
164
|
+
for p in parameters
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
domain_upper.append(sign * Fraction(constraint.rhs))
|
|
168
|
+
labels.append(f"domain:{constraint.name}:{sign}")
|
|
169
|
+
rows = [row + (Fraction(0),) for row in domain_rows]
|
|
170
|
+
upper = list(domain_upper)
|
|
171
|
+
radius_min, radius_max = Fraction(0), Fraction(0)
|
|
172
|
+
for i, p in enumerate(parameters):
|
|
173
|
+
distance_coordinate = claim.distance.space.coordinate(p.name)
|
|
174
|
+
origin, scale = (
|
|
175
|
+
Fraction(distance_coordinate.nominal),
|
|
176
|
+
Fraction(distance_coordinate.scale),
|
|
177
|
+
)
|
|
178
|
+
lo, hi = (
|
|
179
|
+
(Fraction(p.lower) - origin) / scale,
|
|
180
|
+
(Fraction(p.upper) - origin) / scale,
|
|
181
|
+
)
|
|
182
|
+
radius_min = max(radius_min, lo, -hi)
|
|
183
|
+
radius_max = max(radius_max, abs(lo), abs(hi))
|
|
184
|
+
weight, offset = (
|
|
185
|
+
Fraction(p.scale) / scale,
|
|
186
|
+
(Fraction(p.nominal) - origin) / scale,
|
|
187
|
+
)
|
|
188
|
+
for sign in (1, -1):
|
|
189
|
+
row = [Fraction(0)] * (n + 1)
|
|
190
|
+
row[i], row[-1] = sign * weight, Fraction(-1)
|
|
191
|
+
rows.append(tuple(row))
|
|
192
|
+
upper.append(-sign * offset)
|
|
193
|
+
labels.append(f"distance:{p.name}:{sign}")
|
|
194
|
+
model = claim.adapter
|
|
195
|
+
output = model.output(requirement.output)
|
|
196
|
+
coefficients = {t.variable: Fraction(t.coefficient) for t in output.terms}
|
|
197
|
+
residual_scale = Fraction(requirement.residual_scale)
|
|
198
|
+
baseline = requirement.sign * (
|
|
199
|
+
output._exact_value({**box.nominal, **model._fixed_values(claim)})
|
|
200
|
+
- Fraction(requirement.limit)
|
|
201
|
+
)
|
|
202
|
+
rows.append(
|
|
203
|
+
tuple(
|
|
204
|
+
-requirement.sign
|
|
205
|
+
* coefficients.get(p.name, 0)
|
|
206
|
+
* Fraction(p.scale)
|
|
207
|
+
/ residual_scale
|
|
208
|
+
for p in parameters
|
|
209
|
+
)
|
|
210
|
+
+ (Fraction(0),)
|
|
211
|
+
)
|
|
212
|
+
upper.append((baseline - Fraction(threshold)) / residual_scale)
|
|
213
|
+
labels.append(f"target:{requirement.name}")
|
|
214
|
+
bounds = exact_normalized_bounds(box) + ((radius_min, radius_max),)
|
|
215
|
+
# Explicit bound rows provide the LP dual with all bound multipliers.
|
|
216
|
+
# The exact same box also corrects any residual in its proposed dual equality.
|
|
217
|
+
for i, (lo, hi) in enumerate(bounds):
|
|
218
|
+
row = tuple(Fraction(j == i) for j in range(n + 1))
|
|
219
|
+
rows.extend((row, tuple(-v for v in row)))
|
|
220
|
+
upper.extend((hi, -lo))
|
|
221
|
+
labels.extend((f"bound:{i}:upper", f"bound:{i}:lower"))
|
|
222
|
+
return ThresholdProblem(
|
|
223
|
+
claim,
|
|
224
|
+
requirement,
|
|
225
|
+
threshold,
|
|
226
|
+
parameters,
|
|
227
|
+
bounds,
|
|
228
|
+
domain_rows,
|
|
229
|
+
domain_upper,
|
|
230
|
+
rows,
|
|
231
|
+
upper,
|
|
232
|
+
labels,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class BranchResult:
|
|
238
|
+
problem: ThresholdProblem
|
|
239
|
+
lower: Fraction
|
|
240
|
+
lower_proof: dict
|
|
241
|
+
unreachable: bool = False
|
|
242
|
+
infeasibility_proof: dict | None = None
|
|
243
|
+
point: dict | None = None
|
|
244
|
+
distance: Fraction | None = None
|
|
245
|
+
evaluation: object = None
|
|
246
|
+
attempts: list = field(default_factory=list)
|
|
247
|
+
diagnostics: list = field(default_factory=list)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def solve_branch(problem, evaluate):
|
|
251
|
+
result = BranchResult(
|
|
252
|
+
problem, problem.bounds[-1][0], {"method": "distance_to_enclosing_box"}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def consider(point):
|
|
256
|
+
if not problem.accepts(point):
|
|
257
|
+
return
|
|
258
|
+
evaluation = evaluate(point)
|
|
259
|
+
if (
|
|
260
|
+
evaluation.execution != "completed"
|
|
261
|
+
or evaluation.payload.membership.status != "inside"
|
|
262
|
+
):
|
|
263
|
+
return
|
|
264
|
+
distance = problem.distance(point)
|
|
265
|
+
# Ensure the candidate distance can be exported before retaining it.
|
|
266
|
+
round_up(distance)
|
|
267
|
+
if result.distance is None or distance < result.distance:
|
|
268
|
+
result.point, result.distance, result.evaluation = (
|
|
269
|
+
point,
|
|
270
|
+
distance,
|
|
271
|
+
evaluation,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def solve(purpose, *args, **kwargs):
|
|
275
|
+
point, evidence = linear.solve_lp(*args, tolerance=1e-9, **kwargs)
|
|
276
|
+
result.attempts.append({"purpose": purpose, **asdict(evidence)})
|
|
277
|
+
return point
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
consider(problem.claim.domain.feasible_point)
|
|
281
|
+
consider(
|
|
282
|
+
{
|
|
283
|
+
p.name: min(
|
|
284
|
+
p.upper,
|
|
285
|
+
max(
|
|
286
|
+
p.lower, problem.claim.distance.space.coordinate(p.name).nominal
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
for p in problem.parameters
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
if result.distance == result.lower:
|
|
293
|
+
return result
|
|
294
|
+
for i, (row, rhs) in enumerate(zip(problem.rows, problem.upper)):
|
|
295
|
+
if box_minimum(row, problem.bounds) > rhs:
|
|
296
|
+
gap, proof = problem.certificate(
|
|
297
|
+
[float(j == i) for j in range(len(problem.rows))],
|
|
298
|
+
infeasibility=True,
|
|
299
|
+
)
|
|
300
|
+
if gap > 0:
|
|
301
|
+
result.unreachable, result.infeasibility_proof = True, proof
|
|
302
|
+
return result
|
|
303
|
+
rows = [
|
|
304
|
+
[finite(float(a), "LP coefficient") for a in row] for row in problem.rows
|
|
305
|
+
]
|
|
306
|
+
rhs = [finite(float(b), "LP rhs") for b in problem.upper]
|
|
307
|
+
bounds = [(round_down(lo), round_up(hi)) for lo, hi in problem.bounds]
|
|
308
|
+
objective = [float(v) for v in problem.objective]
|
|
309
|
+
primal = solve(
|
|
310
|
+
"threshold_candidate", objective, bounds, inequalities=rows, upper=rhs
|
|
311
|
+
)
|
|
312
|
+
if primal is not None:
|
|
313
|
+
for point in problem.physical_candidates(primal[:-1]):
|
|
314
|
+
consider(point)
|
|
315
|
+
dual = solve(
|
|
316
|
+
"distance_lower_bound",
|
|
317
|
+
rhs,
|
|
318
|
+
[(0, None)] * len(rows),
|
|
319
|
+
equalities=[list(column) for column in zip(*rows)],
|
|
320
|
+
rhs=[-v for v in objective],
|
|
321
|
+
)
|
|
322
|
+
if dual is not None:
|
|
323
|
+
lower, proof = problem.certificate(dual)
|
|
324
|
+
if lower > result.lower:
|
|
325
|
+
result.lower, result.lower_proof = lower, proof
|
|
326
|
+
if result.distance is None:
|
|
327
|
+
multipliers = solve(
|
|
328
|
+
"unreachability_candidate",
|
|
329
|
+
rhs,
|
|
330
|
+
[(0, 1)] * len(rows),
|
|
331
|
+
equalities=[list(column) for column in zip(*rows)]
|
|
332
|
+
+ [[1.0] * len(rows)],
|
|
333
|
+
rhs=[0.0] * len(bounds) + [1.0],
|
|
334
|
+
)
|
|
335
|
+
if multipliers is not None:
|
|
336
|
+
gap, proof = problem.certificate(multipliers, infeasibility=True)
|
|
337
|
+
if gap > 0:
|
|
338
|
+
result.unreachable, result.infeasibility_proof = True, proof
|
|
339
|
+
except (ValueError, OverflowError) as exc:
|
|
340
|
+
result.diagnostics.append(str(exc))
|
|
341
|
+
return result
|