| """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]) |
| |
| 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") |
|
|
| |
| |
| 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) |
| ) |
| |
| |
| 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 |
|
|