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.
Files changed (55) hide show
  1. operange/__init__.py +136 -0
  2. operange/_affine_distance.py +341 -0
  3. operange/_affine_distance_results.py +282 -0
  4. operange/_basic_support.py +75 -0
  5. operange/_change_contracts.py +271 -0
  6. operange/_finite_audit.py +91 -0
  7. operange/_geometry.py +226 -0
  8. operange/_heat_cascade.py +128 -0
  9. operange/_linear_process_results.py +223 -0
  10. operange/_linear_recourse.py +213 -0
  11. operange/_numeric.py +65 -0
  12. operange/_pinch_results.py +198 -0
  13. operange/_reference.py +65 -0
  14. operange/_startup_results.py +307 -0
  15. operange/_storage_reference.py +73 -0
  16. operange/adapters.py +246 -0
  17. operange/affine.py +332 -0
  18. operange/affine_results.py +599 -0
  19. operange/changes.py +373 -0
  20. operange/claim.py +234 -0
  21. operange/composition.py +179 -0
  22. operange/contract_types.py +583 -0
  23. operange/distance.py +51 -0
  24. operange/domain_io.py +68 -0
  25. operange/domains.py +331 -0
  26. operange/engineering_results.py +494 -0
  27. operange/experimental/__init__.py +1 -0
  28. operange/experimental/_quadratic_bounds.py +161 -0
  29. operange/experimental/_residopt_quadratic.py +181 -0
  30. operange/experimental/quadratic.py +563 -0
  31. operange/geometries.py +270 -0
  32. operange/heat_recovery.py +354 -0
  33. operange/heat_results.py +542 -0
  34. operange/heat_sensitivity.py +309 -0
  35. operange/linear.py +100 -0
  36. operange/linear_process.py +230 -0
  37. operange/pinch.py +272 -0
  38. operange/policy.py +115 -0
  39. operange/polytope.py +212 -0
  40. operange/primitives.py +247 -0
  41. operange/profiles.py +53 -0
  42. operange/recourse.py +66 -0
  43. operange/reference.py +102 -0
  44. operange/results.py +87 -0
  45. operange/startup.py +291 -0
  46. operange/storage.py +459 -0
  47. operange/storage_contract.py +488 -0
  48. operange/storage_results.py +58 -0
  49. operange/storage_types.py +154 -0
  50. operange/vector.py +101 -0
  51. operange-0.1.0.dist-info/METADATA +247 -0
  52. operange-0.1.0.dist-info/RECORD +55 -0
  53. operange-0.1.0.dist-info/WHEEL +5 -0
  54. operange-0.1.0.dist-info/licenses/LICENSE +21 -0
  55. 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