gen-f2d1187e333bf403 / generator.py
ShinyUser's picture
cascade generator submission: epoch1211-cleanroom-fullclock-zero-v1
721b1d0 verified
Raw
History Blame Contribute Delete
28.6 kB
"""Score-blind, full-clock zero-overlay clean-room generator for Cascade.
Only the public Cascade contract and the independently authored clean-room
mathematical family were used. Generation is CPU-only and deterministic.
"""
from __future__ import annotations
import json
from collections.abc import Iterator
from pathlib import Path
import numpy as np
from scipy.signal import lfilter
from cascade.interface import DataGenerator
TAU = 2.0 * np.pi
FAMILY_COUNT = 16
_LANE_DOMAIN = 0xC1EA_1211
def _causal_sanitize(
values: np.ndarray, *, integer: bool = False, cap: float = 1.0e12
) -> np.ndarray:
"""Pointwise finite projection; no suffix statistic can revise a prefix."""
out = np.asarray(values, dtype=np.float64)
out = np.nan_to_num(out, nan=0.0, posinf=cap, neginf=-cap)
out = np.clip(out, -cap, cap)
if integer:
out = np.rint(out)
return np.ascontiguousarray(out, dtype=np.float64)
def _rate_vector(obj: object, fallback: tuple[float, ...]) -> np.ndarray:
keys = ("stock_flow", "market", "weather_load", "weekly_count")
if not isinstance(obj, dict):
return np.asarray(fallback, dtype=np.float64)
return np.asarray(
[float(obj.get(key, fallback[i])) for i, key in enumerate(keys)],
dtype=np.float64,
)
def _ar1_fixed(
forcing: np.ndarray, phi: float, initial: np.ndarray
) -> np.ndarray:
"""Batch AR(1), with each row's first output fixed to ``initial``."""
x = np.asarray(forcing, dtype=np.float64)
if x.shape[0] == 0:
return np.empty_like(x)
first = np.asarray(initial, dtype=np.float64).reshape(-1)
zi = (first - x[:, 0])[:, None]
return lfilter((1.0,), (1.0, -float(phi)), x, axis=1, zi=zi)[0]
def _ar1_rows(
forcing: np.ndarray, phi: np.ndarray, initial: np.ndarray
) -> np.ndarray:
"""Independent variable-coefficient AR(1) rows using compiled filters."""
x = np.asarray(forcing, dtype=np.float64)
out = np.empty_like(x)
for row in range(x.shape[0]):
p = float(phi[row])
zi = np.asarray([float(initial[row]) - x[row, 0]])
out[row] = lfilter((1.0,), (1.0, -p), x[row], zi=zi)[0]
return out
def _ar2_rows(
forcing: np.ndarray,
a1: np.ndarray,
a2: np.ndarray,
initial0: np.ndarray,
initial1: np.ndarray,
) -> np.ndarray:
"""Independent stable AR(2) rows using compiled filters."""
x = np.asarray(forcing, dtype=np.float64)
out = np.empty_like(x)
for row in range(x.shape[0]):
c1, c2 = float(a1[row]), float(a2[row])
# Direct-form-II state chosen so y[0:2] equal the supplied initials.
zi = np.asarray(
[
float(initial0[row]) - x[row, 0],
float(initial1[row]) - x[row, 1] - c1 * float(initial0[row]),
]
)
out[row] = lfilter((1.0,), (1.0, -c1, -c2), x[row], zi=zi)[0]
return out
class Generator(DataGenerator):
"""4096-point clean-room family generated in bounded vectorized chunks."""
def __init__(self, config_dir: str, *, seed: int) -> None:
path = Path(config_dir) / "config.json"
cfg = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {}
self._seed = int(seed) % (1 << 64)
self._length = int(cfg.get("length", 4096))
if self._length != 4096:
raise ValueError("cleanroom generator requires length=4096")
chunk_size = cfg.get("chunk_size", 2048)
if (
not isinstance(chunk_size, int)
or isinstance(chunk_size, bool)
or not 1 <= chunk_size <= 4096
):
raise ValueError("chunk_size must be an integer in [1, 4096]")
self._chunk_size = chunk_size
requested_cap = float(cfg.get("max_abs", 1.0e12))
if not np.isfinite(requested_cap) or requested_cap <= 0.0:
raise ValueError("max_abs must be finite and positive")
self._cap = min(max(requested_cap, 1.0), 1.0e15)
self._rate_start = _rate_vector(
cfg.get("source_rate_start"), (0.0, 0.0, 0.0, 0.0)
)
self._rate_final = _rate_vector(
cfg.get("source_rate_final"), (0.0, 0.0, 0.0, 0.0)
)
if (
not np.isfinite(self._rate_start).all()
or not np.isfinite(self._rate_final).all()
or np.any(self._rate_start < 0.0)
or np.any(self._rate_final < 0.0)
or self._rate_start.sum() > 1.0
or self._rate_final.sum() > 1.0
):
raise ValueError("source rates must be finite, nonnegative, and sum to <= 1")
self._expected_fraction = float(
cfg.get("expected_consumption_fraction", 0.65)
)
if (
not np.isfinite(self._expected_fraction)
or not 0.0 < self._expected_fraction <= 1.0
):
raise ValueError(
"expected_consumption_fraction must be finite and in (0, 1]"
)
stream_min = cfg.get("source_progress_stream_min_length", 64)
if (
not isinstance(stream_min, int)
or isinstance(stream_min, bool)
or not 0 < stream_min <= self._length
):
raise ValueError(
"source_progress_stream_min_length must be an integer in [1, length]"
)
self._source_stream_min_length = stream_min
self._market_activity = float(
cfg.get("market_activity_probability", 0.35)
)
if (
not np.isfinite(self._market_activity)
or not 0.0 <= self._market_activity <= 1.0
):
raise ValueError(
"market_activity_probability must be finite and in [0, 1]"
)
self._t = np.arange(self._length, dtype=np.float64)
self._family_early = np.asarray(
[14, 10, 7, 7, 7, 5, 4, 6, 8, 7, 8, 5, 5, 4, 2, 1],
dtype=np.float64,
)
final = np.asarray(
[6, 7, 6, 7, 8, 7, 7, 6, 6, 6, 5, 5, 5, 6, 6, 7],
dtype=np.float64,
)
self._family_delta = final - self._family_early
@property
def name(self) -> str:
return "cleanroom-fullclock-zero-v1"
def _rng(self, tag: int) -> np.random.Generator:
"""A stable tagged lane; called only at a generate-call boundary."""
lo = self._seed & 0xFFFF_FFFF
hi = self._seed >> 32
return np.random.default_rng(
np.random.SeedSequence([lo, hi, _LANE_DOMAIN, int(tag)])
)
def _progress(self, row: int, total: int) -> float:
"""Base-family clock over actually consumable fixed-length rows."""
return self._source_progress(row, total)
def _progress_chunk(self, start: int, stop: int, total: int) -> np.ndarray:
return self._source_progress_chunk(start, stop, total)
def _estimated_consumed_rows(self, total: int) -> int:
total = int(total)
if total <= 0:
return 0
stream_slots = max(total - 2, 0)
if stream_slots == 0:
return 1
points_upper = stream_slots * self._source_stream_min_length
return max(1, (points_upper + self._length - 1) // self._length)
def _source_progress(self, row: int, total: int) -> float:
if row <= 0 or total <= 1:
return 0.0
estimated = self._estimated_consumed_rows(total)
if estimated <= 1:
return 1.0
denominator = float(estimated - 1) * self._expected_fraction
return float(np.clip(row / denominator, 0.0, 1.0))
def _source_progress_chunk(
self, start: int, stop: int, total: int
) -> np.ndarray:
rows = np.arange(start, stop, dtype=np.float64)
if total <= 1:
return np.zeros(stop - start, dtype=np.float64)
estimated = self._estimated_consumed_rows(total)
if estimated <= 1:
out = np.ones(stop - start, dtype=np.float64)
out[rows <= 0.0] = 0.0
return out
denominator = float(estimated - 1) * self._expected_fraction
return np.clip(rows / denominator, 0.0, 1.0)
def _source_rates(self, row: int, total: int) -> np.ndarray:
q = self._source_progress(row, total)
return self._rate_start + q * (self._rate_final - self._rate_start)
def _select_families(
self, rng: np.random.Generator, progress: np.ndarray
) -> np.ndarray:
weights = (
self._family_early[None, :]
+ progress[:, None] * self._family_delta[None, :]
)
targets = rng.random(progress.size) * weights.sum(axis=1)
return np.sum(targets[:, None] >= np.cumsum(weights, axis=1), axis=1)
def _select_sources(
self,
rng: np.random.Generator,
start: int,
stop: int,
total: int,
) -> np.ndarray:
q = self._source_progress_chunk(start, stop, total)
rates = self._rate_start + q[:, None] * (
self._rate_final - self._rate_start
)
targets = rng.random(stop - start)
cumulative = np.cumsum(rates, axis=1)
kinds = np.sum(targets[:, None] >= cumulative, axis=1)
kinds[targets >= cumulative[:, -1]] = -1
return kinds
def _sanitize_chunk(
self, values: np.ndarray, integer_rows: np.ndarray
) -> np.ndarray:
np.nan_to_num(
values,
copy=False,
nan=0.0,
posinf=self._cap,
neginf=-self._cap,
)
np.clip(values, -self._cap, self._cap, out=values)
if np.any(integer_rows):
values[integer_rows] = np.rint(values[integer_rows])
return np.ascontiguousarray(values, dtype=np.float64)
def generate(self, n_series: int) -> Iterator[np.ndarray]:
n = int(n_series)
if n < 0:
raise ValueError("n_series must be nonnegative")
# These stable lanes are re-created once per call. Base generation is
# unconditional, so source toggles cannot perturb any base row.
choice_rng = self._rng(0x0100)
family_rngs = tuple(self._rng(0x0200 + family) for family in range(16))
selector_rng = self._rng(0x0300)
source_rngs = tuple(self._rng(0x0400 + kind) for kind in range(4))
for start in range(0, n, self._chunk_size):
stop = min(start + self._chunk_size, n)
progress = self._progress_chunk(start, stop, n)
families = self._select_families(choice_rng, progress)
values = np.empty((stop - start, self._length), dtype=np.float64)
integers = np.zeros(stop - start, dtype=bool)
for family in range(FAMILY_COUNT):
positions = np.flatnonzero(families == family)
if positions.size:
block, is_integer = self._family_batch(
family_rngs[family], family, positions.size
)
values[positions] = block
if is_integer:
integers[positions] = True
source_kinds = self._select_sources(selector_rng, start, stop, n)
for kind in range(4):
positions = np.flatnonzero(source_kinds == kind)
if positions.size:
block, is_integer = self._source_batch(
source_rngs[kind], kind, positions.size
)
values[positions] = block
integers[positions] = is_integer
values = self._sanitize_chunk(values, integers)
for row in values:
yield row
def _family_batch(
self, rng: np.random.Generator, family: int, count: int
) -> tuple[np.ndarray, bool]:
k, n, t = int(count), self._length, self._t
if family == 0:
y = rng.normal(0.0, 2.0, k)[:, None]
y = y + rng.normal(0.0, 0.002, k)[:, None] * t
components = rng.integers(1, 4, k)
period_values = np.asarray([7, 12, 24, 48, 52, 168, 365])
for component in range(3):
period = rng.choice(period_values, k)
amplitude = rng.uniform(0.2, 4.0, k)
phase = rng.uniform(0.0, TAU, k)
active = (components > component).astype(np.float64)
y += (active * amplitude)[:, None] * np.sin(
TAU * t / period[:, None] + phase[:, None]
)
sigma = rng.uniform(0.05, 0.8, k)
eps = rng.normal(0.0, sigma[:, None], (k, n))
phi = rng.uniform(-0.35, 0.92, k)
return y + _ar1_rows(eps, phi, eps[:, 0]), False
if family == 1:
initial = rng.normal(0.0, 5.0, k)
first_slope = rng.normal(0.0, 0.012, k)
changes = rng.integers(2, 8, k)
raw_cuts = np.sort(rng.integers(128, n - 64, (k, 7)), axis=1)
new_slopes = rng.normal(0.0, 0.025, (k, 7))
new_jumps = rng.normal(0.0, 3.0, (k, 7))
slopes = np.empty((k, n), dtype=np.float64)
jumps = np.zeros((k, n), dtype=np.float64)
for row in range(k):
cursor = 1
slope = first_slope[row]
for change in range(int(changes[row])):
cut = int(raw_cuts[row, change])
slopes[row, cursor:cut] = slope
slope = new_slopes[row, change]
slopes[row, cut] = slope
jumps[row, cut] += new_jumps[row, change]
cursor = cut + 1
slopes[row, cursor:] = slope
noise = rng.normal(0.0, 0.15, (k, n))
y = np.empty((k, n), dtype=np.float64)
y[:, 0] = initial
y[:, 1:] = initial[:, None] + np.cumsum(
slopes[:, 1:] + jumps[:, 1:] + noise[:, 1:], axis=1
)
return y, False
if family == 2:
drift = rng.normal(0.0, 0.0003, k)
sigma = rng.uniform(0.003, 0.025, k)
shocks = rng.normal(drift[:, None], sigma[:, None], (k, n))
log_y = np.log(rng.uniform(0.2, 200.0, k))[:, None]
log_y = log_y + np.cumsum(shocks, axis=1)
amplitude = rng.uniform(0.02, 0.3, k)
period = rng.choice(np.asarray([7, 24, 52, 365]), k)
phase = rng.uniform(0.0, TAU, k)
seasonal = amplitude[:, None] * np.sin(
TAU * t / period[:, None] + phase[:, None]
)
return np.exp(np.clip(log_y + seasonal, -20.0, 25.0)), False
if family == 3:
radius = rng.uniform(0.35, 0.985, k)
angle = rng.uniform(0.08, 1.45, k)
a1 = 2.0 * radius * np.cos(angle)
a2 = -(radius**2)
sigma = rng.uniform(0.1, 1.2, k)
eps = rng.normal(0.0, sigma[:, None], (k, n))
y = _ar2_rows(eps, a1, a2, eps[:, 0], eps[:, 1])
return y + rng.normal(0.0, 3.0, k)[:, None], False
if family == 4:
h_initial = rng.normal(-2.0, 0.3, k)
h_eps = rng.normal(0.0, 0.12, (k, n))
h = -2.0 + _ar1_fixed(h_eps, 0.975, h_initial + 2.0)
degrees = rng.uniform(2.5, 8.0, k)
heavy = np.empty((k, n), dtype=np.float64)
for row in range(k):
heavy[row] = rng.standard_t(float(degrees[row]), n)
base = rng.normal(0.0, 3.0, k)
return base[:, None] + np.cumsum(
np.exp(0.5 * h) * heavy, axis=1
), False
if family == 5:
sigma = rng.uniform(0.1, 0.9, k)
eps = rng.normal(0.0, sigma[:, None], (k, n))
threshold = rng.normal(0.0, 0.5, k)
low = rng.uniform(-0.5, 0.4, k)
high = rng.uniform(0.45, 0.94, k)
y = np.empty((k, n), dtype=np.float64)
previous = eps[:, 0].copy()
y[:, 0] = previous
for index in range(1, n):
phi = np.where(previous > threshold, high, low)
previous = phi * previous + eps[:, index]
y[:, index] = previous
return y, False
if family == 6:
y = np.empty((k, n), dtype=np.float64)
previous = rng.uniform(0.1, 0.9, k)
rate = rng.uniform(3.58, 3.98, k)
y[:, 0] = previous
for index in range(1, n):
previous = rate * previous * (1.0 - previous)
y[:, index] = previous
scale = rng.uniform(0.5, 20.0, k)
return (
scale[:, None] * y + rng.normal(0.0, 0.03, (k, n)),
False,
)
if family == 7:
y = np.zeros((k, n), dtype=np.float64)
components = rng.integers(3, 15, k)
for component in range(14):
period = np.exp(rng.uniform(np.log(5.0), np.log(900.0), k))
amplitude = rng.uniform(0.15, 3.0, k) / np.sqrt(component + 1.0)
phase = rng.uniform(0.0, TAU, k)
active = (components > component).astype(np.float64)
y += (active * amplitude)[:, None] * np.sin(
TAU * t / period[:, None] + phase[:, None]
)
sigma = rng.uniform(0.03, 0.5, k)
return y + rng.normal(0.0, sigma[:, None], (k, n)), False
if family == 8:
innovation = rng.normal(0.0, 1.0, (k, n))
cs = np.concatenate(
(np.zeros((k, 1), dtype=np.float64), np.cumsum(innovation, axis=1)),
axis=1,
)
y = np.zeros((k, n), dtype=np.float64)
indices = np.arange(n)
for window in (2, 4, 8, 16, 32, 64, 128, 256):
left = np.maximum(indices + 1 - window, 0)
widths = np.sqrt(indices + 1 - left)
exponent = rng.uniform(0.08, 0.28, k)
y += (
(cs[:, indices + 1] - cs[:, left])
/ widths[None, :]
/ (window**exponent)[:, None]
)
return y, False
if family == 9:
mean = rng.normal(0.0, 8.0, k)
h_initial = rng.normal(-1.5, 0.2, k)
theta = rng.uniform(0.005, 0.12, k)
innovations = rng.standard_normal((k, n - 1, 2))
h_forcing = np.empty((k, n), dtype=np.float64)
h_forcing[:, 0] = h_initial + 1.5
h_forcing[:, 1:] = 0.1 * innovations[:, :, 0]
h = -1.5 + _ar1_fixed(h_forcing, 0.96, h_initial + 1.5)
forcing = np.zeros((k, n), dtype=np.float64)
forcing[:, 1:] = np.exp(0.5 * h[:, 1:]) * innovations[:, :, 1]
centered = _ar1_rows(
forcing, 1.0 - theta, np.zeros(k, dtype=np.float64)
)
return mean[:, None] + centered, False
if family == 10:
day = rng.choice(np.asarray([24.0, 48.0, 96.0]), k)
baseline = rng.uniform(5.0, 100.0, k)
daily = rng.uniform(1.0, 20.0, k)[:, None] * np.sin(
TAU * t / day[:, None] + rng.uniform(0.0, TAU, k)[:, None]
)
weekly = rng.uniform(0.2, 8.0, k)[:, None] * np.sin(
TAU * t / (7.0 * day[:, None])
+ rng.uniform(0.0, TAU, k)[:, None]
)
slow = rng.uniform(0.0, 10.0, k)[:, None] * np.sin(
TAU * t / (30.0 * day[:, None])
+ rng.uniform(0.0, TAU, k)[:, None]
)
y = (
baseline[:, None]
+ daily
+ weekly
+ slow
+ rng.normal(0.0, 0.8, (k, n))
)
return np.clip(y, 0.0, 250.0), False
if family == 11:
period = rng.choice(np.asarray([7.0, 12.0, 24.0, 52.0, 168.0]), k)
log_rate = rng.uniform(-0.5, 4.0, k)[:, None]
log_rate = log_rate + rng.uniform(0.1, 1.0, k)[:, None] * np.sin(
TAU * t / period[:, None] + rng.uniform(0.0, TAU, k)[:, None]
)
return rng.poisson(np.exp(np.clip(log_rate, -5.0, 8.0))).astype(
np.float64
), True
if family == 12:
probability = rng.uniform(0.015, 0.25, k)
active = rng.random((k, n)) < probability[:, None]
shape = rng.integers(1, 8, k)
probability_nb = rng.uniform(0.15, 0.8, k)
amounts = np.empty((k, n), dtype=np.float64)
for row in range(k):
amounts[row] = rng.negative_binomial(
int(shape[row]), float(probability_nb[row]), n
)
return active * amounts, True
if family == 13:
y = rng.normal(
0.0, rng.uniform(0.02, 0.4, k)[:, None], (k, n)
)
pulse_count = rng.integers(3, 35, k)
for row in range(k):
positions = rng.choice(
n, size=int(pulse_count[row]), replace=False
)
widths = rng.integers(1, 40, int(pulse_count[row]))
amplitudes = rng.normal(0.0, 8.0, int(pulse_count[row]))
for position, width, amplitude in zip(
positions, widths, amplitudes, strict=True
):
stop = min(n, int(position + width))
y[row, position:stop] += amplitude * np.exp(
-np.arange(stop - position) / max(width / 4.0, 1.0)
)
return y, False
if family == 14:
y = np.empty((k, n), dtype=np.float64)
previous = rng.normal(0.0, 1.0, k)
gain = rng.uniform(0.5, 1.8, k)
memory = rng.uniform(-0.45, 0.88, k)
noise = rng.normal(0.0, 0.18, (k, n - 1))
y[:, 0] = previous
for index in range(1, n):
previous = (
memory * previous
+ gain * np.tanh(previous)
+ noise[:, index - 1]
)
y[:, index] = previous
return y, False
slope = rng.normal(0.0, 0.004, k)[:, None] + np.cumsum(
rng.normal(0.0, 0.00012, (k, n)), axis=1
)
trend = rng.normal(0.0, 2.0, k)[:, None] + np.cumsum(slope, axis=1)
amplitude = np.maximum(
0.05,
rng.uniform(0.3, 3.0, k)[:, None]
+ np.cumsum(rng.normal(0.0, 0.002, (k, n)), axis=1),
)
period = rng.choice(np.asarray([12.0, 24.0, 52.0, 168.0]), k)
season = amplitude * np.sin(
TAU * t / period[:, None] + rng.uniform(0.0, TAU, k)[:, None]
)
return trend + season + rng.normal(0.0, 0.25, (k, n)), False
def _source_batch(
self, rng: np.random.Generator, kind: int, count: int
) -> tuple[np.ndarray, bool | np.ndarray]:
k, n, t = int(count), self._length, self._t
if kind == 0:
inventory = rng.random(k) < 0.5
out = np.empty((k, n), dtype=np.float64)
indices = np.flatnonzero(inventory)
if indices.size:
arrivals = rng.poisson(
rng.uniform(0.2, 8.0, indices.size)[:, None],
(indices.size, n),
)
demand = rng.poisson(
rng.uniform(0.15, 7.5, indices.size)[:, None],
(indices.size, n),
)
initial = rng.integers(5, 100, indices.size).astype(np.float64)
unconstrained = np.empty((indices.size, n), dtype=np.float64)
unconstrained[:, 0] = initial
unconstrained[:, 1:] = initial[:, None] + np.cumsum(
arrivals[:, 1:] - demand[:, 1:], axis=1
)
floor = np.minimum(
np.minimum.accumulate(unconstrained, axis=1), 0.0
)
out[indices] = unconstrained - floor
indices = np.flatnonzero(~inventory)
if indices.size:
level = rng.uniform(-1.0, 3.0, indices.size)
amplitude = rng.uniform(0.1, 0.8, indices.size)
period = rng.choice(np.asarray([7.0, 24.0, 168.0]), indices.size)
intensity = np.exp(
level[:, None]
+ amplitude[:, None] * np.sin(TAU * t / period[:, None])
)
out[indices] = rng.poisson(intensity)
return out, True
if kind == 1:
switches = rng.random((k, n)) < 0.006
proposals = rng.integers(0, 3, (k, n))
indices = np.arange(n)
recent = np.maximum.accumulate(
np.where(switches, indices[None, :], -1), axis=1
)
rows = np.arange(k)[:, None]
regimes = np.where(
recent >= 0, proposals[rows, np.maximum(recent, 0)], 0
)
return_noise = rng.normal(0.0, 1.0, (k, n))
innovations = rng.normal(0.0, 1.0, (k, n))
means = np.asarray((-0.0002, 0.0, 0.00025))[regimes]
scales = np.asarray((0.55, 1.0, 2.2))[regimes]
variance = rng.uniform(1.0e-5, 4.0e-4, k)
previous_return = np.zeros(k, dtype=np.float64)
returns = np.empty((k, n), dtype=np.float64)
for index in range(n):
variance = (
2.0e-6 + 0.08 * previous_return**2 + 0.90 * variance
)
previous_return = (
means[:, index]
+ scales[:, index]
* np.sqrt(np.minimum(variance, 0.01))
* return_noise[:, index]
)
returns[:, index] = previous_return
volume = rng.poisson(
20.0 + 5000.0 * np.abs(returns) + 15.0 * np.abs(innovations)
).astype(np.float64)
activity = rng.random(k) < self._market_activity
start_price = rng.uniform(5.0, 300.0, k)
price = start_price[:, None] * np.exp(
np.clip(np.cumsum(returns, axis=1), -8.0, 8.0)
)
# Activity/volume rows are integer-valued while price rows are not;
# return the row mask so grouped generation preserves that fact.
return np.where(activity[:, None], volume, price), activity
if kind == 2:
slots = rng.choice(np.asarray([24.0, 48.0, 96.0]), k)
initial = rng.uniform(5.0, 25.0, k)
jitter = rng.uniform(-0.01, 0.01, (k, n))
equilibrium = 15.0 + 9.0 * np.sin(
TAU * t / slots[:, None] + jitter
)
forcing = 0.04 * equilibrium + rng.normal(0.0, 0.25, (k, n))
temperature = _ar1_fixed(
forcing, 0.96, 0.96 * initial + forcing[:, 0]
)
comfort = rng.uniform(16.0, 22.0, k)
load = rng.uniform(10.0, 80.0, k)[:, None]
load = load + rng.uniform(0.5, 3.0, k)[:, None] * np.abs(
temperature - comfort[:, None]
)
load += rng.uniform(1.0, 15.0, k)[:, None] * (
1.0 + np.sin(TAU * t / slots[:, None] - 1.2)
)
load += rng.uniform(0.0, 8.0, k)[:, None] * np.sin(
TAU * t / (7.0 * slots[:, None])
)
return np.clip(
load + rng.normal(0.0, 1.0, (k, n)), 0.0, 500.0
), False
weeks = rng.choice(np.asarray([7, 14, 168]), k)
out = np.empty((k, n), dtype=np.float64)
for row in range(k):
week = int(weeks[row])
weekday = rng.uniform(0.25, 1.75, week)
baseline = rng.uniform(0.05, 12.0)
events = rng.random(n) < 0.004
proposals = rng.uniform(1.5, 5.0, n)
indices = np.arange(n)
recent = np.maximum.accumulate(np.where(events, indices, -1))
source = np.maximum(recent, 0)
age = indices - recent + 1
promotion = 1.0 + np.where(
recent >= 0,
(proposals[source] - 1.0) * np.power(0.92, age),
0.0,
)
rate = baseline * weekday[indices % week] * promotion
demand = rng.poisson(rate).astype(np.float64)
demand[rng.random(n) < rng.uniform(0.25, 0.75, n)] = 0.0
out[row] = demand
return out, True