certflow 1.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.
- certflow/__init__.py +40 -0
- certflow/baselines.py +279 -0
- certflow/cert.py +1186 -0
- certflow/ch.py +1348 -0
- certflow/conformal.py +275 -0
- certflow/drift.py +472 -0
- certflow/egraph.py +371 -0
- certflow/episodes.py +176 -0
- certflow/fastgraph.py +1032 -0
- certflow/graphcore.py +271 -0
- certflow/harness.py +534 -0
- certflow/movingai.py +673 -0
- certflow/oracle.py +222 -0
- certflow/realworld.py +375 -0
- certflow/roadnet.py +585 -0
- certflow/sensing.py +121 -0
- certflow/snapshot.py +167 -0
- certflow/types.py +119 -0
- certflow-1.0.1.dist-info/METADATA +199 -0
- certflow-1.0.1.dist-info/RECORD +22 -0
- certflow-1.0.1.dist-info/WHEEL +4 -0
- certflow-1.0.1.dist-info/licenses/LICENSE +21 -0
certflow/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""CERT-FLOW: certified route planning under drifting costs.
|
|
2
|
+
|
|
3
|
+
Every replanning round emits a high-probability certificate LB <= OPT <= UB
|
|
4
|
+
on the optimal route cost, built from age-weighted non-exchangeable conformal
|
|
5
|
+
prediction over drift-adjusted observation residuals, and directs paid sensing
|
|
6
|
+
at the edges that shrink the certified gap fastest.
|
|
7
|
+
|
|
8
|
+
Quickstart::
|
|
9
|
+
|
|
10
|
+
from certflow import CertPlanner, PlannerConfig
|
|
11
|
+
from certflow.drift import grid_world
|
|
12
|
+
|
|
13
|
+
world = grid_world(6, 6, seed=0, kind="bounded", rho=0.02, noise_scale=0.05)
|
|
14
|
+
planner = CertPlanner(world, (0, 0), (5, 5),
|
|
15
|
+
PlannerConfig(epsilon=5.0, alpha_prime=0.2))
|
|
16
|
+
for _ in range(150):
|
|
17
|
+
cert, sensed = planner.round()
|
|
18
|
+
print(cert.lb, cert.ub, cert.confidence)
|
|
19
|
+
|
|
20
|
+
Submodules: conformal (quantile machinery), cert (the planner loop), sensing
|
|
21
|
+
(observation selection), fastgraph (flat-array engine), snapshot / ch
|
|
22
|
+
(certificate-gated preprocessing), drift / realworld / movingai / roadnet
|
|
23
|
+
(worlds and graphs), harness / episodes / oracle (experiment infrastructure).
|
|
24
|
+
"""
|
|
25
|
+
from certflow.cert import CertPlanner, PlannerConfig
|
|
26
|
+
from certflow.conformal import ACITracker, ConformalScorer
|
|
27
|
+
from certflow.types import Certificate, EdgeBelief, World
|
|
28
|
+
|
|
29
|
+
__version__ = "1.0.1"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ACITracker",
|
|
33
|
+
"CertPlanner",
|
|
34
|
+
"Certificate",
|
|
35
|
+
"ConformalScorer",
|
|
36
|
+
"EdgeBelief",
|
|
37
|
+
"PlannerConfig",
|
|
38
|
+
"World",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
certflow/baselines.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Gaussian mu±beta*sigma calibration baseline for CERT.
|
|
2
|
+
|
|
3
|
+
This module provides the honest strawman for the paper's coverage claim: the
|
|
4
|
+
same planner loop, same widening, same D* Lite dual search — only the quantile
|
|
5
|
+
machinery is swapped. The key contrast:
|
|
6
|
+
|
|
7
|
+
- ConformalScorer: distribution-free, uses the empirical weighted quantile.
|
|
8
|
+
Coverage degrades gracefully on heavy-tailed or drifted noise.
|
|
9
|
+
- GaussianScorer: parametric, uses mu + z_{1-alpha} * sigma. Claims full
|
|
10
|
+
confidence (delta_stale = 0) regardless of buffer age. Over-claims coverage
|
|
11
|
+
on heavy-tailed distributions — that is the mechanism under test.
|
|
12
|
+
|
|
13
|
+
The GaussianCertPlanner disables ACI (gamma -> 0) because the purpose of this
|
|
14
|
+
baseline is to expose the raw Gaussian claim without the assumption-free ACI
|
|
15
|
+
safety net widening it back. Leaving ACI active would partially mask the
|
|
16
|
+
over-claim and muddy the comparison. This is documented via the ``use_aci``
|
|
17
|
+
flag attribute; the underlying trick is replacing the ACITracker with one that
|
|
18
|
+
has gamma=1e-12 so working_alpha stays effectively fixed at alpha_target.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
|
|
24
|
+
from scipy.stats import norm
|
|
25
|
+
|
|
26
|
+
from certflow.cert import CertPlanner, PlannerConfig
|
|
27
|
+
from certflow.conformal import ACITracker, CalSample
|
|
28
|
+
from certflow.drift import grid_world
|
|
29
|
+
from certflow.harness import ExperimentConfig
|
|
30
|
+
from certflow.oracle import opt
|
|
31
|
+
from certflow.types import EpisodeResult, RoundLog
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# GaussianScorer
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
class GaussianScorer:
|
|
39
|
+
"""Gaussian mu±z*sigma quantile using the same age-geometric weights as
|
|
40
|
+
ConformalScorer.
|
|
41
|
+
|
|
42
|
+
Interface mirrors ConformalScorer exactly:
|
|
43
|
+
push(residual, t), quantile(alpha, t), delta_stale(t), ready(alpha, t),
|
|
44
|
+
__len__.
|
|
45
|
+
|
|
46
|
+
Key differences from ConformalScorer
|
|
47
|
+
-------------------------------------
|
|
48
|
+
- quantile: parametric Gaussian formula using weighted mean and weighted
|
|
49
|
+
std, then z_{1-alpha} (scipy.stats.norm.ppf). No distribution-free
|
|
50
|
+
quantile computation.
|
|
51
|
+
- delta_stale: always 0.0. The Gaussian baseline silently assumes full
|
|
52
|
+
confidence regardless of how stale the buffer is. This over-claim is
|
|
53
|
+
the exact mechanism the paper tests against.
|
|
54
|
+
- ready: requires sum of weights >= 5 (effective sample count >= 5).
|
|
55
|
+
Returns False until threshold is met; quantile returns +inf before then.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
rho_w : float
|
|
60
|
+
Age-geometric weight decay per unit time (same meaning as
|
|
61
|
+
ConformalScorer.rho_w).
|
|
62
|
+
max_buffer : int
|
|
63
|
+
Rolling buffer capacity (same as ConformalScorer).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
rho_w: float = 0.99,
|
|
69
|
+
max_buffer: int = 2000,
|
|
70
|
+
) -> None:
|
|
71
|
+
if not 0.0 < rho_w <= 1.0:
|
|
72
|
+
raise ValueError("rho_w must be in (0, 1]")
|
|
73
|
+
self.rho_w = rho_w
|
|
74
|
+
self.max_buffer = max_buffer
|
|
75
|
+
self._buf: list[CalSample] = []
|
|
76
|
+
|
|
77
|
+
def push(self, residual: float, t: float) -> None:
|
|
78
|
+
self._buf.append(CalSample(residual, t))
|
|
79
|
+
if len(self._buf) > self.max_buffer:
|
|
80
|
+
self._buf.sort(key=lambda s: s.t)
|
|
81
|
+
del self._buf[0 : len(self._buf) - self.max_buffer]
|
|
82
|
+
|
|
83
|
+
def __len__(self) -> int:
|
|
84
|
+
return len(self._buf)
|
|
85
|
+
|
|
86
|
+
def _weights(self, t: float) -> list[float]:
|
|
87
|
+
return [self.rho_w ** max(0.0, t - s.t) for s in self._buf]
|
|
88
|
+
|
|
89
|
+
def _effective_n(self, t: float) -> float:
|
|
90
|
+
return sum(self._weights(t))
|
|
91
|
+
|
|
92
|
+
def quantile(self, alpha: float, t: float) -> float:
|
|
93
|
+
"""Weighted Gaussian (1-alpha)-quantile: mu_w + z_{1-alpha} * sigma_w.
|
|
94
|
+
|
|
95
|
+
Returns +inf when ready() is False (fewer than 5 effective samples).
|
|
96
|
+
"""
|
|
97
|
+
if not 0.0 < alpha < 1.0:
|
|
98
|
+
raise ValueError("alpha must be in (0, 1)")
|
|
99
|
+
if not self._buf:
|
|
100
|
+
return math.inf
|
|
101
|
+
w = self._weights(t)
|
|
102
|
+
w_sum = sum(w)
|
|
103
|
+
if w_sum < 5.0:
|
|
104
|
+
return math.inf
|
|
105
|
+
# Weighted mean
|
|
106
|
+
residuals = [s.residual for s in self._buf]
|
|
107
|
+
mu = sum(wi * r for wi, r in zip(w, residuals)) / w_sum
|
|
108
|
+
# Weighted variance (reliability weights: divide by w_sum, no Bessel)
|
|
109
|
+
var = sum(wi * (r - mu) ** 2 for wi, r in zip(w, residuals)) / w_sum
|
|
110
|
+
sigma = math.sqrt(var) if var > 0.0 else 0.0
|
|
111
|
+
z = float(norm.ppf(1.0 - alpha))
|
|
112
|
+
return mu + z * sigma
|
|
113
|
+
|
|
114
|
+
def delta_stale(self, t: float) -> float:
|
|
115
|
+
"""Always 0.0.
|
|
116
|
+
|
|
117
|
+
The Gaussian baseline claims full confidence regardless of how old the
|
|
118
|
+
calibration buffer is. This is the over-claim the paper is designed to
|
|
119
|
+
expose: there is no staleness correction, so the reported confidence
|
|
120
|
+
never degrades with buffer age.
|
|
121
|
+
"""
|
|
122
|
+
return 0.0
|
|
123
|
+
|
|
124
|
+
def push_signed(self, deviation: float, t: float) -> None:
|
|
125
|
+
"""Interface parity with ConformalScorer; the Gaussian baseline does
|
|
126
|
+
not implement the sum-aware UB (T4), so signed deviations are dropped."""
|
|
127
|
+
|
|
128
|
+
def block_quantile(self, alpha: float, t: float, block_len: int) -> float:
|
|
129
|
+
return float("inf")
|
|
130
|
+
|
|
131
|
+
def block_delta_stale(self, t: float, block_len: int) -> float:
|
|
132
|
+
return 1.0
|
|
133
|
+
|
|
134
|
+
def effective_mass(self, t: float) -> float:
|
|
135
|
+
"""Interface parity: weighted effective sample size (for annealing)."""
|
|
136
|
+
return sum(self._weights(t)) if hasattr(self, "_weights") else float(len(self))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def ready(self, alpha: float, t: float) -> bool:
|
|
140
|
+
"""True when the effective sample weight sum >= 5."""
|
|
141
|
+
return math.isfinite(self.quantile(alpha, t))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# GaussianCertPlanner
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
class GaussianCertPlanner(CertPlanner):
|
|
149
|
+
"""CertPlanner subclass with GaussianScorer and ACI disabled.
|
|
150
|
+
|
|
151
|
+
Construction: calls super().__init__ then replaces self.scorer with a
|
|
152
|
+
GaussianScorer (same rho_w) and replaces self.aci with an ACITracker
|
|
153
|
+
whose gamma=1e-12 so working_alpha stays essentially pinned at alpha_target
|
|
154
|
+
(the change in raw alpha per update is ~1e-12, negligible).
|
|
155
|
+
|
|
156
|
+
The ``use_aci`` flag (False by default) documents intent: the Gaussian
|
|
157
|
+
baseline intentionally does not use the assumption-free ACI safety net.
|
|
158
|
+
Enabling ACI (use_aci=True) is valid but changes the comparison by letting
|
|
159
|
+
the ACI net partially compensate for Gaussian over-claims.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
world,
|
|
165
|
+
start,
|
|
166
|
+
goal,
|
|
167
|
+
config: PlannerConfig,
|
|
168
|
+
t0: float = 0.0,
|
|
169
|
+
use_aci: bool = False,
|
|
170
|
+
) -> None:
|
|
171
|
+
super().__init__(world, start, goal, config, t0=t0)
|
|
172
|
+
# Swap the conformal scorer for the Gaussian one (same rho_w).
|
|
173
|
+
self.scorer = GaussianScorer(
|
|
174
|
+
rho_w=config.rho_w,
|
|
175
|
+
max_buffer=2000,
|
|
176
|
+
)
|
|
177
|
+
self.use_aci = use_aci
|
|
178
|
+
if not use_aci:
|
|
179
|
+
# Freeze working_alpha by using a vanishingly small gamma.
|
|
180
|
+
# ACITracker still accepts updates (no structural changes to the
|
|
181
|
+
# round() loop) but alpha_raw moves by at most ~1e-12 per step,
|
|
182
|
+
# so working_alpha() is effectively constant at alpha_target.
|
|
183
|
+
self.aci = ACITracker(
|
|
184
|
+
alpha_target=config.alpha_prime,
|
|
185
|
+
gamma=1e-12,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# gaussian_tier0_episode
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def gaussian_tier0_episode(config: ExperimentConfig, seed: int) -> EpisodeResult:
|
|
194
|
+
"""Tier-0 episode using GaussianCertPlanner.
|
|
195
|
+
|
|
196
|
+
Mirror of episodes.tier0_episode; constructs a GaussianCertPlanner
|
|
197
|
+
instead of CertPlanner. Reuses planner_config and coverage_among_valid
|
|
198
|
+
from certflow.episodes without modifying that module.
|
|
199
|
+
"""
|
|
200
|
+
from certflow.episodes import planner_config # refactor-free reuse
|
|
201
|
+
|
|
202
|
+
import time
|
|
203
|
+
|
|
204
|
+
world_kwargs: dict = {
|
|
205
|
+
"noise_family": config.noise_family,
|
|
206
|
+
"noise_scale": config.noise_scale,
|
|
207
|
+
}
|
|
208
|
+
if config.kind == "bounded":
|
|
209
|
+
world_kwargs["rho"] = config.rho
|
|
210
|
+
world = grid_world(config.rows, config.cols, seed=seed, kind=config.kind, **world_kwargs)
|
|
211
|
+
|
|
212
|
+
start, goal = (0, 0), (config.rows - 1, config.cols - 1)
|
|
213
|
+
planner = GaussianCertPlanner(
|
|
214
|
+
world, start, goal, planner_config(config), use_aci=False
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
result = EpisodeResult()
|
|
218
|
+
prev_spend = planner.sense_spend
|
|
219
|
+
for _ in range(config.max_rounds):
|
|
220
|
+
t_round = planner.t
|
|
221
|
+
wall = time.perf_counter()
|
|
222
|
+
cert, sensed = planner.round()
|
|
223
|
+
wall = time.perf_counter() - wall
|
|
224
|
+
|
|
225
|
+
_, true_opt = opt(world, t_round, start, goal)
|
|
226
|
+
covered = bool(
|
|
227
|
+
cert.valid and cert.lb - 1e-9 <= true_opt <= cert.ub + 1e-9
|
|
228
|
+
)
|
|
229
|
+
certified = bool(cert.valid and cert.gap <= config.epsilon)
|
|
230
|
+
result.rounds.append(
|
|
231
|
+
RoundLog(
|
|
232
|
+
t=t_round,
|
|
233
|
+
lb=cert.lb,
|
|
234
|
+
ub=cert.ub,
|
|
235
|
+
confidence=cert.confidence,
|
|
236
|
+
opt=true_opt,
|
|
237
|
+
covered=covered,
|
|
238
|
+
certified=certified,
|
|
239
|
+
sensed_edge=sensed,
|
|
240
|
+
sense_spend=planner.sense_spend - prev_spend,
|
|
241
|
+
replan_seconds=wall,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
prev_spend = planner.sense_spend
|
|
245
|
+
|
|
246
|
+
result.sense_cost = planner.sense_spend
|
|
247
|
+
result.reached_goal = False # Tier 0 is stationary by design
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def adstar_bound(
|
|
252
|
+
beliefs,
|
|
253
|
+
graph_struct,
|
|
254
|
+
start,
|
|
255
|
+
goal,
|
|
256
|
+
w: float = 1.5,
|
|
257
|
+
cost_floor: float = 1e-3,
|
|
258
|
+
):
|
|
259
|
+
"""AD*/ARA*-semantics suboptimality interval from current point estimates.
|
|
260
|
+
|
|
261
|
+
Bounded-suboptimal search with inflation w returns a path P-hat whose
|
|
262
|
+
cost on ITS map satisfies cost(P-hat) <= w * OPT_map, i.e. the standard
|
|
263
|
+
claim OPT_map in [cost(P-hat)/w, cost(P-hat)]. That claim is sound on
|
|
264
|
+
the searcher's own (stale, noisy point-estimate) map and has no
|
|
265
|
+
mechanism for observation noise or staleness; we evaluate it as-is
|
|
266
|
+
against the TRUE drifting optimum. The comparison targets the bound
|
|
267
|
+
SEMANTICS under staleness, not the search algorithm (on these graph
|
|
268
|
+
sizes exact search is instant, so anytime behavior is not the axis).
|
|
269
|
+
"""
|
|
270
|
+
from certflow.graphcore import dijkstra
|
|
271
|
+
|
|
272
|
+
g = {
|
|
273
|
+
u: {v: max(beliefs[(u, v)].c_hat, cost_floor) for v in nbrs}
|
|
274
|
+
for u, nbrs in graph_struct.items()
|
|
275
|
+
}
|
|
276
|
+
path, c = dijkstra(g, start, goal)
|
|
277
|
+
if path is None:
|
|
278
|
+
return 0.0, float("inf")
|
|
279
|
+
return c / w, c
|