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