Spaces:
Running
Running
| """Exact finite-state models used to audit arXiv:2602.01381. | |
| The functions here mirror the paper's definitions. They deliberately avoid | |
| using the claimed bounds as simulated observations: target laws, guided laws, | |
| assumption constants, and finite-N SMC bias are computed independently. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from math import comb, exp, floor, lgamma, log | |
| from typing import Iterable | |
| import numpy as np | |
| def total_variation(p: np.ndarray, q: np.ndarray) -> float: | |
| p = np.asarray(p, dtype=float) | |
| q = np.asarray(q, dtype=float) | |
| if p.shape != q.shape: | |
| raise ValueError("TV inputs must have the same shape") | |
| if not np.isclose(p.sum(), 1.0, atol=1e-11): | |
| raise ValueError("first TV input is not normalized") | |
| if not np.isclose(q.sum(), 1.0, atol=1e-11): | |
| raise ValueError("second TV input is not normalized") | |
| return float(0.5 * np.abs(p - q).sum()) | |
| def _binomial_half_pmf(n: int) -> np.ndarray: | |
| """Stable Binomial(n, 1/2) PMF, built outwards from its mode.""" | |
| if n < 1: | |
| raise ValueError("n must be positive") | |
| mode = floor((n + 1) / 2) | |
| pmf = np.zeros(n + 1, dtype=float) | |
| pmf[mode] = exp( | |
| lgamma(n + 1) | |
| - lgamma(mode + 1) | |
| - lgamma(n - mode + 1) | |
| - n * log(2.0) | |
| ) | |
| for k in range(mode, 0, -1): | |
| pmf[k - 1] = pmf[k] * k / (n - k + 1) | |
| for k in range(mode, n): | |
| pmf[k + 1] = pmf[k] * (n - k) / (k + 1) | |
| pmf /= pmf.sum() | |
| return pmf | |
| def exact_resampled_bit_probability(n_particles: int, reward_ratio: float) -> float: | |
| """Marginal probability of bit 1 after one naive SMC resampling step. | |
| K ~ Binomial(N, 1/2) propagated particles have bit 1. Conditional on K, | |
| multinomial resampling selects bit 1 with probability K*r/(K*r+N-K). | |
| Taking the expectation gives the expected empirical output law exactly. | |
| """ | |
| if reward_ratio <= 0: | |
| raise ValueError("reward_ratio must be positive") | |
| k = np.arange(n_particles + 1, dtype=float) | |
| pmf = _binomial_half_pmf(n_particles) | |
| denominator = k * reward_ratio + n_particles - k | |
| conditional = np.divide( | |
| k * reward_ratio, | |
| denominator, | |
| out=np.zeros_like(k), | |
| where=denominator > 0, | |
| ) | |
| return float(pmf @ conditional) | |
| def product_bernoulli_tv(horizon: int, p: float, q: float) -> float: | |
| """TV between iid Bernoulli product laws, reduced exactly by Hamming weight.""" | |
| if not (0 <= p <= 1 and 0 <= q <= 1): | |
| raise ValueError("probabilities must lie in [0, 1]") | |
| terms = [] | |
| for k in range(horizon + 1): | |
| multiplicity = comb(horizon, k) | |
| pk = p**k * (1 - p) ** (horizon - k) | |
| qk = q**k * (1 - q) ** (horizon - k) | |
| terms.append(multiplicity * abs(pk - qk)) | |
| return 0.5 * float(sum(terms)) | |
| def product_bernoulli_tv_by_paths(horizon: int, p: float, q: float) -> float: | |
| """Independent checker: enumerate every path instead of grouping by weight.""" | |
| if horizon > 20: | |
| raise ValueError("path enumeration is intentionally capped at T=20") | |
| total = 0.0 | |
| for path_id in range(1 << horizon): | |
| ones = path_id.bit_count() | |
| pp = p**ones * (1 - p) ** (horizon - ones) | |
| qq = q**ones * (1 - q) ** (horizon - ones) | |
| total += abs(pp - qq) | |
| return 0.5 * total | |
| class ProductAudit: | |
| horizon: int | |
| reward_ratio: float | |
| ratio_bound_l: float | |
| bellman_epsilon: float | |
| target_bit_probability: float | |
| def audit_product_model(horizon: int, reward_ratio: float) -> ProductAudit: | |
| """Audit V(prefix)=r**(#ones) under a uniform binary reference model.""" | |
| if horizon < 1 or reward_ratio < 1: | |
| raise ValueError("requires T>=1 and r>=1") | |
| expected_ratio = (1.0 + reward_ratio) / 2.0 | |
| epsilon = max(expected_ratio, 1.0 / expected_ratio) - 1.0 | |
| return ProductAudit( | |
| horizon=horizon, | |
| reward_ratio=reward_ratio, | |
| ratio_bound_l=reward_ratio, | |
| bellman_epsilon=epsilon, | |
| target_bit_probability=reward_ratio / (1.0 + reward_ratio), | |
| ) | |
| def theorem_5_1_particle_bound( | |
| horizon: int, ratio_bound_l: float, epsilon: float, delta_tv: float | |
| ) -> float: | |
| if horizon < 2 or ratio_bound_l <= 0 or epsilon <= 0: | |
| raise ValueError("Theorem 5.1 requires T>=2 and L, epsilon > 0") | |
| if not 0 < delta_tv < 1: | |
| raise ValueError("delta_tv must be in (0,1)") | |
| return ( | |
| ratio_bound_l**6 | |
| * horizon | |
| * (1.0 + epsilon) ** (6 * (horizon - 1)) | |
| / (2.0 * delta_tv) | |
| ) | |
| def exact_product_smc_tv( | |
| horizon: int, n_particles: int, reward_ratio: float | |
| ) -> tuple[float, float, float]: | |
| audit = audit_product_model(horizon, reward_ratio) | |
| q = exact_resampled_bit_probability(n_particles, reward_ratio) | |
| tv = product_bernoulli_tv(horizon, audit.target_bit_probability, q) | |
| return audit.target_bit_probability, q, tv | |
| def minimum_particles_for_product_tv( | |
| horizon: int, | |
| reward_ratio: float, | |
| delta_tv: float, | |
| *, | |
| maximum_particles: int = 2_000_000, | |
| ) -> tuple[int, float]: | |
| """Find the minimum N meeting a TV target without using Theorem 5.1. | |
| The search calls the independently derived finite-N output law. It first | |
| doubles an upper bracket and then performs an integer binary search. | |
| """ | |
| if not 0 < delta_tv < 1: | |
| raise ValueError("delta_tv must be in (0,1)") | |
| def tv_at(n_particles: int) -> float: | |
| return exact_product_smc_tv( | |
| horizon, n_particles, reward_ratio | |
| )[2] | |
| if tv_at(1) <= delta_tv: | |
| return 1, tv_at(1) | |
| high = 2 | |
| while high <= maximum_particles and tv_at(high) > delta_tv: | |
| high *= 2 | |
| if high > maximum_particles: | |
| raise ValueError("minimum particle count exceeds search limit") | |
| low = high // 2 + 1 | |
| while low < high: | |
| midpoint = (low + high) // 2 | |
| if tv_at(midpoint) <= delta_tv: | |
| high = midpoint | |
| else: | |
| low = midpoint + 1 | |
| return low, tv_at(low) | |
| class PrefixTree: | |
| horizon: int | |
| levels: tuple[np.ndarray, ...] | |
| epsilon: float | |
| ratio_bound_l: float | |
| def build_prefix_tree(horizon: int, epsilon: float) -> PrefixTree: | |
| """Construct a nontrivial full binary reward tree with exact Bellman audit. | |
| Terminal rewards alternate smoothly. Internal values are a child mean times | |
| alternating factors at the two extrema allowed by Assumption 3.2. | |
| """ | |
| if horizon < 2 or not 0 < epsilon < 0.25: | |
| raise ValueError("requires T>=2 and epsilon in (0, .25)") | |
| terminal = np.array( | |
| [1.0 + 0.15 * ((path_id.bit_count() % 3) - 1) for path_id in range(1 << horizon)], | |
| dtype=float, | |
| ) | |
| levels: list[np.ndarray] = [np.array([]) for _ in range(horizon + 1)] | |
| levels[horizon] = terminal | |
| for t in range(horizon - 1, -1, -1): | |
| children = levels[t + 1].reshape(-1, 2) | |
| mean = children.mean(axis=1) | |
| index = np.arange(mean.size) | |
| factor = np.where(index % 2 == 0, 1.0 + epsilon, 1.0 / (1.0 + epsilon)) | |
| levels[t] = mean * factor | |
| max_ratio = 1.0 | |
| max_bellman = 1.0 | |
| for t in range(horizon): | |
| parent = levels[t] | |
| children = levels[t + 1].reshape(-1, 2) | |
| mean = children.mean(axis=1) | |
| max_ratio = max( | |
| max_ratio, | |
| float(np.max(children / parent[:, None])), | |
| float(np.max(parent[:, None] / children)), | |
| ) | |
| max_bellman = max( | |
| max_bellman, | |
| float(np.max(parent / mean)), | |
| float(np.max(mean / parent)), | |
| ) | |
| if max_bellman > 1.0 + epsilon + 1e-12: | |
| raise AssertionError("constructed tree violates its Bellman contract") | |
| return PrefixTree( | |
| horizon=horizon, | |
| levels=tuple(levels), | |
| epsilon=max_bellman - 1.0, | |
| ratio_bound_l=max_ratio, | |
| ) | |
| def target_prefix_law(tree: PrefixTree, t: int) -> np.ndarray: | |
| values = tree.levels[t] | |
| law = values / values.sum() | |
| return law | |
| def sp_guided_laws(tree: PrefixTree) -> tuple[np.ndarray, ...]: | |
| laws: list[np.ndarray] = [np.array([1.0])] | |
| for t in range(1, tree.horizon + 1): | |
| parent_law = laws[-1] | |
| child_values = tree.levels[t].reshape(-1, 2) | |
| conditional = child_values / child_values.sum(axis=1, keepdims=True) | |
| laws.append((parent_law[:, None] * conditional).reshape(-1)) | |
| return tuple(laws) | |
| def sp_tv_curve(tree: PrefixTree) -> list[float]: | |
| laws = sp_guided_laws(tree) | |
| return [ | |
| total_variation(target_prefix_law(tree, t), laws[t]) | |
| for t in range(tree.horizon + 1) | |
| ] | |
| def product_sp_tv(horizon: int, reward_ratio: float) -> float: | |
| """SP-gSMC is exactly the product target for multiplicative V.""" | |
| audit = audit_product_model(horizon, reward_ratio) | |
| guided_bit_probability = reward_ratio / (1.0 + reward_ratio) | |
| return product_bernoulli_tv( | |
| horizon, audit.target_bit_probability, guided_bit_probability | |
| ) | |
| class HardFamilyCertificate: | |
| horizon: int | |
| m: int | |
| branching: int | |
| reward_ratio: float | |
| hidden_prefixes: int | |
| target_region_mass: float | |
| tv_forced_hit_probability: float | |
| query_lower_bound: float | |
| ratio_bound_l: float | |
| bellman_epsilon: float | |
| def hard_family_certificate( | |
| horizon: int, branching: int, reward_ratio: float | |
| ) -> HardFamilyCertificate: | |
| """Executable certificate for Appendix C equations (6)--(14).""" | |
| if horizon % 3: | |
| raise ValueError("Appendix C construction writes T=3m") | |
| if branching < 2 or reward_ratio <= 1: | |
| raise ValueError("requires integer B>=2 and reward ratio >1") | |
| m = horizon // 3 | |
| hidden = branching ** (2 * m) | |
| good_weight = reward_ratio**m | |
| bad_weight = reward_ratio ** (-m) | |
| target_mass = good_weight / (good_weight + (hidden - 1) * bad_weight) | |
| forced = target_mass - 1.0 / 3.0 | |
| return HardFamilyCertificate( | |
| horizon=horizon, | |
| m=m, | |
| branching=branching, | |
| reward_ratio=reward_ratio, | |
| hidden_prefixes=hidden, | |
| target_region_mass=target_mass, | |
| tv_forced_hit_probability=forced, | |
| query_lower_bound=forced * hidden, | |
| ratio_bound_l=max(reward_ratio, 1.0 / reward_ratio), | |
| bellman_epsilon=reward_ratio - 1.0, | |
| ) | |
| def empirical_sequential_query_hits( | |
| hidden_prefixes: int, queries: int, trials: int, seed: int | |
| ) -> tuple[int, float]: | |
| """Run a no-guess oracle algorithm that queries prefixes 0,1,...,Q-1.""" | |
| if not 0 <= queries <= hidden_prefixes: | |
| raise ValueError("queries must lie in [0, hidden_prefixes]") | |
| rng = np.random.default_rng(seed) | |
| hidden_u = rng.integers(0, hidden_prefixes, size=trials) | |
| hits = int(np.count_nonzero(hidden_u < queries)) | |
| return hits, hits / trials | |
| def wilson_interval(successes: int, trials: int, z: float = 3.290526731) -> tuple[float, float]: | |
| """Two-sided Wilson interval; default z gives approximately 99.9% coverage.""" | |
| p = successes / trials | |
| denominator = 1.0 + z * z / trials | |
| center = (p + z * z / (2 * trials)) / denominator | |
| radius = ( | |
| z | |
| * np.sqrt(p * (1 - p) / trials + z * z / (4 * trials * trials)) | |
| / denominator | |
| ) | |
| return float(center - radius), float(center + radius) | |
| def log_log_slope(xs: Iterable[float], ys: Iterable[float]) -> float: | |
| x = np.log(np.asarray(tuple(xs), dtype=float)) | |
| y = np.log(np.asarray(tuple(ys), dtype=float)) | |
| return float(np.polyfit(x, y, 1)[0]) | |
| def log_linear_slope(xs: Iterable[float], ys: Iterable[float]) -> float: | |
| x = np.asarray(tuple(xs), dtype=float) | |
| y = np.log(np.asarray(tuple(ys), dtype=float)) | |
| return float(np.polyfit(x, y, 1)[0]) | |
| def product_target_path_law(horizon: int, reward_ratio: float) -> np.ndarray: | |
| """Target path law proportional to 2^-T r^(number of one bits).""" | |
| paths = np.arange(1 << horizon) | |
| ones = np.fromiter( | |
| (int(path).bit_count() for path in paths), dtype=np.int16, count=len(paths) | |
| ) | |
| probabilities = reward_ratio**ones.astype(float) | |
| return probabilities / probabilities.sum() | |
| def product_target_weight_law(horizon: int, reward_ratio: float) -> np.ndarray: | |
| """Target distribution of Hamming weight for the product path law.""" | |
| p = reward_ratio / (1.0 + reward_ratio) | |
| return np.asarray( | |
| [ | |
| comb(horizon, k) * p**k * (1.0 - p) ** (horizon - k) | |
| for k in range(horizon + 1) | |
| ], | |
| dtype=float, | |
| ) | |
| def empirical_weight_tv(weights: np.ndarray, target: np.ndarray) -> float: | |
| """TV on exchangeable path laws, reduced exactly to Hamming weights.""" | |
| histogram = np.bincount(weights, minlength=len(target)).astype(float) | |
| histogram /= histogram.sum() | |
| return total_variation(histogram, target) | |
| def _pool_proposal_batch( | |
| *, | |
| horizon: int, | |
| pool_size: int, | |
| reward_ratio: float, | |
| repetitions: int, | |
| xi: float, | |
| rng: np.random.Generator, | |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: | |
| """Generate Algorithm-2 augmented proposals through exact sufficient statistics. | |
| For the product potential V(s_1:t)=r^(sum s_i), a pool is summarized without | |
| approximation by K, the number of one-bits among M fair-reference draws. | |
| The returned log weight is exactly Algorithm 2 line 9. | |
| """ | |
| path_ids = np.zeros(repetitions, dtype=np.int32) | |
| ones_total = np.zeros(repetitions, dtype=np.int16) | |
| log_weights = np.zeros(repetitions, dtype=float) | |
| good = np.ones(repetitions, dtype=bool) | |
| max_relative_error = np.zeros(repetitions, dtype=float) | |
| mean_base_value = (1.0 + reward_ratio) / 2.0 | |
| log_ratio = log(reward_ratio) | |
| for _ in range(horizon): | |
| pool_ones = rng.binomial(pool_size, 0.5, size=repetitions) | |
| pool_sum = pool_size - pool_ones + pool_ones * reward_ratio | |
| empirical_mean = pool_sum / pool_size | |
| selected_probability = pool_ones * reward_ratio / pool_sum | |
| selected = (rng.random(repetitions) < selected_probability).astype(np.int16) | |
| relative_error = np.abs(empirical_mean / mean_base_value - 1.0) | |
| good &= relative_error <= xi | |
| max_relative_error = np.maximum(max_relative_error, relative_error) | |
| log_weights += selected * log_ratio - np.log(empirical_mean) | |
| ones_total += selected | |
| path_ids = (path_ids << 1) | selected | |
| terminal_log_values = ones_total.astype(float) * log_ratio | |
| return path_ids, log_weights, terminal_log_values, np.column_stack( | |
| [good, max_relative_error] | |
| ) | |
| def run_resampling_pool_mh( | |
| *, | |
| horizon: int, | |
| iterations: int, | |
| pool_size: int, | |
| reward_ratio: float, | |
| repetitions: int, | |
| xi: float, | |
| seed: int, | |
| invert_acceptance: bool = False, | |
| ) -> dict[str, np.ndarray | int]: | |
| """Vectorized, literal implementation of Algorithm 2 on a product model.""" | |
| if horizon < 1 or iterations < 1 or pool_size < 1 or repetitions < 1: | |
| raise ValueError("positive horizon, iterations, pool size, and repetitions required") | |
| rng = np.random.default_rng(seed) | |
| accepted_path, accepted_log_weight, accepted_log_value, diagnostics = ( | |
| _pool_proposal_batch( | |
| horizon=horizon, | |
| pool_size=pool_size, | |
| reward_ratio=reward_ratio, | |
| repetitions=repetitions, | |
| xi=xi, | |
| rng=rng, | |
| ) | |
| ) | |
| all_good = diagnostics[:, 0].astype(bool) | |
| maximum_relative_error = diagnostics[:, 1].copy() | |
| accepted_updates = np.zeros(repetitions, dtype=np.int16) | |
| for _ in range(1, iterations): | |
| proposed_path, proposed_log_weight, proposed_log_value, diagnostics = ( | |
| _pool_proposal_batch( | |
| horizon=horizon, | |
| pool_size=pool_size, | |
| reward_ratio=reward_ratio, | |
| repetitions=repetitions, | |
| xi=xi, | |
| rng=rng, | |
| ) | |
| ) | |
| all_good &= diagnostics[:, 0].astype(bool) | |
| maximum_relative_error = np.maximum( | |
| maximum_relative_error, diagnostics[:, 1] | |
| ) | |
| # Algorithm 2 line 15: | |
| # min(1, w_acc * V(proposal) / (w_proposal * V(accepted))). | |
| log_acceptance_ratio = ( | |
| accepted_log_weight | |
| + proposed_log_value | |
| - proposed_log_weight | |
| - accepted_log_value | |
| ) | |
| if invert_acceptance: | |
| log_acceptance_ratio = -log_acceptance_ratio | |
| accept = np.log(rng.random(repetitions)) < np.minimum( | |
| 0.0, log_acceptance_ratio | |
| ) | |
| accepted_path[accept] = proposed_path[accept] | |
| accepted_log_weight[accept] = proposed_log_weight[accept] | |
| accepted_log_value[accept] = proposed_log_value[accept] | |
| accepted_updates += accept | |
| return { | |
| "path_ids": accepted_path, | |
| "accepted_ones": np.rint( | |
| accepted_log_value / log(reward_ratio) | |
| ).astype(np.int16), | |
| "all_good": all_good, | |
| "maximum_relative_error": maximum_relative_error, | |
| "accepted_updates": accepted_updates, | |
| "pool_draws": repetitions * iterations * horizon * pool_size, | |
| } | |
| def exact_pool_good_probability(pool_size: int, reward_ratio: float, xi: float) -> float: | |
| """Exact probability that one product-model pool satisfies the good-set test.""" | |
| pmf = _binomial_half_pmf(pool_size) | |
| k = np.arange(pool_size + 1, dtype=float) | |
| empirical_mean = (pool_size - k + k * reward_ratio) / pool_size | |
| exact_mean = (1.0 + reward_ratio) / 2.0 | |
| good = np.abs(empirical_mean / exact_mean - 1.0) <= xi | |
| return float(pmf[good].sum()) | |
| def empirical_path_tv(path_ids: np.ndarray, target: np.ndarray) -> float: | |
| """TV between an empirical finite path law and an explicit target law.""" | |
| histogram = np.bincount(path_ids, minlength=len(target)).astype(float) | |
| histogram /= histogram.sum() | |
| return total_variation(histogram, target) | |
| def multinomial_tv_radius( | |
| states: int, samples: int, failure_probability: float | |
| ) -> float: | |
| """Simultaneous TV radius from the Weissman L1 concentration inequality.""" | |
| if states < 2 or samples < 1 or not 0 < failure_probability < 1: | |
| raise ValueError("invalid concentration parameters") | |
| log_prefactor = states * log(2.0) | |
| l1_radius = np.sqrt( | |
| 2.0 * (log_prefactor + log(1.0 / failure_probability)) / samples | |
| ) | |
| return float(min(1.0, 0.5 * l1_radius)) | |
| def _enumerated_pool_proposal( | |
| horizon: int, pool_size: int, reward_ratio: float | |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: | |
| """Enumerate the augmented sufficient-statistic proposal for a tiny model.""" | |
| one_step: list[tuple[int, float, float]] = [] | |
| for pool_ones in range(pool_size + 1): | |
| pool_probability = comb(pool_size, pool_ones) / (2.0**pool_size) | |
| pool_sum = pool_size - pool_ones + pool_ones * reward_ratio | |
| empirical_mean = pool_sum / pool_size | |
| selected_one_probability = pool_ones * reward_ratio / pool_sum | |
| for selected, selected_probability in ( | |
| (0, 1.0 - selected_one_probability), | |
| (1, selected_one_probability), | |
| ): | |
| probability = pool_probability * selected_probability | |
| if probability == 0: | |
| continue | |
| log_weight_increment = selected * log(reward_ratio) - log(empirical_mean) | |
| one_step.append((selected, log_weight_increment, probability)) | |
| states: dict[tuple[int, float], float] = {(0, 0.0): 1.0} | |
| for _ in range(horizon): | |
| next_states: dict[tuple[int, float], float] = {} | |
| for (path_id, log_weight), state_probability in states.items(): | |
| for selected, increment, option_probability in one_step: | |
| key = ((path_id << 1) | selected, round(log_weight + increment, 13)) | |
| next_states[key] = ( | |
| next_states.get(key, 0.0) | |
| + state_probability * option_probability | |
| ) | |
| states = next_states | |
| paths = np.fromiter((key[0] for key in states), dtype=np.int32) | |
| log_weights = np.fromiter((key[1] for key in states), dtype=float) | |
| probabilities = np.fromiter(states.values(), dtype=float) | |
| probabilities /= probabilities.sum() | |
| return paths, log_weights, probabilities | |
| def exact_augmented_mh_audit( | |
| *, | |
| horizon: int, | |
| iterations: int, | |
| pool_size: int, | |
| reward_ratio: float, | |
| invert_acceptance: bool = False, | |
| ) -> dict[str, float | int]: | |
| """Independent exhaustive checker of Algorithm 2's augmented-space MH ratio.""" | |
| paths, log_weights, proposal = _enumerated_pool_proposal( | |
| horizon, pool_size, reward_ratio | |
| ) | |
| terminal_log_values = np.array( | |
| [int(path).bit_count() * log(reward_ratio) for path in paths] | |
| ) | |
| log_density_ratio = terminal_log_values - log_weights | |
| if invert_acceptance: | |
| log_density_ratio = -log_density_ratio | |
| density_ratio = np.exp(log_density_ratio) | |
| augmented_target = proposal * density_ratio | |
| augmented_target /= augmented_target.sum() | |
| acceptance = np.minimum( | |
| 1.0, density_ratio[None, :] / density_ratio[:, None] | |
| ) | |
| transition = proposal[None, :] * acceptance | |
| transition[np.diag_indices_from(transition)] += 1.0 - transition.sum(axis=1) | |
| detailed_balance_error = float( | |
| np.max( | |
| np.abs( | |
| augmented_target[:, None] * transition | |
| - augmented_target[None, :] * transition.T | |
| ) | |
| ) | |
| ) | |
| stationarity_error = float( | |
| np.max(np.abs(augmented_target @ transition - augmented_target)) | |
| ) | |
| law = proposal.copy() | |
| for _ in range(1, iterations): | |
| law = law @ transition | |
| target_path = product_target_path_law(horizon, reward_ratio) | |
| output_path = np.bincount( | |
| paths, weights=law, minlength=len(target_path) | |
| ).astype(float) | |
| invariant_path = np.bincount( | |
| paths, weights=augmented_target, minlength=len(target_path) | |
| ).astype(float) | |
| return { | |
| "augmented_states": len(paths), | |
| "detailed_balance_max_error": detailed_balance_error, | |
| "stationarity_max_error": stationarity_error, | |
| "invariant_path_tv": total_variation(invariant_path, target_path), | |
| "finite_iteration_path_tv": total_variation(output_path, target_path), | |
| } | |