eb-optimization 0.2.4__tar.gz → 0.2.5__tar.gz
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.
- {eb_optimization-0.2.4/src/eb_optimization.egg-info → eb_optimization-0.2.5}/PKG-INFO +2 -1
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/pyproject.toml +4 -4
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/policies/__init__.py +25 -3
- eb_optimization-0.2.5/src/eb_optimization/policies/dqc_policy.py +402 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/policies/evaluation.py +59 -19
- {eb_optimization-0.2.4 → eb_optimization-0.2.5/src/eb_optimization.egg-info}/PKG-INFO +2 -1
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization.egg-info/requires.txt +1 -0
- eb_optimization-0.2.4/src/eb_optimization/policies/dqc_policy.py +0 -331
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/LICENSE +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/README.md +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/setup.cfg +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/__init__.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/_utils.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/policies/cost_ratio_policy.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/policies/ral_policy.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/policies/tau_policy.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/search/__init__.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/search/grid.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/search/kernels.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/tuning/__init__.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/tuning/cost_ratio.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/tuning/ral.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/tuning/sensitivity.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/tuning/tau.py +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization.egg-info/SOURCES.txt +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization.egg-info/dependency_links.txt +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization.egg-info/top_level.txt +0 -0
- {eb_optimization-0.2.4 → eb_optimization-0.2.5}/tests/test_public_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eb-optimization
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Electric Barometer: Optimization and tuning utilities for EB objectives and policy parameters.
|
|
5
5
|
Author-email: "Kyle Corrie (Economistician)" <kcorrie@economistician.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -21,6 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: numpy>=1.24
|
|
23
23
|
Requires-Dist: pandas>=2.0
|
|
24
|
+
Requires-Dist: eb-metrics<0.3,>=0.2
|
|
24
25
|
Provides-Extra: eb
|
|
25
26
|
Requires-Dist: eb-metrics<0.3,>=0.2; extra == "eb"
|
|
26
27
|
Requires-Dist: eb-evaluation<0.3,>=0.2; extra == "eb"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
######################################
|
|
4
4
|
[project]
|
|
5
5
|
name = "eb-optimization"
|
|
6
|
-
version = "0.2.
|
|
6
|
+
version = "0.2.5"
|
|
7
7
|
description = "Electric Barometer: Optimization and tuning utilities for EB objectives and policy parameters."
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.10"
|
|
@@ -38,11 +38,10 @@ classifiers = [
|
|
|
38
38
|
######################################
|
|
39
39
|
# Core runtime dependencies
|
|
40
40
|
######################################
|
|
41
|
-
# Keep this intentionally lean. eb-optimization should be installable even
|
|
42
|
-
# without pulling the rest of the EB stack unless users opt-in via extras.
|
|
43
41
|
dependencies = [
|
|
44
42
|
"numpy>=1.24",
|
|
45
43
|
"pandas>=2.0",
|
|
44
|
+
"eb-metrics>=0.2,<0.3",
|
|
46
45
|
]
|
|
47
46
|
|
|
48
47
|
######################################
|
|
@@ -59,7 +58,8 @@ Documentation = "https://github.com/Economistician/eb-docs"
|
|
|
59
58
|
######################################
|
|
60
59
|
[project.optional-dependencies]
|
|
61
60
|
|
|
62
|
-
#
|
|
61
|
+
# EB ecosystem deps (metrics primitives, diagnostics contracts)
|
|
62
|
+
# Use this when you want DQC diagnostics from eb-evaluation and HR@τ primitive from eb-metrics.
|
|
63
63
|
eb = [
|
|
64
64
|
# Electric Barometer compatibility band (0.2.x)
|
|
65
65
|
"eb-metrics>=0.2,<0.3",
|
|
@@ -23,8 +23,18 @@ Exported policies
|
|
|
23
23
|
- Tau (τ) tolerance governance for HR@τ
|
|
24
24
|
- Cost-ratio (R = c_u / c_o) governance for asymmetric loss
|
|
25
25
|
- RAL policy governance (readiness adjustment layer)
|
|
26
|
-
- DQC (Δ*)
|
|
27
|
-
|
|
26
|
+
- DQC (Δ*) enforcement for packed / quantized demand snapping and evaluation
|
|
27
|
+
|
|
28
|
+
DQC note
|
|
29
|
+
--------
|
|
30
|
+
DQC *detection/classification* lives in `eb-evaluation` diagnostics.
|
|
31
|
+
Preferred usage:
|
|
32
|
+
- run `eb_evaluation.diagnostics.validate_dqc(y=...)`
|
|
33
|
+
- pass that DQCResult into `enforce_snapping` / `hr_at_tau_grid_units`
|
|
34
|
+
(or policy-composed `evaluate_with_dqc_hr`)
|
|
35
|
+
|
|
36
|
+
The `compute_dqc` helper exported here is kept for backwards compatibility and
|
|
37
|
+
delegates to eb-evaluation when available.
|
|
28
38
|
"""
|
|
29
39
|
|
|
30
40
|
from __future__ import annotations
|
|
@@ -45,8 +55,16 @@ from .dqc_policy import (
|
|
|
45
55
|
snap_to_grid,
|
|
46
56
|
)
|
|
47
57
|
from .evaluation import DQCEvaluation, evaluate_with_dqc_hr
|
|
48
|
-
from .ral_policy import
|
|
58
|
+
from .ral_policy import (
|
|
59
|
+
DEFAULT_RAL_POLICY,
|
|
60
|
+
RALBands,
|
|
61
|
+
RALBandThresholds,
|
|
62
|
+
RALPolicy,
|
|
63
|
+
RALThresholdTwoBandPolicy,
|
|
64
|
+
apply_ral_policy,
|
|
65
|
+
)
|
|
49
66
|
from .tau_policy import (
|
|
67
|
+
DEFAULT_TAU_POLICY,
|
|
50
68
|
TauPolicy,
|
|
51
69
|
apply_entity_tau_policy,
|
|
52
70
|
apply_tau_policy,
|
|
@@ -57,11 +75,15 @@ __all__ = [
|
|
|
57
75
|
"DEFAULT_COST_RATIO_POLICY",
|
|
58
76
|
"DEFAULT_DQC_POLICY",
|
|
59
77
|
"DEFAULT_RAL_POLICY",
|
|
78
|
+
"DEFAULT_TAU_POLICY",
|
|
60
79
|
"CostRatioPolicy",
|
|
61
80
|
"DQCEvaluation",
|
|
62
81
|
"DQCPolicy",
|
|
63
82
|
"DQCResult",
|
|
83
|
+
"RALBandThresholds",
|
|
84
|
+
"RALBands",
|
|
64
85
|
"RALPolicy",
|
|
86
|
+
"RALThresholdTwoBandPolicy",
|
|
65
87
|
"TauPolicy",
|
|
66
88
|
"apply_cost_ratio_policy",
|
|
67
89
|
"apply_entity_cost_ratio_policy",
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Demand Quantization Compatibility (DQC) policy and snapping enforcement.
|
|
2
|
+
|
|
3
|
+
This module defines the governance logic required to correctly evaluate forecasts
|
|
4
|
+
when realized demand is quantized or unit-packed.
|
|
5
|
+
|
|
6
|
+
Important
|
|
7
|
+
---------
|
|
8
|
+
In EB, DQC itself is a *diagnostic* (classification + signals) and lives in
|
|
9
|
+
`eb-evaluation`. This module is *policy*: it consumes a DQC result and enforces
|
|
10
|
+
unit-compatibility (snapping) and grid-unit tolerance interpretation.
|
|
11
|
+
|
|
12
|
+
Key responsibilities:
|
|
13
|
+
- Enforce unit compatibility by snapping forecasts to the DQC-inferred grid.
|
|
14
|
+
- Interpret evaluation tolerances (τ) in grid units rather than raw numeric units.
|
|
15
|
+
- Provide a small set of enforcement modes suitable for governance/policy layers.
|
|
16
|
+
|
|
17
|
+
This module does not define forecasting models or metric primitives. Instead, it
|
|
18
|
+
provides policy-level wrappers that ensure evaluation metrics operate in a valid
|
|
19
|
+
unit space. When demand is packed or quantized, unsnapped evaluation can be made
|
|
20
|
+
invalid; policy decides whether to correct (snap), reject (raise), or ignore.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Sequence
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any, Literal
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# eb-optimization does not define metric primitives; it delegates to eb-metrics.
|
|
33
|
+
from eb_metrics.metrics.service import hr_at_tau as _hr_at_tau
|
|
34
|
+
except Exception: # pragma: no cover
|
|
35
|
+
_hr_at_tau = None
|
|
36
|
+
|
|
37
|
+
# Optional dependency on eb-evaluation (preferred DQC source)
|
|
38
|
+
try: # pragma: no cover - import guard
|
|
39
|
+
from eb_evaluation.diagnostics.dqc import classify_dqc as _classify_dqc
|
|
40
|
+
except Exception: # pragma: no cover - import guard
|
|
41
|
+
_classify_dqc = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
DQCClass = Literal["CONTINUOUS", "QUANTIZED", "PACKED"]
|
|
45
|
+
SnapMode = Literal["nearest", "floor", "ceil"]
|
|
46
|
+
EnforcementMode = Literal["snap", "raise", "ignore"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class DQCPolicy:
|
|
51
|
+
"""Legacy DQCPolicy container.
|
|
52
|
+
|
|
53
|
+
This policy previously owned DQC *detection* thresholds. DQC detection now lives
|
|
54
|
+
in `eb-evaluation` (diagnostics). This object remains for backwards compatibility
|
|
55
|
+
with callers that may still pass `policy=` into `compute_dqc`.
|
|
56
|
+
|
|
57
|
+
New code should:
|
|
58
|
+
- run DQC in `eb-evaluation` (e.g., `validate_dqc(y=...)`)
|
|
59
|
+
- pass the resulting DQCResult into `enforce_snapping` / `hr_at_tau_grid_units`
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Minimum positive support required to consider a DQC result meaningful.
|
|
63
|
+
min_n_pos: int = 50
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
DEFAULT_DQC_POLICY = DQCPolicy()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class DQCResult:
|
|
71
|
+
"""Output of DQC computation (policy-facing summary).
|
|
72
|
+
|
|
73
|
+
Notes
|
|
74
|
+
-----
|
|
75
|
+
- `delta_star` is the inferred grid (Δ*) in y-units.
|
|
76
|
+
- This is a lightweight summary shape used by eb-optimization policy code.
|
|
77
|
+
- When using eb-evaluation DQC, `delta_star` maps to `signals.granularity`.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
dqc_class: DQCClass
|
|
81
|
+
delta_star: float | None
|
|
82
|
+
rho_star: float | None
|
|
83
|
+
|
|
84
|
+
# Evidence / diagnostics
|
|
85
|
+
n_pos: int
|
|
86
|
+
support_size: int
|
|
87
|
+
offgrid_mad_over_delta: float | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def snap_to_grid(
|
|
91
|
+
x: np.ndarray,
|
|
92
|
+
delta: float,
|
|
93
|
+
*,
|
|
94
|
+
mode: SnapMode = "nearest",
|
|
95
|
+
nonneg: bool = True,
|
|
96
|
+
) -> np.ndarray:
|
|
97
|
+
"""Project values onto multiples of delta.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
x: Array of values to snap (may include NaNs).
|
|
101
|
+
delta: Grid size (Δ). Must be > 0.
|
|
102
|
+
mode: Nearest, floor, or ceil snapping.
|
|
103
|
+
nonneg: If True, clamps to >= 0 after snapping.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Snapped array (float dtype), preserving NaNs.
|
|
107
|
+
"""
|
|
108
|
+
if not (isinstance(delta, int | float) and delta > 0.0):
|
|
109
|
+
raise ValueError(f"delta must be > 0; got {delta!r}")
|
|
110
|
+
|
|
111
|
+
x = np.asarray(x, dtype=float)
|
|
112
|
+
out = np.full_like(x, np.nan, dtype=float)
|
|
113
|
+
|
|
114
|
+
m = np.isfinite(x)
|
|
115
|
+
if not np.any(m):
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
q = x[m] / delta
|
|
119
|
+
|
|
120
|
+
if mode == "nearest":
|
|
121
|
+
snapped = np.round(q) * delta
|
|
122
|
+
elif mode == "floor":
|
|
123
|
+
snapped = np.floor(q) * delta
|
|
124
|
+
elif mode == "ceil":
|
|
125
|
+
snapped = np.ceil(q) * delta
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(f"Unsupported mode: {mode!r}")
|
|
128
|
+
|
|
129
|
+
if nonneg:
|
|
130
|
+
snapped = np.maximum(snapped, 0.0)
|
|
131
|
+
|
|
132
|
+
out[m] = snapped
|
|
133
|
+
return out
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _type_label(obj: Any) -> str:
|
|
137
|
+
try:
|
|
138
|
+
return f"{obj.__class__.__module__}.{obj.__class__.__name__}"
|
|
139
|
+
except Exception: # pragma: no cover
|
|
140
|
+
return str(type(obj))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _get_eval_granularity(dqc: Any) -> float | None:
|
|
144
|
+
"""Extract Δ* (granularity) from an eb-evaluation DQCResult-like object."""
|
|
145
|
+
try:
|
|
146
|
+
signals = dqc.signals
|
|
147
|
+
g = signals.granularity
|
|
148
|
+
if g is None:
|
|
149
|
+
return None
|
|
150
|
+
fg = float(g)
|
|
151
|
+
return fg if fg > 0 else None
|
|
152
|
+
except Exception:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_eval_class_value(dqc: Any) -> str:
|
|
157
|
+
"""Return a normalized eb-evaluation DQC class value (lowercase) if possible."""
|
|
158
|
+
try:
|
|
159
|
+
cls = dqc.dqc_class
|
|
160
|
+
except Exception:
|
|
161
|
+
return ""
|
|
162
|
+
|
|
163
|
+
# Enum-like (preferred)
|
|
164
|
+
if hasattr(cls, "value"):
|
|
165
|
+
try:
|
|
166
|
+
return str(cls.value).lower()
|
|
167
|
+
except Exception:
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
# Fallback stringification
|
|
171
|
+
try:
|
|
172
|
+
return str(cls).lower()
|
|
173
|
+
except Exception:
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _map_eval_dqc_to_policy_dqc(dqc: Any) -> DQCResult:
|
|
178
|
+
"""Map eb-evaluation DQCResult -> eb-optimization DQCResult summary."""
|
|
179
|
+
granularity = _get_eval_granularity(dqc)
|
|
180
|
+
|
|
181
|
+
# Class mapping (eb-evaluation -> policy)
|
|
182
|
+
dqc_class: DQCClass = "CONTINUOUS"
|
|
183
|
+
cls_val = _get_eval_class_value(dqc)
|
|
184
|
+
|
|
185
|
+
if cls_val in ("quantized",):
|
|
186
|
+
dqc_class = "QUANTIZED"
|
|
187
|
+
elif cls_val in ("piecewise_packed",):
|
|
188
|
+
dqc_class = "PACKED"
|
|
189
|
+
elif cls_val in ("continuous_like", "unknown", ""):
|
|
190
|
+
dqc_class = "CONTINUOUS"
|
|
191
|
+
else:
|
|
192
|
+
# Last resort heuristics
|
|
193
|
+
if "quant" in cls_val:
|
|
194
|
+
dqc_class = "QUANTIZED"
|
|
195
|
+
elif "pack" in cls_val:
|
|
196
|
+
dqc_class = "PACKED"
|
|
197
|
+
else:
|
|
198
|
+
dqc_class = "CONTINUOUS"
|
|
199
|
+
|
|
200
|
+
rho_star: float | None = None
|
|
201
|
+
support_size: int = 0
|
|
202
|
+
offgrid_mad_over_delta: float | None = None
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
signals = dqc.signals
|
|
206
|
+
rho_star = float(signals.multiple_rate)
|
|
207
|
+
support_size = int(signals.support_size)
|
|
208
|
+
offgrid_mad = float(signals.offgrid_mad)
|
|
209
|
+
if granularity is not None and granularity > 0:
|
|
210
|
+
offgrid_mad_over_delta = offgrid_mad / granularity
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# `n_pos` is not carried explicitly by eb-evaluation DQC; callers can compute it
|
|
215
|
+
# from their realized series if needed. We set it to 0 here and optionally fill
|
|
216
|
+
# it in compute_dqc().
|
|
217
|
+
return DQCResult(
|
|
218
|
+
dqc_class=dqc_class,
|
|
219
|
+
delta_star=granularity,
|
|
220
|
+
rho_star=rho_star,
|
|
221
|
+
n_pos=0,
|
|
222
|
+
support_size=support_size,
|
|
223
|
+
offgrid_mad_over_delta=offgrid_mad_over_delta,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def compute_dqc(
|
|
228
|
+
y: Any,
|
|
229
|
+
*,
|
|
230
|
+
policy: DQCPolicy = DEFAULT_DQC_POLICY,
|
|
231
|
+
use_positive_only: bool = True,
|
|
232
|
+
) -> DQCResult:
|
|
233
|
+
"""Compute DQC over a realized demand series.
|
|
234
|
+
|
|
235
|
+
Preferred behavior:
|
|
236
|
+
- If `eb-evaluation` is available, this delegates to its DQC diagnostic and
|
|
237
|
+
maps the result into this module's lightweight DQCResult shape.
|
|
238
|
+
|
|
239
|
+
Backwards compatibility:
|
|
240
|
+
- This function remains so existing eb-optimization call sites keep working.
|
|
241
|
+
- New code should call `eb_evaluation.diagnostics.validate_dqc(y=...)` directly
|
|
242
|
+
and pass that DQCResult into policy functions here.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
y: Realized demand sequence.
|
|
246
|
+
policy: Legacy DQCPolicy (only min_n_pos is used here as a conservative guard).
|
|
247
|
+
use_positive_only: If True, only y>0 are used to compute `n_pos` for the guard.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
DQCResult summary.
|
|
251
|
+
"""
|
|
252
|
+
# Compute n_pos for the legacy "insufficient signal" guard.
|
|
253
|
+
y_arr = np.asarray(y, dtype=float)
|
|
254
|
+
y_arr = y_arr[np.isfinite(y_arr)]
|
|
255
|
+
y_pos = y_arr[y_arr > 0.0] if use_positive_only else y_arr
|
|
256
|
+
|
|
257
|
+
n_pos = int(y_pos.size)
|
|
258
|
+
support_size = int(np.unique(np.round(y_pos, 6)).size) if n_pos else 0
|
|
259
|
+
|
|
260
|
+
if n_pos < policy.min_n_pos:
|
|
261
|
+
return DQCResult(
|
|
262
|
+
dqc_class="CONTINUOUS",
|
|
263
|
+
delta_star=None,
|
|
264
|
+
rho_star=None,
|
|
265
|
+
n_pos=n_pos,
|
|
266
|
+
support_size=support_size,
|
|
267
|
+
offgrid_mad_over_delta=None,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if _classify_dqc is None:
|
|
271
|
+
raise ImportError(
|
|
272
|
+
"DQC diagnostics are not available. Install/enable `eb-evaluation` to compute DQC, "
|
|
273
|
+
"or run DQC in the evaluation layer and pass the result into policy enforcement."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Delegate to eb-evaluation (diagnostic)
|
|
277
|
+
eval_result = _classify_dqc(y=y_pos.tolist(), thresholds=None)
|
|
278
|
+
mapped = _map_eval_dqc_to_policy_dqc(eval_result)
|
|
279
|
+
|
|
280
|
+
# Fill in n_pos from the series guard (more informative than 0)
|
|
281
|
+
return DQCResult(
|
|
282
|
+
dqc_class=mapped.dqc_class,
|
|
283
|
+
delta_star=mapped.delta_star,
|
|
284
|
+
rho_star=mapped.rho_star,
|
|
285
|
+
n_pos=n_pos,
|
|
286
|
+
support_size=mapped.support_size,
|
|
287
|
+
offgrid_mad_over_delta=mapped.offgrid_mad_over_delta,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _resolve_policy_class_and_delta(dqc: Any) -> tuple[DQCClass, float | None]:
|
|
292
|
+
"""
|
|
293
|
+
Resolve a DQCResult-like input into a policy class and Δ*.
|
|
294
|
+
|
|
295
|
+
Supports:
|
|
296
|
+
- this module's DQCResult (dqc_class, delta_star)
|
|
297
|
+
- eb-evaluation DQCResult (dqc_class.value, signals.granularity)
|
|
298
|
+
"""
|
|
299
|
+
if isinstance(dqc, DQCResult):
|
|
300
|
+
return dqc.dqc_class, dqc.delta_star
|
|
301
|
+
|
|
302
|
+
delta = _get_eval_granularity(dqc)
|
|
303
|
+
cls_val = _get_eval_class_value(dqc)
|
|
304
|
+
|
|
305
|
+
# Treat eb-evaluation UNKNOWN as CONTINUOUS for enforcement (no snapping by default).
|
|
306
|
+
if cls_val == "quantized":
|
|
307
|
+
return "QUANTIZED", delta
|
|
308
|
+
if cls_val == "piecewise_packed":
|
|
309
|
+
return "PACKED", delta
|
|
310
|
+
return "CONTINUOUS", delta
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def enforce_snapping(
|
|
314
|
+
y_hat: Sequence[float] | np.ndarray,
|
|
315
|
+
*,
|
|
316
|
+
dqc: Any,
|
|
317
|
+
enforce: EnforcementMode = "snap",
|
|
318
|
+
mode: SnapMode = "nearest",
|
|
319
|
+
tol: float = 1e-6,
|
|
320
|
+
) -> np.ndarray:
|
|
321
|
+
"""Apply DQC snapping enforcement to forecasts.
|
|
322
|
+
|
|
323
|
+
Policy intent:
|
|
324
|
+
- PACKED / QUANTIZED demand => snapping is required (unit compatibility).
|
|
325
|
+
- CONTINUOUS-like demand => no snapping.
|
|
326
|
+
- UNKNOWN => no snapping unless a granularity is available (in which case
|
|
327
|
+
snapping is applied, since Δ* exists).
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
y_hat: Forecast values.
|
|
331
|
+
dqc: DQCResult-like object (preferred: eb-evaluation DQCResult) OR this module's DQCResult.
|
|
332
|
+
enforce: "snap" (default), "raise" (error if off-grid), "ignore".
|
|
333
|
+
mode: Snapping mode (if enforce == "snap").
|
|
334
|
+
tol: Absolute tolerance for off-grid checks (used when enforce == "raise").
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Forecast array, snapped or unchanged depending on class and enforcement.
|
|
338
|
+
"""
|
|
339
|
+
y_hat_arr = np.asarray(y_hat, dtype=float)
|
|
340
|
+
|
|
341
|
+
dqc_class, delta = _resolve_policy_class_and_delta(dqc)
|
|
342
|
+
|
|
343
|
+
if dqc_class == "CONTINUOUS" or delta is None:
|
|
344
|
+
return y_hat_arr
|
|
345
|
+
|
|
346
|
+
if enforce == "ignore":
|
|
347
|
+
return y_hat_arr
|
|
348
|
+
|
|
349
|
+
if enforce == "raise":
|
|
350
|
+
snapped = snap_to_grid(y_hat_arr, float(delta), mode="nearest", nonneg=True)
|
|
351
|
+
offgrid = np.isfinite(y_hat_arr) & (np.abs(y_hat_arr - snapped) > tol)
|
|
352
|
+
if bool(np.any(offgrid)):
|
|
353
|
+
raise ValueError(
|
|
354
|
+
"Forecast contains off-grid values under PACKED/QUANTIZED DQC policy. "
|
|
355
|
+
"Either snap forecasts before evaluation or use enforce='snap'."
|
|
356
|
+
)
|
|
357
|
+
return y_hat_arr
|
|
358
|
+
|
|
359
|
+
if enforce == "snap":
|
|
360
|
+
return snap_to_grid(y_hat_arr, float(delta), mode=mode, nonneg=True)
|
|
361
|
+
|
|
362
|
+
raise ValueError(f"Unsupported enforce mode: {enforce!r}")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def hr_at_tau_grid_units(
|
|
366
|
+
y_true: Sequence[float] | np.ndarray,
|
|
367
|
+
y_hat: Sequence[float] | np.ndarray,
|
|
368
|
+
*,
|
|
369
|
+
dqc: Any,
|
|
370
|
+
tau_units: float,
|
|
371
|
+
enforce: EnforcementMode = "snap",
|
|
372
|
+
snap_mode: SnapMode = "nearest",
|
|
373
|
+
) -> float:
|
|
374
|
+
"""Compute HR@τ where τ is measured in grid units (Δ*).
|
|
375
|
+
|
|
376
|
+
For PACKED / QUANTIZED demand:
|
|
377
|
+
- Forecasts are snapped per enforcement policy (default: snap).
|
|
378
|
+
- Error is evaluated in grid units: |y - yhat| / Δ* <= τ_units.
|
|
379
|
+
- We convert tau_units to y-units via tau = tau_units * Δ*.
|
|
380
|
+
|
|
381
|
+
For CONTINUOUS-like demand:
|
|
382
|
+
- tau_units is interpreted as y-units directly (caller responsibility).
|
|
383
|
+
|
|
384
|
+
Delegates to eb-metrics `hr_at_tau` after converting τ into y-units.
|
|
385
|
+
"""
|
|
386
|
+
if _hr_at_tau is None: # pragma: no cover
|
|
387
|
+
raise ImportError(
|
|
388
|
+
"eb-metrics is required to compute HR@τ (missing eb_metrics.metrics.service.hr_at_tau)."
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
y_true_arr = np.asarray(y_true, dtype=float)
|
|
392
|
+
y_hat_arr = np.asarray(y_hat, dtype=float)
|
|
393
|
+
|
|
394
|
+
dqc_class, delta = _resolve_policy_class_and_delta(dqc)
|
|
395
|
+
|
|
396
|
+
if dqc_class in ("PACKED", "QUANTIZED") and delta is not None:
|
|
397
|
+
y_hat_arr = enforce_snapping(y_hat_arr, dqc=dqc, enforce=enforce, mode=snap_mode)
|
|
398
|
+
tau = float(tau_units) * float(delta)
|
|
399
|
+
else:
|
|
400
|
+
tau = float(tau_units)
|
|
401
|
+
|
|
402
|
+
return float(_hr_at_tau(y_true_arr, y_hat_arr, tau=tau))
|
|
@@ -12,12 +12,20 @@ Primary responsibilities:
|
|
|
12
12
|
|
|
13
13
|
This module is intentionally small. It does not implement forecasting models,
|
|
14
14
|
data adapters, or tuning routines.
|
|
15
|
+
|
|
16
|
+
DQC note
|
|
17
|
+
--------
|
|
18
|
+
DQC *detection/classification* lives in `eb-evaluation` diagnostics.
|
|
19
|
+
This module consumes a DQCResult-like object (either eb-evaluation or the
|
|
20
|
+
eb-optimization summary shape) and applies enforcement + unit semantics via
|
|
21
|
+
`eb_optimization.policies.dqc_policy`.
|
|
15
22
|
"""
|
|
16
23
|
|
|
17
24
|
from __future__ import annotations
|
|
18
25
|
|
|
19
26
|
from collections.abc import Sequence
|
|
20
27
|
from dataclasses import dataclass
|
|
28
|
+
from typing import Any
|
|
21
29
|
|
|
22
30
|
import numpy as np
|
|
23
31
|
|
|
@@ -28,7 +36,7 @@ from eb_optimization.policies.dqc_policy import (
|
|
|
28
36
|
EnforcementMode,
|
|
29
37
|
SnapMode,
|
|
30
38
|
compute_dqc,
|
|
31
|
-
|
|
39
|
+
hr_at_tau_grid_units,
|
|
32
40
|
)
|
|
33
41
|
|
|
34
42
|
try:
|
|
@@ -41,7 +49,7 @@ except Exception: # pragma: no cover
|
|
|
41
49
|
class DQCEvaluation:
|
|
42
50
|
"""Result container for evaluations performed under DQC governance."""
|
|
43
51
|
|
|
44
|
-
dqc:
|
|
52
|
+
dqc: Any
|
|
45
53
|
tau_units: float
|
|
46
54
|
tau_y_units: float
|
|
47
55
|
hr_at_tau: float
|
|
@@ -52,7 +60,7 @@ def evaluate_with_dqc_hr(
|
|
|
52
60
|
y_hat: Sequence[float] | np.ndarray,
|
|
53
61
|
*,
|
|
54
62
|
tau_units: float,
|
|
55
|
-
dqc:
|
|
63
|
+
dqc: Any | None = None,
|
|
56
64
|
y_for_dqc: Sequence[float] | np.ndarray | None = None,
|
|
57
65
|
policy: DQCPolicy = DEFAULT_DQC_POLICY,
|
|
58
66
|
enforce: EnforcementMode = "snap",
|
|
@@ -62,29 +70,39 @@ def evaluate_with_dqc_hr(
|
|
|
62
70
|
"""Evaluate HR@τ under DQC governance with τ expressed in grid units.
|
|
63
71
|
|
|
64
72
|
This entrypoint enforces Demand Quantization Compatibility (DQC) rules:
|
|
65
|
-
- If demand is PACKED
|
|
66
|
-
to the governing grid size Δ*
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
- If demand is PACKED/QUANTIZED (policy taxonomy) or PIECEWISE_PACKED/QUANTIZED
|
|
74
|
+
(eb-evaluation taxonomy), forecasts are snapped to the governing grid size Δ*
|
|
75
|
+
(subject to enforcement mode).
|
|
76
|
+
- Tolerance τ is interpreted in grid units; we convert to y-units internally.
|
|
69
77
|
|
|
70
|
-
Callers may supply a precomputed
|
|
78
|
+
Callers may supply a precomputed DQC result (recommended in production), or
|
|
71
79
|
allow this function to compute DQC from realized demand via `y_for_dqc` (or
|
|
72
80
|
`y_true` if `y_for_dqc` is not provided).
|
|
73
81
|
|
|
82
|
+
Notes
|
|
83
|
+
-----
|
|
84
|
+
This function intentionally supports *duck-typed* DQC results so that an
|
|
85
|
+
upstream diagnostic layer (e.g., eb-evaluation) can supply DQC without
|
|
86
|
+
forcing eb-optimization to hard-depend on it.
|
|
87
|
+
|
|
74
88
|
Args:
|
|
75
89
|
y_true: Realized demand values.
|
|
76
90
|
y_hat: Forecast demand values.
|
|
77
91
|
tau_units: Tolerance in grid units (multiples of Δ*).
|
|
78
|
-
dqc: Optional precomputed
|
|
92
|
+
dqc: Optional precomputed DQC result. If provided, DQC is not recomputed.
|
|
93
|
+
May be:
|
|
94
|
+
- `eb_optimization.policies.dqc_policy.DQCResult` (summary shape), or
|
|
95
|
+
- `eb_evaluation.diagnostics.dqc.DQCResult` (diagnostic shape).
|
|
79
96
|
y_for_dqc: Optional series to compute DQC from (e.g., entity-level
|
|
80
97
|
historical realized demand). Ignored if `dqc` is provided.
|
|
81
|
-
policy: DQCPolicy
|
|
98
|
+
policy: Legacy DQCPolicy (used only when computing DQC locally for
|
|
99
|
+
backwards compatibility).
|
|
82
100
|
enforce: Snapping enforcement behavior ("snap", "raise", "ignore").
|
|
83
101
|
snap_mode: Snapping mode applied when enforce == "snap".
|
|
84
102
|
use_positive_only_for_dqc: If True, DQC detection uses y>0 values.
|
|
85
103
|
|
|
86
104
|
Returns:
|
|
87
|
-
DQCEvaluation including the
|
|
105
|
+
DQCEvaluation including the DQC result, τ in grid units, τ in y-units,
|
|
88
106
|
and HR@τ.
|
|
89
107
|
|
|
90
108
|
Raises:
|
|
@@ -101,18 +119,40 @@ def evaluate_with_dqc_hr(
|
|
|
101
119
|
y_src = y_true_arr if y_for_dqc is None else np.asarray(y_for_dqc, dtype=float)
|
|
102
120
|
dqc = compute_dqc(y_src, policy=policy, use_positive_only=use_positive_only_for_dqc)
|
|
103
121
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
# Compute HR via the policy helper (handles snapping + tau scaling).
|
|
123
|
+
hr = hr_at_tau_grid_units(
|
|
124
|
+
y_true_arr,
|
|
125
|
+
y_hat_arr,
|
|
126
|
+
dqc=dqc,
|
|
127
|
+
tau_units=float(tau_units),
|
|
128
|
+
enforce=enforce,
|
|
129
|
+
snap_mode=snap_mode,
|
|
130
|
+
)
|
|
110
131
|
|
|
111
|
-
|
|
132
|
+
# Derive tau_y_units for reporting:
|
|
133
|
+
# - if `dqc` is our summary type, delta_star is explicit
|
|
134
|
+
# - otherwise, try to read eb-evaluation signals.granularity
|
|
135
|
+
tau_y_units = float(tau_units)
|
|
136
|
+
delta_star: float | None = None
|
|
137
|
+
|
|
138
|
+
if isinstance(dqc, DQCResult):
|
|
139
|
+
delta_star = dqc.delta_star
|
|
140
|
+
else:
|
|
141
|
+
try:
|
|
142
|
+
signals = dqc.signals
|
|
143
|
+
g = signals.granularity
|
|
144
|
+
delta_star = float(g) if g is not None else None
|
|
145
|
+
except Exception:
|
|
146
|
+
delta_star = None
|
|
147
|
+
|
|
148
|
+
if delta_star is not None and delta_star > 0:
|
|
149
|
+
# Only scale when the diagnostic implies a meaningful grid; the policy helper
|
|
150
|
+
# already handled correctness — this is just for reporting.
|
|
151
|
+
tau_y_units = float(tau_units) * float(delta_star)
|
|
112
152
|
|
|
113
153
|
return DQCEvaluation(
|
|
114
154
|
dqc=dqc,
|
|
115
155
|
tau_units=float(tau_units),
|
|
116
156
|
tau_y_units=tau_y_units,
|
|
117
|
-
hr_at_tau=hr,
|
|
157
|
+
hr_at_tau=float(hr),
|
|
118
158
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eb-optimization
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Electric Barometer: Optimization and tuning utilities for EB objectives and policy parameters.
|
|
5
5
|
Author-email: "Kyle Corrie (Economistician)" <kcorrie@economistician.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -21,6 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: numpy>=1.24
|
|
23
23
|
Requires-Dist: pandas>=2.0
|
|
24
|
+
Requires-Dist: eb-metrics<0.3,>=0.2
|
|
24
25
|
Provides-Extra: eb
|
|
25
26
|
Requires-Dist: eb-metrics<0.3,>=0.2; extra == "eb"
|
|
26
27
|
Requires-Dist: eb-evaluation<0.3,>=0.2; extra == "eb"
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
"""Demand Quantization Compatibility (DQC) policy and snapping enforcement.
|
|
2
|
-
|
|
3
|
-
This module defines the governance logic required to correctly evaluate forecasts
|
|
4
|
-
when realized demand is quantized or unit-packed. It implements Demand Quantization
|
|
5
|
-
Compatibility (DQC) as a diagnostic and enforcement layer that detects whether demand
|
|
6
|
-
lies on a discrete grid and, if so, enforces projection ("snapping") of forecasts
|
|
7
|
-
onto that grid prior to evaluation.
|
|
8
|
-
|
|
9
|
-
Key responsibilities:
|
|
10
|
-
- Detect demand quantization structure and infer the governing grid size (Δ*).
|
|
11
|
-
- Classify demand as CONTINUOUS, QUANTIZED, or PACKED based on alignment evidence.
|
|
12
|
-
- Enforce unit compatibility by snapping forecasts to Δ* when required.
|
|
13
|
-
- Interpret evaluation tolerances (τ) in grid units rather than raw numeric units.
|
|
14
|
-
|
|
15
|
-
This module does not define forecasting models or metric primitives. Instead, it
|
|
16
|
-
provides policy-level wrappers that ensure evaluation metrics operate in a valid
|
|
17
|
-
unit space. When demand is PACKED or QUANTIZED, unsnapped evaluation is considered
|
|
18
|
-
invalid and must be corrected or rejected according to policy.
|
|
19
|
-
|
|
20
|
-
DQC is a structural compatibility gate, not a performance metric. Its purpose is to
|
|
21
|
-
prevent mathematically invalid evaluation and model comparison when demand outcomes
|
|
22
|
-
are intrinsically discrete.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from __future__ import annotations
|
|
26
|
-
|
|
27
|
-
from collections.abc import Sequence
|
|
28
|
-
from dataclasses import dataclass
|
|
29
|
-
import math
|
|
30
|
-
from typing import Literal
|
|
31
|
-
|
|
32
|
-
import numpy as np
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
# eb-optimization does not define metric primitives; it delegates to eb-metrics.
|
|
36
|
-
from eb_metrics.metrics.service import hr_at_tau as _hr_at_tau
|
|
37
|
-
except Exception: # pragma: no cover
|
|
38
|
-
_hr_at_tau = None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
DQCClass = Literal["CONTINUOUS", "QUANTIZED", "PACKED"]
|
|
42
|
-
SnapMode = Literal["nearest", "floor", "ceil"]
|
|
43
|
-
EnforcementMode = Literal["snap", "raise", "ignore"]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass(frozen=True, slots=True)
|
|
47
|
-
class DQCPolicy:
|
|
48
|
-
"""Governance thresholds and candidate grids for DQC."""
|
|
49
|
-
|
|
50
|
-
# Candidate grids (Δ) to test for alignment. Keep small and auditable.
|
|
51
|
-
candidates: tuple[float, ...] = (0.25, 0.5, 1.0, 2.0, 4.0, 6.0, 8.0, 12.0, 16.0)
|
|
52
|
-
|
|
53
|
-
# Numerical tolerance for "on-grid" checks in y-units.
|
|
54
|
-
tol: float = 1e-6
|
|
55
|
-
|
|
56
|
-
# Classification thresholds.
|
|
57
|
-
rho_packed: float = 0.99
|
|
58
|
-
rho_quantized: float = 0.90
|
|
59
|
-
|
|
60
|
-
# Packed-ness evidence thresholds.
|
|
61
|
-
support_packed_max: int = 100
|
|
62
|
-
offgrid_mad_over_delta_max: float = 0.05
|
|
63
|
-
|
|
64
|
-
# Minimum positive support required to consider the DQC result meaningful.
|
|
65
|
-
min_n_pos: int = 50
|
|
66
|
-
|
|
67
|
-
# Tie-break rule for selecting delta_star among candidates with equal rho.
|
|
68
|
-
# "max_delta" prefers the coarsest grid among ties (more conservative packing).
|
|
69
|
-
tie_break: Literal["max_delta", "min_delta"] = "max_delta"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
DEFAULT_DQC_POLICY = DQCPolicy()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@dataclass(frozen=True, slots=True)
|
|
76
|
-
class DQCResult:
|
|
77
|
-
"""Output of DQC computation."""
|
|
78
|
-
|
|
79
|
-
dqc_class: DQCClass
|
|
80
|
-
delta_star: float | None
|
|
81
|
-
rho_star: float | None
|
|
82
|
-
|
|
83
|
-
# Evidence / diagnostics
|
|
84
|
-
n_pos: int
|
|
85
|
-
support_size: int
|
|
86
|
-
offgrid_mad_over_delta: float | None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def snap_to_grid(
|
|
90
|
-
x: np.ndarray,
|
|
91
|
-
delta: float,
|
|
92
|
-
*,
|
|
93
|
-
mode: SnapMode = "nearest",
|
|
94
|
-
nonneg: bool = True,
|
|
95
|
-
) -> np.ndarray:
|
|
96
|
-
"""Project values onto multiples of delta.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
x: Array of values to snap (may include NaNs).
|
|
100
|
-
delta: Grid size (Δ). Must be > 0.
|
|
101
|
-
mode: Nearest, floor, or ceil snapping.
|
|
102
|
-
nonneg: If True, clamps to >= 0 after snapping.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
Snapped array (float dtype), preserving NaNs.
|
|
106
|
-
"""
|
|
107
|
-
if not (isinstance(delta, int | float) and delta > 0.0):
|
|
108
|
-
raise ValueError(f"delta must be > 0; got {delta!r}")
|
|
109
|
-
|
|
110
|
-
x = np.asarray(x, dtype=float)
|
|
111
|
-
out = np.full_like(x, np.nan, dtype=float)
|
|
112
|
-
|
|
113
|
-
m = np.isfinite(x)
|
|
114
|
-
if not np.any(m):
|
|
115
|
-
return out
|
|
116
|
-
|
|
117
|
-
q = x[m] / delta
|
|
118
|
-
|
|
119
|
-
if mode == "nearest":
|
|
120
|
-
snapped = np.round(q) * delta
|
|
121
|
-
elif mode == "floor":
|
|
122
|
-
snapped = np.floor(q) * delta
|
|
123
|
-
elif mode == "ceil":
|
|
124
|
-
snapped = np.ceil(q) * delta
|
|
125
|
-
else:
|
|
126
|
-
raise ValueError(f"Unsupported mode: {mode!r}")
|
|
127
|
-
|
|
128
|
-
if nonneg:
|
|
129
|
-
snapped = np.maximum(snapped, 0.0)
|
|
130
|
-
|
|
131
|
-
out[m] = snapped
|
|
132
|
-
return out
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _mad(a: np.ndarray) -> float:
|
|
136
|
-
"""Median absolute deviation around the median (robust)."""
|
|
137
|
-
med = float(np.median(a))
|
|
138
|
-
return float(np.median(np.abs(a - med)))
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def compute_dqc(
|
|
142
|
-
y: Sequence[float] | np.ndarray,
|
|
143
|
-
*,
|
|
144
|
-
policy: DQCPolicy = DEFAULT_DQC_POLICY,
|
|
145
|
-
use_positive_only: bool = True,
|
|
146
|
-
) -> DQCResult:
|
|
147
|
-
"""Compute DQC over a realized demand series.
|
|
148
|
-
|
|
149
|
-
Notes:
|
|
150
|
-
- For grid detection, positive demand values carry the most information.
|
|
151
|
-
Zeros can dominate alignment trivially, so default behavior uses positives only.
|
|
152
|
-
- Missing values (NaN) are ignored.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
y: Realized demand sequence.
|
|
156
|
-
policy: DQCPolicy thresholds and candidate grids.
|
|
157
|
-
use_positive_only: If True, only y>0 are used for detection.
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
DQCResult with class + delta_star + diagnostics.
|
|
161
|
-
"""
|
|
162
|
-
y_arr = np.asarray(y, dtype=float)
|
|
163
|
-
y_arr = y_arr[np.isfinite(y_arr)]
|
|
164
|
-
|
|
165
|
-
if use_positive_only:
|
|
166
|
-
y_arr = y_arr[y_arr > 0.0]
|
|
167
|
-
|
|
168
|
-
n_pos = int(y_arr.size)
|
|
169
|
-
if n_pos < policy.min_n_pos:
|
|
170
|
-
# Not enough signal to conclude; treat as continuous-like by default.
|
|
171
|
-
return DQCResult(
|
|
172
|
-
dqc_class="CONTINUOUS",
|
|
173
|
-
delta_star=None,
|
|
174
|
-
rho_star=None,
|
|
175
|
-
n_pos=n_pos,
|
|
176
|
-
support_size=int(np.unique(np.round(y_arr, 4)).size) if n_pos else 0,
|
|
177
|
-
offgrid_mad_over_delta=None,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
# Support size (rounded to label precision)
|
|
181
|
-
support_size = int(np.unique(np.round(y_arr, 4)).size)
|
|
182
|
-
|
|
183
|
-
candidates = np.asarray(policy.candidates, dtype=float)
|
|
184
|
-
if candidates.ndim != 1 or candidates.size == 0:
|
|
185
|
-
raise ValueError("policy.candidates must be a non-empty 1D sequence")
|
|
186
|
-
|
|
187
|
-
if np.any(candidates <= 0.0):
|
|
188
|
-
raise ValueError("All candidate deltas must be > 0")
|
|
189
|
-
|
|
190
|
-
# Residual to nearest multiple: |y - round(y/Δ)*Δ|
|
|
191
|
-
y_col = y_arr.reshape(-1, 1) # (n,1)
|
|
192
|
-
deltas = candidates.reshape(1, -1) # (1,k)
|
|
193
|
-
|
|
194
|
-
nearest = np.round(y_col / deltas) * deltas
|
|
195
|
-
resid = np.abs(y_col - nearest) # (n,k)
|
|
196
|
-
|
|
197
|
-
rho = (resid <= policy.tol).mean(axis=0) # (k,)
|
|
198
|
-
|
|
199
|
-
# Choose delta_star: maximize rho, break ties by delta direction.
|
|
200
|
-
max_rho = float(np.max(rho))
|
|
201
|
-
tie_idx = np.flatnonzero(rho == max_rho)
|
|
202
|
-
|
|
203
|
-
if tie_idx.size == 1:
|
|
204
|
-
best_idx = int(tie_idx[0])
|
|
205
|
-
else:
|
|
206
|
-
if policy.tie_break == "max_delta":
|
|
207
|
-
best_idx = int(tie_idx[np.argmax(candidates[tie_idx])])
|
|
208
|
-
elif policy.tie_break == "min_delta":
|
|
209
|
-
best_idx = int(tie_idx[np.argmin(candidates[tie_idx])])
|
|
210
|
-
else:
|
|
211
|
-
raise ValueError(f"Unsupported tie_break: {policy.tie_break!r}")
|
|
212
|
-
|
|
213
|
-
delta_star = float(candidates[best_idx])
|
|
214
|
-
rho_star = float(rho[best_idx])
|
|
215
|
-
|
|
216
|
-
resid_star = resid[:, best_idx]
|
|
217
|
-
mad = _mad(resid_star)
|
|
218
|
-
offgrid_mad_over_delta = float(mad / delta_star) if delta_star > 0 else math.nan
|
|
219
|
-
|
|
220
|
-
# Classification
|
|
221
|
-
if (
|
|
222
|
-
rho_star >= policy.rho_packed
|
|
223
|
-
and offgrid_mad_over_delta <= policy.offgrid_mad_over_delta_max
|
|
224
|
-
and support_size <= policy.support_packed_max
|
|
225
|
-
):
|
|
226
|
-
dqc_class: DQCClass = "PACKED"
|
|
227
|
-
elif rho_star >= policy.rho_quantized:
|
|
228
|
-
dqc_class = "QUANTIZED"
|
|
229
|
-
else:
|
|
230
|
-
dqc_class = "CONTINUOUS"
|
|
231
|
-
|
|
232
|
-
return DQCResult(
|
|
233
|
-
dqc_class=dqc_class,
|
|
234
|
-
delta_star=delta_star,
|
|
235
|
-
rho_star=rho_star,
|
|
236
|
-
n_pos=n_pos,
|
|
237
|
-
support_size=support_size,
|
|
238
|
-
offgrid_mad_over_delta=offgrid_mad_over_delta,
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def enforce_snapping(
|
|
243
|
-
y_hat: Sequence[float] | np.ndarray,
|
|
244
|
-
*,
|
|
245
|
-
dqc: DQCResult,
|
|
246
|
-
enforce: EnforcementMode = "snap",
|
|
247
|
-
mode: SnapMode = "nearest",
|
|
248
|
-
) -> np.ndarray:
|
|
249
|
-
"""Apply DQC snapping enforcement to forecasts.
|
|
250
|
-
|
|
251
|
-
Policy intent:
|
|
252
|
-
- PACKED demand => snapping is required (unit compatibility).
|
|
253
|
-
- QUANTIZED demand => snapping is strongly recommended; default behavior snaps.
|
|
254
|
-
- CONTINUOUS => no snapping.
|
|
255
|
-
|
|
256
|
-
Args:
|
|
257
|
-
y_hat: Forecast values.
|
|
258
|
-
dqc: DQCResult for the relevant entity/window.
|
|
259
|
-
enforce: "snap" (default), "raise" (error if off-grid), "ignore".
|
|
260
|
-
mode: Snapping mode (if enforce == "snap").
|
|
261
|
-
|
|
262
|
-
Returns:
|
|
263
|
-
Forecast array, snapped or unchanged depending on class and enforcement.
|
|
264
|
-
"""
|
|
265
|
-
y_hat_arr = np.asarray(y_hat, dtype=float)
|
|
266
|
-
|
|
267
|
-
if dqc.dqc_class == "CONTINUOUS" or dqc.delta_star is None:
|
|
268
|
-
return y_hat_arr
|
|
269
|
-
|
|
270
|
-
if enforce == "ignore":
|
|
271
|
-
return y_hat_arr
|
|
272
|
-
|
|
273
|
-
delta = float(dqc.delta_star)
|
|
274
|
-
|
|
275
|
-
if enforce == "raise":
|
|
276
|
-
snapped = snap_to_grid(y_hat_arr, delta, mode="nearest", nonneg=True)
|
|
277
|
-
offgrid = np.isfinite(y_hat_arr) & (np.abs(y_hat_arr - snapped) > DEFAULT_DQC_POLICY.tol)
|
|
278
|
-
if bool(np.any(offgrid)):
|
|
279
|
-
raise ValueError("Forecast contains off-grid values under PACKED/QUANTIZED DQC policy.")
|
|
280
|
-
return y_hat_arr
|
|
281
|
-
|
|
282
|
-
if enforce == "snap":
|
|
283
|
-
return snap_to_grid(y_hat_arr, delta, mode=mode, nonneg=True)
|
|
284
|
-
|
|
285
|
-
raise ValueError(f"Unsupported enforce mode: {enforce!r}")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def hr_at_tau_grid_units(
|
|
289
|
-
y_true: Sequence[float] | np.ndarray,
|
|
290
|
-
y_hat: Sequence[float] | np.ndarray,
|
|
291
|
-
*,
|
|
292
|
-
dqc: DQCResult,
|
|
293
|
-
tau_units: float,
|
|
294
|
-
enforce: EnforcementMode = "snap",
|
|
295
|
-
snap_mode: SnapMode = "nearest",
|
|
296
|
-
) -> float:
|
|
297
|
-
"""Compute HR@τ where τ is measured in grid units (Δ*).
|
|
298
|
-
|
|
299
|
-
For PACKED / QUANTIZED demand:
|
|
300
|
-
- Forecasts are snapped per enforcement policy (default: snap).
|
|
301
|
-
- Error is evaluated in grid units: |y - yhat| / Δ* <= τ_units.
|
|
302
|
-
|
|
303
|
-
Delegates to eb-metrics `hr_at_tau` after converting τ into y-units.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
y_true: Realized demand.
|
|
307
|
-
y_hat: Forecast demand.
|
|
308
|
-
dqc: DQCResult for the relevant entity/window.
|
|
309
|
-
tau_units: Tolerance in grid units (Δ* multiples).
|
|
310
|
-
enforce: Snapping enforcement behavior.
|
|
311
|
-
snap_mode: Snapping mode for y_hat when enforce == "snap".
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
Hit rate in [0,1].
|
|
315
|
-
"""
|
|
316
|
-
if _hr_at_tau is None: # pragma: no cover
|
|
317
|
-
raise ImportError(
|
|
318
|
-
"eb-metrics is required to compute HR@τ (missing eb_metrics.metrics.service.hr_at_tau)."
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
y_true_arr = np.asarray(y_true, dtype=float)
|
|
322
|
-
y_hat_arr = np.asarray(y_hat, dtype=float)
|
|
323
|
-
|
|
324
|
-
if dqc.dqc_class in ("PACKED", "QUANTIZED") and dqc.delta_star is not None:
|
|
325
|
-
y_hat_arr = enforce_snapping(y_hat_arr, dqc=dqc, enforce=enforce, mode=snap_mode)
|
|
326
|
-
tau = float(tau_units) * float(dqc.delta_star)
|
|
327
|
-
else:
|
|
328
|
-
# Continuous-like: interpret tau_units as y-units directly (caller responsibility).
|
|
329
|
-
tau = float(tau_units)
|
|
330
|
-
|
|
331
|
-
return float(_hr_at_tau(y_true_arr, y_hat_arr, tau=tau))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization/policies/cost_ratio_policy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{eb_optimization-0.2.4 → eb_optimization-0.2.5}/src/eb_optimization.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|