From 21c8ec166e85c9bd648c3e3a5683d25bf10d08a8 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 12 Jul 2026 10:49:26 -0700 Subject: [PATCH 01/26] bump to version 0.4.2 --- reports/junit/junit.xml | 2 +- src/tfscreen/__version__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index ed025d1..6327ea0 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/__version__.py b/src/tfscreen/__version__.py index ff3c18a..9480c73 100644 --- a/src/tfscreen/__version__.py +++ b/src/tfscreen/__version__.py @@ -5,5 +5,5 @@ Version string. """ -VERSION = (0, 4, 1) +VERSION = (0, 4, 2) __version__ = '.'.join(map(str, VERSION)) From b401b58a5ab8e3144550a437d5f80261d514af43 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 12 Jul 2026 18:21:26 -0700 Subject: [PATCH 02/26] fixed numerical stability in build empirical matrix --- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/junit/junit.xml | 2 +- .../simulate/empirical/fit_phenotypes.py | 7 ++- src/tfscreen/simulate/empirical/population.py | 49 ++++++++++++++----- .../simulate/empirical/test_population.py | 42 ++++++++++++++++ 6 files changed, 89 insertions(+), 15 deletions(-) diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index c24babe..9b5a50e 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.12%coverage96.12% \ No newline at end of file +coverage: 96.13%coverage96.13% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index e8bb384..e743b17 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4015tests4015 \ No newline at end of file +tests: 4017tests4017 \ No newline at end of file diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 6327ea0..d895a3a 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/empirical/fit_phenotypes.py b/src/tfscreen/simulate/empirical/fit_phenotypes.py index fa9fb9d..8279e35 100644 --- a/src/tfscreen/simulate/empirical/fit_phenotypes.py +++ b/src/tfscreen/simulate/empirical/fit_phenotypes.py @@ -544,7 +544,12 @@ def fit_phenotypes(growth_df, for (geno, titr), gf in zip(keys, gfs): fits[(geno, titr)] = gf natural = _natural_from_transformed(gf.est_t, gf.pheno_slice) - std_t = np.sqrt(np.diag(gf.cov_t))[gf.pheno_slice] + # A near-singular least-squares fit can return a covariance with tiny + # negative diagonal entries (non-PSD); clamp at 0 before sqrt so the + # diagnostic std is 0 rather than NaN and no RuntimeWarning is raised. + # Stage-2 repairs these covariances properly via _nearest_psd. + var_t = np.clip(np.diag(gf.cov_t)[gf.pheno_slice], 0.0, None) + std_t = np.sqrt(var_t) row = {"genotype": geno, "titrant_name": titr, "n_obs": gf.n_obs, "converged": gf.converged} row.update(natural) diff --git a/src/tfscreen/simulate/empirical/population.py b/src/tfscreen/simulate/empirical/population.py index 76b87af..6baa893 100644 --- a/src/tfscreen/simulate/empirical/population.py +++ b/src/tfscreen/simulate/empirical/population.py @@ -50,7 +50,6 @@ ) from scipy.special import expit -_JITTER = 1e-9 # added to matrices before inversion _EIG_FLOOR = 1e-8 # floor on eigenvalues of S_i and projected Sigma @@ -62,10 +61,28 @@ def _nearest_psd(A, floor=_EIG_FLOOR): return (V * w) @ V.T -def _batched_inv(mats): - """Invert a stack of matrices with a small diagonal jitter.""" - D = mats.shape[-1] - return np.linalg.inv(mats + _JITTER * np.eye(D)) +def _batched_inv(mats, floor=_EIG_FLOOR): + """Symmetric-PSD inverse (single ``(D, D)`` or batched ``(N, D, D)``). + + Robust where ``np.linalg.inv`` is not. ``inv`` raises + ``LinAlgError: Singular matrix`` when a matrix is numerically singular, and + a *single* bad matrix fails an entire batched call. A diagonal jitter can't + rescue a covariance whose largest eigenvalue dwarfs the jitter: an + unidentified Stage-1 parameter yields variances ~1e10+, so once its tiny + (floored) eigenvalue is combined with a huge one the condition number + exceeds double precision (~1e16), the small eigenvalue is lost to roundoff, + and the LU factorization sees an exact-zero pivot. + + Inverting straight from the eigendecomposition instead floors the + eigenvalues at ``floor`` *before* taking reciprocals, so the result is + always finite, symmetric, and well-conditioned (precision capped at + ``1/floor``) no matter how ill-conditioned the input. ``eigh`` of a finite + symmetric matrix never fails, so this cannot raise. + """ + mats = 0.5 * (mats + np.swapaxes(mats, -1, -2)) + w, V = np.linalg.eigh(mats) + inv_w = 1.0 / np.clip(w, floor, None) + return (V * inv_w[..., None, :]) @ np.swapaxes(V, -1, -2) def _pheno_block(fit): @@ -133,13 +150,23 @@ def _moment_init(Y, S): def _marginal_loglik(Y, S, mu, sigma): - """Sum_i log Normal(Y_i | mu, Sigma + S_i).""" + """Sum_i log Normal(Y_i | mu, Sigma + S_i). + + logdet and the quadratic form are both derived from a single floored + eigendecomposition of ``C = Sigma + S_i`` so this stays finite and never + raises even when a ``S_i`` is (numerically) singular — consistent with the + flooring used in ``_batched_inv``. + """ D = Y.shape[1] - C = sigma[None] + S # (N, D, D) + C = 0.5 * (sigma[None] + S) # (N, D, D) + C = C + np.swapaxes(C, -1, -2) r = Y - mu # (N, D) - sign, logdet = np.linalg.slogdet(C) - sol = np.linalg.solve(C, r[..., None])[..., 0] - quad = np.einsum("ni,ni->n", r, sol) + w, V = np.linalg.eigh(C) + w = np.clip(w, _EIG_FLOOR, None) + logdet = np.sum(np.log(w), axis=-1) # (N,) + # quad_n = r_n^T C^{-1} r_n with C^{-1} = V diag(1/w) V^T + proj = np.einsum("nij,ni->nj", V, r) # r in the eigenbasis + quad = np.einsum("nj,nj->n", proj ** 2, 1.0 / w) return float(np.sum(-0.5 * (D * np.log(2 * np.pi) + logdet + quad))) @@ -153,7 +180,7 @@ def _em_measurement_error(Y, S, max_iter=500, tol=1e-8): ll_prev = -np.inf for it in range(1, max_iter + 1): - sigma_inv = np.linalg.inv(sigma + _JITTER * np.eye(sigma.shape[0])) + sigma_inv = _batched_inv(sigma) # E-step: posterior of each z_i given y_i. prec = sigma_inv[None] + S_inv # (N, D, D) diff --git a/tests/tfscreen/simulate/empirical/test_population.py b/tests/tfscreen/simulate/empirical/test_population.py index 608ad5f..e22c031 100644 --- a/tests/tfscreen/simulate/empirical/test_population.py +++ b/tests/tfscreen/simulate/empirical/test_population.py @@ -13,6 +13,7 @@ fit_population, PopulationModel, _collect_estimates, + _batched_inv, ) D = 5 @@ -148,6 +149,47 @@ def test_save_load_roundtrip(tmp_path): assert loaded.n_used == model.n_used +def test_batched_inv_handles_singular_and_ill_conditioned(): + """_batched_inv must not raise on singular / ill-conditioned matrices.""" + rng = np.random.default_rng(0) + + # A well-conditioned PSD matrix: robust inverse ~ true inverse. + A = _random_psd(rng) + inv = _batched_inv(A) + assert np.allclose(inv @ A, np.eye(D), atol=1e-6) + + # A stack containing a genuinely singular matrix (a rank-1 outer product) + # and a wildly ill-conditioned one (eigenvalues spanning 1e12 .. 1e-8, the + # case that makes np.linalg.inv report "Singular matrix"). The robust + # inverse must produce a finite result rather than raising. + v = rng.normal(size=D) + singular = np.outer(v, v) # rank 1 + + Q, _ = np.linalg.qr(rng.normal(size=(D, D))) + eigs = np.array([1e12, 1e6, 1.0, 1e-4, 1e-8]) + ill = (Q * eigs) @ Q.T + + out = _batched_inv(np.stack([singular, ill])) # robust inv copes + assert np.all(np.isfinite(out)) + # Precision is bounded by the eigenvalue floor (1 / 1e-8 = 1e8). + assert np.max(np.abs(out)) <= 1e8 * (1 + 1e-6) + + +def test_fit_population_survives_singular_covariance(): + """A single unidentified (huge-variance) genotype must not crash the EM.""" + fits, _, _ = _make_population(300, _MU, _SIGMA, meas_scale=0.2, seed=9) + + # Inject a genotype whose Stage-1 covariance is effectively singular: one + # parameter is unconstrained (variance 1e12), mirroring a real failed fit. + bad_S = 0.1 * np.eye(D) + bad_S[0, 0] = 1e12 + fits[("unidentified", "iptg")] = _make_fit(_MU, bad_S) + + model = fit_population(fits, drop_railed=False) # must not raise + assert np.all(np.isfinite(model.mu)) + assert np.all(np.isfinite(model.cov)) + + def test_wt_ref_roundtrip(tmp_path): fits, _, _ = _make_population(300, _MU, _SIGMA, meas_scale=0.2, seed=8) model = fit_population(fits, drop_railed=False) From 0566949f6fd7adba0e3636fd42854bdf78be5b2a Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Thu, 16 Jul 2026 09:58:25 -0700 Subject: [PATCH 03/26] broke out genotype-level fitting into its own script --- CLAUDE.md | 3 + docs/badges/tests-badge.svg | 2 +- pyproject.toml | 1 + reports/junit/junit.xml | 2 +- .../simulate/empirical/congression.py | 202 +----- .../simulate/empirical/fit_phenotypes.py | 580 +--------------- .../simulate/scripts/build_empirical_cli.py | 32 +- src/tfscreen/tfmodel/genotype_fit/__init__.py | 23 + .../tfmodel/genotype_fit/congression.py | 231 +++++++ src/tfscreen/tfmodel/genotype_fit/fit.py | 643 ++++++++++++++++++ .../tfmodel/scripts/fit_genotypes_cli.py | 172 +++++ .../tfscreen/tfmodel/genotype_fit/__init__.py | 0 .../tfmodel/genotype_fit/test_congression.py | 111 +++ .../tfscreen/tfmodel/genotype_fit/test_fit.py | 114 ++++ .../genotype_fit/test_fit_genotypes_cli.py | 122 ++++ 15 files changed, 1480 insertions(+), 758 deletions(-) create mode 100644 src/tfscreen/tfmodel/genotype_fit/__init__.py create mode 100644 src/tfscreen/tfmodel/genotype_fit/congression.py create mode 100644 src/tfscreen/tfmodel/genotype_fit/fit.py create mode 100644 src/tfscreen/tfmodel/scripts/fit_genotypes_cli.py create mode 100644 tests/tfscreen/tfmodel/genotype_fit/__init__.py create mode 100644 tests/tfscreen/tfmodel/genotype_fit/test_congression.py create mode 100644 tests/tfscreen/tfmodel/genotype_fit/test_fit.py create mode 100644 tests/tfscreen/tfmodel/genotype_fit/test_fit_genotypes_cli.py diff --git a/CLAUDE.md b/CLAUDE.md index 5c8d899..806f2cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,7 @@ tfs-process-presplit # Process pre-split count files tfs-configure-model # Generate YAML config template tfs-prefit-calibration # Pre-fit linking function via MAP tfs-fit-model # Main hierarchical Bayesian inference +tfs-fit-genotypes # Per-genotype MLE fits of the growth model (+ optional congression de-attenuation) tfs-sample-posterior # Draw posterior samples from fitted model tfs-sample-prior # Draw prior predictive samples tfs-extract-params # Extract parameters from checkpoint @@ -266,6 +267,8 @@ These two dicts are the mechanism by which binding data is "pinned" into the gro An alternative to prior-predictive phenotype sampling: instead of drawing theta from made-up priors, fit **real** screen data to an empirical phenotype-generating distribution and resample from it, so the simulated library's phenotype *distribution* matches reality while ground truth stays known. Targets `hill_geno` + linear growth; asserts `A ≡ 1` (repressor: blocks or not, leaky binding absorbed into `theta_low`). Three stages: +**Module location note.** Stage 1 (per-genotype MLE fit) and Stage 1.5 (congression de-attenuation) live in `tfmodel/genotype_fit/` (`fit.py`, `congression.py`) — they are a general per-genotype inference engine for the growth model, exposed standalone as **`tfs-fit-genotypes`** (`tfmodel/scripts/fit_genotypes_cli.py`). `simulate/empirical/fit_phenotypes.py` and `.../congression.py` are back-compat re-export shims. `tfs-fit-genotypes` takes `growth_file` + `calibration_file` (positional; a prefit priors CSV or wide `condition_rep,growth_k,growth_m`) and writes `_params.csv` (raw fit), `_theta.csv` (predicted θ vs `[genotype,titrant_name,titrant_conc]`), and — with `--congression_lambda` — `_params_deattenuated.csv` + a `theta_deattenuated` column (and, with `--save_theta_history`, the fixed-point trajectory `_theta_history.csv`). Reusable helpers `fits_to_results_df(fits)` (rebuild the params table from any fits dict) and `predict_theta(fits, growth_df)` back both this CLI and `tfs-build-empirical`. **`_stage1_fits.csv` from `tfs-build-empirical` is the RAW (pre-de-attenuation) fit; the de-attenuated fits are now also written to `_stage1p5_fits.csv` when `--congression_lambda` is given.** The Stage-1.5 correction is inherently a *population* operation (the background CDF couples all bulk genotypes), even though the per-genotype MLE fits themselves are independent/parallel. + - **Stage 1** (`fit_phenotypes.py`): per-genotype MLE of the growth model on a real `ln_cfu` DataFrame (derives `ln_cfu_std` from the processed `ln_cfu_var` via `get_scaled_cfu`, like `model_orchestrator`), calibration (`growth_k`/`growth_m` per `condition_rep`) frozen. Fits `(dk_geno, theta_low, theta_high, log_hill_K, hill_n)` in transformed coords (logit theta bounds, log n) so `run_least_squares` returns covariance in the space Stage 2 needs. Per-genotype independent (no cross-genotype coupling); weak `dk_geno` Tikhonov prior; ±16 logit clamp. Returns `GenotypeFit(estimate, covariance)` per genotype. The fits are embarrassingly parallel — `num_workers` (`-1` = `cpu_count-1`) runs them over a `ProcessPoolExecutor`; the parallel path strips the genotype `Categorical` first (it carries all categories on every group → O(N²) pickling). Worker startup pays a ~2s JAX import, so parallelism is a wash for tiny/fast runs and near-linear for large real libraries. `_hill_theta` is numerically identical to `hill_geno.run_model` and `binding_params._hill_theta` (verified — same `_ZERO_CONC_SENTINEL`). - **Stage 2** (`population.py`): measurement-error EM (`z_i~N(mu,Σ)`, `y_i~N(z_i,S_i)`) that **deconvolves estimation noise** (`Cov(y)=Σ+mean(S_i)`), so it recovers a narrower population than a naive KDE on the point estimates. `fit_population()` → `PopulationModel` (single MV-Normal in transformed space; `wt_ref` field holds wt's actual Stage-1 fit; `.sample(n)` → natural-space DataFrame; `.save()`/`.load()`). - **Stage 1.5** (`congression.py`, optional; between Stage 1 and Stage 2, gated by `--congression_lambda`): de-attenuates the **bulk** genotypes' theta curves for co-transformation. Reuses the inference's own θ-level operator `transformation._congression.update_thetas` (the `E[max(x,M)]` **dominant-max occupancy** map — the tightest-bound operator sets effective θ, so `E[max]` only, never the min variant — with an empirical background CDF). `correct_theta_matrix` is the fixed point: `θ_true ← θ_obs`; iterate `θ_true += gain·(θ_obs − update_thetas(θ_true; background=θ_true))` per-concentration until converged (the background *is* the corrected population, hence the iteration). `deattenuate_congression` evaluates each bulk genotype's Stage-1 Hill at the growth `titrant_conc` grid, corrects, and refits Hill (dk_geno and the Stage-1 covariance are left untouched — a deliberate bias-only correction; Stage 2's estimation-noise deconvolution still runs after). λ passes straight through **unconverted**: a focal barcode is size-biased into its cell, so co-residents are Poisson(λ) at the *same* zero-truncated `transformation_poisson_lambda` (`_sim_transform` uses `zero_truncated_poisson` + i.i.d. plasmid draws). Spiked genotypes are congression-free → excluded from correction and the background CDF, and pass through unchanged. This is the θ-level analogue of the simulator's growth-level congression (agree to first order, exact at λ→0); spiked-only vs bulk distribution is the external check. diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index e743b17..fb3e408 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4017tests4017 \ No newline at end of file +tests: 4028tests4028 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 47cb058..24cb4fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ tfs-process-presplit = "tfscreen.process_raw.scripts.process_presplit_cli:main" tfs-configure-model = "tfscreen.tfmodel.scripts.configure_model_cli:main" tfs-prefit-calibration = "tfscreen.tfmodel.scripts.prefit_calibration_cli:main" tfs-fit-model = "tfscreen.tfmodel.scripts.fit_model_cli:main" +tfs-fit-genotypes = "tfscreen.tfmodel.scripts.fit_genotypes_cli:main" tfs-sample-posterior = "tfscreen.tfmodel.scripts.sample_posterior_cli:main" tfs-sample-prior = "tfscreen.tfmodel.scripts.sample_prior_cli:main" tfs-extract-params = "tfscreen.tfmodel.scripts.extract_params_cli:main" diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index d895a3a..4140e86 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/simulate/empirical/congression.py b/src/tfscreen/simulate/empirical/congression.py index 031657d..c9b4b55 100644 --- a/src/tfscreen/simulate/empirical/congression.py +++ b/src/tfscreen/simulate/empirical/congression.py @@ -1,194 +1,18 @@ """ -Stage 1.5 of the empirical-phenotype pipeline: de-attenuate the per-genotype -theta curves for congression (co-transformation). +Backward-compatible shim: Stage 1.5 of the empirical-phenotype pipeline. -A barcode observed in the bulk is really a cell that received a -zero-truncated-Poisson(lambda) number of plasmids. Under **dominant-max -occupancy** — the tightest-bound operator in a co-transformed cell sets the -effective theta, regardless of marker — the theta a monoclonal Hill fit -recovers for genotype ``g`` is inflated toward the population's high-occupancy -envelope: - - theta_obs = E[ max(theta_g, M) ], M = max of Poisson(lambda) co-residents - -drawn from the population theta distribution. This is exactly the inference's -own congression operator (``transformation._congression.update_thetas``, the -``E[max(x, M)]`` map with an empirical background CDF), and because a focal -barcode is size-biased into its cell, the co-resident count is Poisson at the -same ``lambda`` the simulator's ``transformation_poisson_lambda`` uses — so no -lambda conversion is needed. - -We want ``theta_true`` such that pushing it through that forward map (with the -background built from ``theta_true`` itself) reproduces ``theta_obs``. That is -a fixed point because the background *is* the corrected population, which -changes every pass. Per titrant concentration is an independent 1-D -correction; the Hill refit afterward re-links the concentrations into a curve. - -Scope / assumptions -------------------- -* **Dominant-max occupancy only** (single ``E[max]`` operator; no min variant). -* Corrects only **bulk** genotypes; spiked genotypes are congression-free, so - they are excluded from both the correction and the background CDF and pass - through unchanged. -* Operates on the genotype × concentration theta *point estimates* (pooled over - replicate/library by Stage 1); ``dk_geno`` is untouched (it is a growth - parameter, not part of the theta curve). Estimation-noise deconvolution - stays in Stage 2 — this stage only removes the congression *bias*, and keeps - each genotype's Stage-1 covariance (a deliberate approximation: the bias - shift barely changes estimation precision). - -This is the θ-level analogue of the simulator's growth-level congression; the -two agree to first order (exact at ``lambda -> 0``), and the spiked-only -distribution is the external check on the residual. +The congression de-attenuation moved to +:mod:`tfscreen.tfmodel.genotype_fit.congression` (it is reused by the standalone +``tfs-fit-genotypes`` command). This module re-exports it so existing +``simulate.empirical.congression`` imports keep working. """ -import numpy as np -import jax.numpy as jnp -from scipy.special import expit - -from tfscreen.util.io import read_dataframe -from tfscreen.mle.fitters.least_squares import run_least_squares -from tfscreen.simulate.empirical.fit_phenotypes import ( - _hill_theta, _POWER_CLIP, _LOGIT_BOUND, +from tfscreen.tfmodel.genotype_fit.congression import * # noqa: F401,F403 +from tfscreen.tfmodel.genotype_fit.congression import ( # noqa: F401 + correct_theta_matrix, + deattenuate_congression, + _theta_from_fit, + _refit_hill_theta, + _THETA_PARAM_IDX, + _THETA_EPS, ) -from tfscreen.tfmodel.generative.components.transformation._congression import ( - update_thetas, -) - -# Positions within the 5-element pheno block (PHENO_PARAMS_TRANSFORMED) of the -# theta-curve params: logit_theta_low, logit_theta_high, log_hill_K, log_hill_n. -# Index 0 (dk_geno) is a growth parameter and is left untouched. -_THETA_PARAM_IDX = np.array([1, 2, 3, 4]) - -_THETA_EPS = 1e-6 - - -def _theta_from_fit(fit, concs): - """Evaluate a genotype's fitted Hill curve at ``concs`` (natural conc).""" - pheno = np.asarray(fit.est_t)[fit.pheno_slice] - theta_low = expit(pheno[1]) - theta_high = expit(pheno[2]) - log_K = pheno[3] - n = np.exp(pheno[4]) - return _hill_theta(concs, theta_low, theta_high, log_K, n) - - -def correct_theta_matrix(theta_obs, lam, gain=1.0, tol=1e-4, max_iter=50, - n_grid=256): - """Fixed-point de-attenuation of an observed-theta matrix. - - Parameters - ---------- - theta_obs : np.ndarray, shape (n_conc, n_geno) - Observed theta per concentration (rows) and genotype (cols). Each row - is corrected against its own population CDF. - lam : float - Poisson co-resident rate (== the zero-truncated ``transformation_poisson_lambda``). - gain, tol, max_iter, n_grid : see module notes. - - Returns - ------- - theta_true : np.ndarray, shape (n_conc, n_geno) - n_iter : int - """ - theta_obs = np.asarray(theta_obs, dtype=float) - theta_true = np.clip(theta_obs.copy(), _THETA_EPS, 1.0 - _THETA_EPS) - - if lam is None or float(lam) <= 0: - return theta_obs.copy(), 0 - - n_iter = 0 - for n_iter in range(1, max_iter + 1): - theta_pred = np.asarray(update_thetas( - jnp.asarray(theta_true), (float(lam),), theta_dist="empirical", - population_theta=jnp.asarray(theta_true), n_grid=n_grid)) - resid = theta_obs - theta_pred - if np.nanmax(np.abs(resid)) < tol: - break - theta_true = np.clip(theta_true + gain * resid, _THETA_EPS, 1.0 - _THETA_EPS) - - return theta_true, n_iter - - -def _refit_hill_theta(concs, theta, guess_pheno): - """Refit the 4 transformed theta-Hill params to a corrected theta curve. - - ``guess_pheno`` is the genotype's current pheno block (used to seed). - Returns ``(logit_low, logit_high, log_K, log_n)``. - """ - def model(p_t, x): - low = expit(p_t[0]) - high = expit(p_t[1]) - log_K = p_t[2] - n = np.exp(p_t[3]) - return _hill_theta(x, low, high, log_K, n) - - guess = np.asarray(guess_pheno)[_THETA_PARAM_IDX].astype(float) - lower = np.array([-_LOGIT_BOUND, -_LOGIT_BOUND, np.log(1e-12), np.log(0.05)]) - upper = np.array([_LOGIT_BOUND, _LOGIT_BOUND, np.log(1e6), np.log(_POWER_CLIP)]) - est, _std, _cov, _fit = run_least_squares( - some_model=model, - obs=np.asarray(theta, dtype=float), - obs_std=np.ones_like(theta, dtype=float), - guesses=guess, lower_bounds=lower, upper_bounds=upper, - args=(np.asarray(concs, dtype=float),)) - return est - - -def deattenuate_congression(fits, growth_df, lam, spiked=None, - gain=1.0, tol=1e-4, max_iter=50, n_grid=256): - """Congression-correct the bulk theta curves in a Stage-1 ``fits`` dict. - - Parameters - ---------- - fits : dict - ``{(genotype, titrant_name): GenotypeFit}`` from ``fit_phenotypes``. - growth_df : pandas.DataFrame or str - The real ln_cfu data (supplies the per-titrant concentration grid the - theta curves are corrected on). - lam : float or None - Zero-truncated Poisson congression rate. ``None``/``<=0`` -> no-op. - spiked : iterable of str or None - Congression-free genotypes to exclude from correction and background. - gain, tol, max_iter, n_grid : fixed-point controls. - - Returns - ------- - dict - A new fits dict: bulk genotypes' theta-Hill params de-attenuated (Hill - refit; dk_geno and covariance unchanged); spiked genotypes untouched. - """ - if lam is None or float(lam) <= 0: - return dict(fits) - - spiked = set(spiked or []) - growth_df = read_dataframe(growth_df) - corrected = dict(fits) - - for titrant_name, sub in growth_df.groupby("titrant_name", observed=True): - concs = np.sort(sub["titrant_conc"].unique().astype(float)) - - keys = [k for k in fits - if k[1] == titrant_name and k[0] not in spiked - and np.all(np.isfinite(fits[k].est_t))] - if len(keys) < 2: - continue # need a population to build a background CDF - - # (n_conc, n_geno) observed theta. - theta_obs = np.stack([_theta_from_fit(fits[k], concs) for k in keys], - axis=1) - theta_true, _ = correct_theta_matrix( - theta_obs, float(lam), gain=gain, tol=tol, max_iter=max_iter, - n_grid=n_grid) - - for j, k in enumerate(keys): - fit = fits[k] - new_theta_t = _refit_hill_theta( - concs, theta_true[:, j], fit.est_t[fit.pheno_slice]) - est_t = np.array(fit.est_t, dtype=float) - pheno = est_t[fit.pheno_slice].copy() - pheno[_THETA_PARAM_IDX] = new_theta_t - est_t[fit.pheno_slice] = pheno - corrected[k] = fit._replace(est_t=est_t) - - return corrected diff --git a/src/tfscreen/simulate/empirical/fit_phenotypes.py b/src/tfscreen/simulate/empirical/fit_phenotypes.py index 8279e35..3ecb3cc 100644 --- a/src/tfscreen/simulate/empirical/fit_phenotypes.py +++ b/src/tfscreen/simulate/empirical/fit_phenotypes.py @@ -1,566 +1,24 @@ """ -Stage 1 of the empirical-phenotype pipeline: per-genotype MLE fits of the -growth model against *real* experimental data. +Backward-compatible shim: Stage 1 of the empirical-phenotype pipeline. -Given a processed ``ln_cfu`` DataFrame (the output of ``tfs-process-counts``) -and a set of *frozen* per-condition growth-calibration parameters (``k``, ``m`` -from ``tfs-prefit-calibration``), this module fits an independent, lightly -regularized nonlinear least-squares model to each genotype's ~O(100) -observations: - - ln_cfu = ln_cfu0 - + (k_pre + dk_geno + m_pre * theta) * t_pre - + (k_sel + dk_geno + m_sel * theta) * t_sel - -where ``theta`` is a 4-parameter Hill curve in the titrant concentration -(``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n``) and activity is -asserted to be 1 (appropriate for a repressor: it blocks transcription or it -does not; leaky binding is absorbed into ``theta_low``). - -The purpose is *not* genotype-specific truth — the per-genotype fits are -confounded (see the module notes) and, on bulk data, congression-attenuated. -The purpose is to harvest a realistic *joint distribution* of per-genotype -phenotype parameters that a later stage resamples to seed a simulation. For -that, each fit returns both a point estimate **and** its covariance, both in -the transformed coordinate system in which the downstream distribution is -estimated and sampled. - -Design / identifiability notes ------------------------------- -* The fit is done **per genotype, calibration frozen** — there is no - cross-genotype coupling and the condition ``k``/``m`` never re-float. This - is deliberate: a joint/global fit would let parameters soak up variance and - re-open the k/dk_geno slide. -* ``dk_geno`` and the vertical placement of the theta curve (``theta_low``) - are only softly separated. What is robustly identified is the titration - *shape* (``log_hill_K``, ``hill_n``) and *amplitude* - (``theta_high - theta_low``). The absolute level is separated only by the - contrast between the two selective markers (different ``m_sel``); a weak - prior on ``dk_geno`` (``dk_geno_prior_sd``) is applied as belt-and-suspenders. -* ``theta_low``/``theta_high`` are fit through a logit transform, so they are - intrinsically confined to ``(0, 1)`` and cannot run off to the - ``logit ~= +-16`` artefact that a raw-space fit produces. -* Per-tube starting-abundance variation is handled with a nuisance intercept - per ``intercept_cols`` group (default: one ``ln_cfu0`` per ``replicate``). - These intercepts are marginalized away for the downstream distribution. - -Assumptions to confirm against real data (flagged intentionally) ----------------------------------------------------------------- -1. Titrant concentration is constant within a sample across the pre- and - sel-phases, so a single ``theta`` enters both phase terms. -2. ``condition_pre`` / ``condition_sel`` strings match the ``condition_rep`` - keys used by the calibration table verbatim (this is the same identity - used on the fit side; see ``model_orchestrator._build_growth_tm``). -3. The Hill functional form here mirrors the ``hill_geno`` theta component so - that resampled parameters reproduce the fitted curves in Stage 3. +The per-genotype MLE fitter moved to +:mod:`tfscreen.tfmodel.genotype_fit.fit` (it is a general per-genotype +inference engine, also exposed standalone as ``tfs-fit-genotypes``). This +module re-exports it so existing ``simulate.empirical.fit_phenotypes`` imports +keep working. """ -import numpy as np -import pandas as pd -from scipy.special import expit, logit -import tqdm -import os -import warnings -from collections import namedtuple -from concurrent.futures import ProcessPoolExecutor - -from tfscreen.util.io import read_dataframe -from tfscreen.util.dataframe import check_columns, get_scaled_cfu -from tfscreen.mle.fitters.least_squares import run_least_squares - -# Numerical guards, matched to tfscreen.mle.curve_models.models so the fitted -# Hill is numerically identical to the empirical-curve library. -_EXP_CLIP = 700.0 -_POWER_CLIP = 25.0 - -# Zero-concentration substitution, matched to simulate.binding_params so the -# Hill evaluated when *fitting* here is identical to the one used when the -# resampled parameters are *injected* in Stage 3 (and to hill_geno inference). -_ZERO_CONC_SENTINEL = 1e-20 - -# Logit-space bound on theta_low/theta_high (mirrors FitManager's stability -# bound): keeps theta strictly inside (expit(-16), expit(16)) so the fit -# cannot float-saturate to exactly 0 or 1 when the dk_geno/theta_low -# degeneracy pushes the vertical placement to an asymptote. -_LOGIT_BOUND = 16.0 - -# Phenotype parameters carried forward to the distribution-estimation stage, -# in their transformed (fit) coordinates. The nuisance intercepts are *not* -# in this list. -PHENO_PARAMS_TRANSFORMED = [ - "dk_geno", - "logit_theta_low", - "logit_theta_high", - "log_hill_K", - "log_hill_n", -] - -# Natural-space names, aligned index-for-index with PHENO_PARAMS_TRANSFORMED. -PHENO_PARAMS_NATURAL = [ - "dk_geno", - "theta_low", - "theta_high", - "log_hill_K", - "hill_n", -] - -_Design = namedtuple( - "_Design", - ["conc", "t_pre", "t_sel", "k_pre", "m_pre", "k_sel", "m_sel", - "intercept_onehot", "n_intercept", "prior_idx"], +from tfscreen.tfmodel.genotype_fit.fit import * # noqa: F401,F403 +from tfscreen.tfmodel.genotype_fit.fit import ( # noqa: F401 + _hill_theta, + _natural_from_transformed, + _build_calib_lookup, + _lookup_km, + _initial_guess_transformed, + _forward, + _Design, + _POWER_CLIP, + _LOGIT_BOUND, + _EXP_CLIP, + _ZERO_CONC_SENTINEL, ) - -# Per-genotype fit result. ``est_t`` / ``cov_t`` are the *full* transformed -# parameter vector and covariance (intercepts first, then the five phenotype -# params in PHENO_PARAMS_TRANSFORMED order); ``pheno_slice`` indexes the -# phenotype block for the downstream stage. -GenotypeFit = namedtuple( - "GenotypeFit", - ["genotype", "titrant_name", "n_obs", "converged", - "param_names_t", "est_t", "cov_t", "pheno_slice"], -) - - -def _hill_theta(conc, theta_low, theta_high, log_K, n): - """Hill occupancy at ``conc`` (mirrors curve_models.models._hill). - - ``theta = theta_low + (theta_high - theta_low) * x^n / (x^n + K^n)`` with - ``x = conc`` and ``K = exp(log_K)``. For a repressor induced by titrant, - ``theta_high < theta_low`` (occupancy falls as titrant rises). - - This is identical to ``hill_geno.run_model``'s - ``sigmoid(n * (ln x - ln K))`` occupancy and to - ``simulate.binding_params._hill_theta`` (the Stage-3 injection path); zero - concentrations use the same ``_ZERO_CONC_SENTINEL`` substitution as the - injection path so fitted and injected curves coincide. - """ - conc = np.asarray(conc, dtype=float) - x_safe = np.where(conc == 0.0, _ZERO_CONC_SENTINEL, conc) - n_safe = np.clip(n, -_POWER_CLIP, _POWER_CLIP) - ln_K_to_n = np.clip(n_safe * log_K, -_EXP_CLIP, _EXP_CLIP) - ln_x_to_n = np.clip(n_safe * np.log(x_safe), -_EXP_CLIP, _EXP_CLIP) - K_to_n = np.exp(ln_K_to_n) - x_to_n = np.exp(ln_x_to_n) - fx = x_to_n / (x_to_n + K_to_n) - return theta_low + (theta_high - theta_low) * fx - - -def _forward(p_t, design): - """Predict ln_cfu (and prior pseudo-observations) from transformed params. - - Parameter layout in ``p_t``: - [ln_cfu0_1 ... ln_cfu0_G, dk_geno, logit_theta_low, - logit_theta_high, log_hill_K, log_hill_n] - where ``G = design.n_intercept``. - """ - n_int = design.n_intercept - ln_cfu0_vec = p_t[:n_int] - dk_geno = p_t[n_int + 0] - theta_low = expit(p_t[n_int + 1]) - theta_high = expit(p_t[n_int + 2]) - log_K = p_t[n_int + 3] - n = np.exp(p_t[n_int + 4]) - - theta = _hill_theta(design.conc, theta_low, theta_high, log_K, n) - rate_pre = design.k_pre + dk_geno + design.m_pre * theta - rate_sel = design.k_sel + dk_geno + design.m_sel * theta - - ln_cfu0_row = design.intercept_onehot @ ln_cfu0_vec - pred = ln_cfu0_row + rate_pre * design.t_pre + rate_sel * design.t_sel - - # Append the regularized parameter values as pseudo-observations so a - # Gaussian prior enters as extra (Tikhonov) residuals in the same - # least-squares problem. - if design.prior_idx is not None: - pred = np.concatenate([pred, p_t[design.prior_idx]]) - - return pred - - -# prefit priors-CSV parameter names -> wide calibration columns (linear model). -# The prefit writes these via _csv_row_name = "growth.{component}.{field}". -_CALIB_PRIORS_MAP = { - "growth.condition_growth.k_loc": "growth_k", - "growth.condition_growth.m_loc": "growth_m", -} - - -def _pivot_priors_calibration(df): - """Reshape a prefit priors CSV (long form) to wide (condition_rep, k, m).""" - if "value" not in df.columns: - raise ValueError("priors CSV lacks a 'value' column.") - - sub = df[df["parameter"].isin(list(_CALIB_PRIORS_MAP))].copy() - if sub.empty: - raise ValueError( - "priors CSV has no 'growth.condition_growth.k_loc' / " - "'growth.condition_growth.m_loc' rows; run tfs-prefit-calibration on " - "a linear condition_growth model first (the empirical pipeline " - "assumes the linear growth model).") - - if "condition_rep" not in sub.columns or sub["condition_rep"].isna().all(): - raise ValueError( - "growth k/m priors are present but carry no per-condition " - "'condition_rep' labels — this looks like a fresh configure. Run " - "tfs-prefit-calibration to write the per-condition calibration first.") - - sub = sub.dropna(subset=["condition_rep"]) - sub["_field"] = sub["parameter"].map(_CALIB_PRIORS_MAP) - wide = (sub.pivot_table(index="condition_rep", columns="_field", - values="value", aggfunc="first") - .reset_index()) - wide.columns.name = None - - missing = {"growth_k", "growth_m"} - set(wide.columns) - if missing: - raise ValueError( - f"priors CSV is missing calibration rows for: {sorted(missing)} " - f"(expected both condition_growth_k_loc and condition_growth_m_loc).") - - wide["growth_k"] = wide["growth_k"].astype(float) - wide["growth_m"] = wide["growth_m"].astype(float) - return wide[["condition_rep", "growth_k", "growth_m"]] - - -def read_calibration(calib): - """Normalize a calibration source to wide ``(condition_rep, growth_k, growth_m)``. - - Accepts, transparently: - - * the **priors CSV** written by ``tfs-prefit-calibration`` (long form: a - ``parameter`` column with ``growth.condition_growth.k_loc`` / - ``growth.condition_growth.m_loc`` rows carrying a ``condition_rep`` label), or - * an already-wide table (columns ``condition_rep``, ``growth_k``, - ``growth_m``), - - as either a path or a DataFrame. - """ - df = read_dataframe(calib) - - if {"condition_rep", "growth_k", "growth_m"}.issubset(df.columns): - out = df[["condition_rep", "growth_k", "growth_m"]].copy() - out["growth_k"] = out["growth_k"].astype(float) - out["growth_m"] = out["growth_m"].astype(float) - return out - - if "parameter" in df.columns: - return _pivot_priors_calibration(df) - - raise ValueError( - "calibration must be either a wide table with columns " - "[condition_rep, growth_k, growth_m] or a prefit priors CSV with " - "'parameter' rows growth.condition_growth.k_loc / " - "growth.condition_growth.m_loc.") - - -def _build_calib_lookup(calib): - """Return (k_map, m_map): dicts condition_rep -> growth_k / growth_m. - - ``calib`` may be a mapping ``{condition_rep: {"growth_k": .., - "growth_m": ..}}``, or anything :func:`read_calibration` accepts (a path or - DataFrame in either the prefit priors form or the wide form). - """ - if isinstance(calib, dict): - k_map, m_map = {}, {} - for cond, d in calib.items(): - k_map[cond] = d["growth_k"] - m_map[cond] = d["growth_m"] - return k_map, m_map - - calib_df = read_calibration(calib) - k_map = dict(zip(calib_df["condition_rep"], calib_df["growth_k"])) - m_map = dict(zip(calib_df["condition_rep"], calib_df["growth_m"])) - return k_map, m_map - - -def _lookup_km(conditions, k_map, m_map, which): - """Vectorized (k, m) lookup with a fail-fast on unknown conditions.""" - conditions = np.asarray(conditions, dtype=object) - missing = sorted({c for c in conditions if c not in k_map}) - if missing: - raise ValueError( - f"condition_{which} value(s) absent from the calibration table: " - f"{missing}. The per-genotype fit requires frozen k/m for every " - f"condition; run tfs-prefit-calibration first.") - k = np.array([k_map[c] for c in conditions], dtype=float) - m = np.array([m_map[c] for c in conditions], dtype=float) - return k, m - - -def _initial_guess_transformed(sub, n_int): - """Transformed-space initial guesses for one genotype group.""" - pos_conc = sub["titrant_conc"].to_numpy(dtype=float) - pos_conc = pos_conc[pos_conc > 0] - log_K0 = np.log(np.median(pos_conc)) if pos_conc.size else 0.0 - - guess = np.empty(n_int + 5, dtype=float) - guess[:n_int] = float(np.nanmedian(sub["ln_cfu"].to_numpy(dtype=float))) - guess[n_int + 0] = 0.0 # dk_geno - guess[n_int + 1] = logit(0.9) # theta_low (repressed at 0) - guess[n_int + 2] = logit(0.1) # theta_high (induced) - guess[n_int + 3] = log_K0 # log_hill_K - guess[n_int + 4] = 0.0 # log_hill_n (n = 1) - return guess - - -def fit_one_genotype(sub, k_map, m_map, intercept_cols, dk_geno_prior=(0.0, 1.0)): - """Fit the growth model to a single (genotype, titrant_name) group. - - Parameters - ---------- - sub : pandas.DataFrame - Rows for one genotype/titrant with columns ``ln_cfu``, ``ln_cfu_std``, - ``t_pre``, ``t_sel``, ``titrant_conc``, ``condition_pre``, - ``condition_sel`` and every column in ``intercept_cols``. - k_map, m_map : dict - ``condition_rep -> growth_k`` and ``-> growth_m`` (frozen calibration). - intercept_cols : list of str - Columns whose unique combinations each get their own nuisance - ``ln_cfu0``. Empty list -> a single shared intercept. - dk_geno_prior : (loc, sd) or None - Weak Gaussian prior on ``dk_geno`` (natural == transformed space). - ``None`` disables regularization. - - Returns - ------- - GenotypeFit - """ - # Drop unusable observations (dead / zero-cfu rows carry NaN ln_cfu or a - # non-finite / non-positive std that run_least_squares cannot weight). - finite = (np.isfinite(sub["ln_cfu"].to_numpy(dtype=float)) - & np.isfinite(sub["ln_cfu_std"].to_numpy(dtype=float)) - & (sub["ln_cfu_std"].to_numpy(dtype=float) > 0)) - sub = sub.loc[finite] - - n_obs = len(sub) - - # Build the nuisance-intercept one-hot design. - if intercept_cols: - keys = sub[intercept_cols].astype(str).agg("|".join, axis=1) - cats = pd.Categorical(keys) - onehot = np.zeros((n_obs, len(cats.categories)), dtype=float) - onehot[np.arange(n_obs), cats.codes] = 1.0 - else: - onehot = np.ones((n_obs, 1), dtype=float) - n_int = onehot.shape[1] - - param_names_t = ([f"ln_cfu0[{i}]" for i in range(n_int)] - + PHENO_PARAMS_TRANSFORMED) - pheno_slice = slice(n_int, n_int + 5) - - # A group with fewer observations than parameters cannot be fit. - n_params = n_int + 5 - if n_obs < n_params + 1: - return GenotypeFit( - genotype=sub["genotype"].iloc[0] if n_obs else None, - titrant_name=sub["titrant_name"].iloc[0] if n_obs else None, - n_obs=n_obs, converged=False, param_names_t=param_names_t, - est_t=np.full(n_params, np.nan), - cov_t=np.full((n_params, n_params), np.nan), - pheno_slice=pheno_slice) - - k_pre, m_pre = _lookup_km(sub["condition_pre"], k_map, m_map, "pre") - k_sel, m_sel = _lookup_km(sub["condition_sel"], k_map, m_map, "sel") - - # Assemble prior pseudo-observations (currently only dk_geno). - if dk_geno_prior is not None: - prior_idx = np.array([n_int + 0]) - prior_loc = np.array([dk_geno_prior[0]], dtype=float) - prior_sd = np.array([dk_geno_prior[1]], dtype=float) - else: - prior_idx = None - prior_loc = np.array([], dtype=float) - prior_sd = np.array([], dtype=float) - - design = _Design( - conc=sub["titrant_conc"].to_numpy(dtype=float), - t_pre=sub["t_pre"].to_numpy(dtype=float), - t_sel=sub["t_sel"].to_numpy(dtype=float), - k_pre=k_pre, m_pre=m_pre, k_sel=k_sel, m_sel=m_sel, - intercept_onehot=onehot, n_intercept=n_int, prior_idx=prior_idx) - - obs = np.concatenate([sub["ln_cfu"].to_numpy(dtype=float), prior_loc]) - obs_std = np.concatenate([sub["ln_cfu_std"].to_numpy(dtype=float), prior_sd]) - - guess = _initial_guess_transformed(sub, n_int) - - # Bounds: intercepts and dk_geno free; theta logits clamped to keep theta - # in (0, 1); log_hill_K to a wide but finite window; log_hill_n to the - # component's power-clip range. - lower = np.full(n_int + 5, -np.inf) - upper = np.full(n_int + 5, np.inf) - lower[n_int + 1:n_int + 3] = -_LOGIT_BOUND - upper[n_int + 1:n_int + 3] = _LOGIT_BOUND - lower[n_int + 3], upper[n_int + 3] = np.log(1e-12), np.log(1e6) - lower[n_int + 4], upper[n_int + 4] = np.log(0.05), np.log(_POWER_CLIP) - - est_t, _std_t, cov_t, fit = run_least_squares( - some_model=_forward, obs=obs, obs_std=obs_std, - guesses=guess, lower_bounds=lower, upper_bounds=upper, args=(design,)) - - converged = bool(getattr(fit, "success", False)) - - return GenotypeFit( - genotype=sub["genotype"].iloc[0], - titrant_name=sub["titrant_name"].iloc[0], - n_obs=n_obs, converged=converged, param_names_t=param_names_t, - est_t=est_t, cov_t=cov_t, pheno_slice=pheno_slice) - - -def _natural_from_transformed(est_t, pheno_slice): - """Back-transform the 5-element phenotype block to natural space.""" - dk, lo_t, hi_t, logK, logn = est_t[pheno_slice] - return { - "dk_geno": dk, - "theta_low": expit(lo_t), - "theta_high": expit(hi_t), - "log_hill_K": logK, - "hill_n": np.exp(logn), - } - - -# Read-only per-worker state for the process pool, populated once per worker by -# ``_init_worker`` so the (small) calibration maps are not re-pickled per task. -_WORKER_STATE = {} - - -def _init_worker(k_map, m_map, intercept_cols, dk_geno_prior): - _WORKER_STATE.update(k_map=k_map, m_map=m_map, - intercept_cols=intercept_cols, - dk_geno_prior=dk_geno_prior) - - -def _fit_one_task(sub): - """Pool worker: fit one genotype group using the shared worker state.""" - return fit_one_genotype(sub, _WORKER_STATE["k_map"], _WORKER_STATE["m_map"], - _WORKER_STATE["intercept_cols"], - dk_geno_prior=_WORKER_STATE["dk_geno_prior"]) - - -def _resolve_workers(num_workers): - """joblib-style worker count: 1 -> serial, -1 -> cpu_count-1, N -> N.""" - if num_workers is None or int(num_workers) == 1: - return 1 - if int(num_workers) < 0: - return max(1, (os.cpu_count() or 2) - 1) - return int(num_workers) - - -def fit_phenotypes(growth_df, - calib, - intercept_cols=("replicate",), - dk_geno_prior=(0.0, 1.0), - min_obs=None, - progress=True, - num_workers=1): - """Fit the growth model to every genotype in a real ``ln_cfu`` DataFrame. - - Parameters - ---------- - growth_df : pandas.DataFrame or str - Processed ln_cfu data (``tfs-process-counts`` output) or a path to it. - Required columns: ``genotype``, ``titrant_name``, ``titrant_conc``, - ``condition_pre``, ``condition_sel``, ``t_pre``, ``t_sel``, ``ln_cfu``, - ``ln_cfu_std``, plus every column in ``intercept_cols``. - calib : pandas.DataFrame or dict - Frozen per-condition growth calibration keyed by ``condition_rep``, - with ``growth_k`` and ``growth_m`` (see ``_build_calib_lookup``). - intercept_cols : sequence of str - Columns whose unique combinations each get a nuisance ``ln_cfu0``. - Default one intercept per ``replicate``; pass ``()`` for a single - shared intercept. - dk_geno_prior : (loc, sd) or None - Weak Gaussian prior on ``dk_geno``. ``None`` disables it. - min_obs : int or None - Skip genotype groups with fewer than this many usable observations. - progress : bool - Show a tqdm progress bar. - num_workers : int - Per-genotype fits are independent, so they can run in parallel over a - process pool. ``1`` (default) fits serially; ``-1`` uses - ``os.cpu_count() - 1`` workers; ``N`` uses ``N`` workers. - - Returns - ------- - results_df : pandas.DataFrame - One row per (genotype, titrant_name): ``n_obs``, ``converged``, the - five natural-space phenotype parameters, and their transformed-space - estimate/std (``_t``, ``_t_std``) for the downstream - distribution stage. - fits : dict - ``(genotype, titrant_name) -> GenotypeFit`` carrying the full - transformed estimate and covariance (the Stage-2 deconvolution input). - """ - growth_df = read_dataframe(growth_df) - # Real tfs-process-counts output carries ln_cfu + ln_cfu_var; derive - # ln_cfu / ln_cfu_std here exactly as the fit side (model_orchestrator) does. - growth_df = get_scaled_cfu(growth_df, need_columns=["ln_cfu", "ln_cfu_std"]) - intercept_cols = list(intercept_cols) - - required = ["genotype", "titrant_name", "titrant_conc", - "condition_pre", "condition_sel", "t_pre", "t_sel", - "ln_cfu", "ln_cfu_std"] + intercept_cols - check_columns(growth_df, required_columns=required) - - k_map, m_map = _build_calib_lookup(calib) - - workers = _resolve_workers(num_workers) - - # For the parallel path, drop the genotype Categorical: it carries the full - # category list on every group, so pickling each sub-frame to a worker would - # be O(num_genotype) per task (O(N^2) overall). Plain strings pickle in - # O(rows). fit_one_genotype reads the genotype via ``.iloc[0]``, so a str - # column behaves identically. - if workers != 1 and str(growth_df["genotype"].dtype) == "category": - growth_df = growth_df.copy() - growth_df["genotype"] = growth_df["genotype"].astype(str) - - # Collect the per-genotype work items (each is an independent fit). - keys, subs = [], [] - for key, sub in growth_df.groupby(["genotype", "titrant_name"], - observed=True, sort=False): - if min_obs is not None and len(sub) < min_obs: - continue - keys.append(key) - subs.append(sub) - - if workers == 1: - it = tqdm.tqdm(subs, desc="fitting genotypes") if progress else subs - gfs = [fit_one_genotype(sub, k_map, m_map, intercept_cols, - dk_geno_prior=dk_geno_prior) for sub in it] - else: - chunksize = max(1, len(subs) // (workers * 8)) if subs else 1 - with ProcessPoolExecutor( - max_workers=workers, initializer=_init_worker, - initargs=(k_map, m_map, intercept_cols, dk_geno_prior)) as ex: - mapped = ex.map(_fit_one_task, subs, chunksize=chunksize) - if progress: - mapped = tqdm.tqdm(mapped, total=len(subs), - desc=f"fitting genotypes ({workers} workers)") - gfs = list(mapped) - - rows = [] - fits = {} - for (geno, titr), gf in zip(keys, gfs): - fits[(geno, titr)] = gf - natural = _natural_from_transformed(gf.est_t, gf.pheno_slice) - # A near-singular least-squares fit can return a covariance with tiny - # negative diagonal entries (non-PSD); clamp at 0 before sqrt so the - # diagnostic std is 0 rather than NaN and no RuntimeWarning is raised. - # Stage-2 repairs these covariances properly via _nearest_psd. - var_t = np.clip(np.diag(gf.cov_t)[gf.pheno_slice], 0.0, None) - std_t = np.sqrt(var_t) - row = {"genotype": geno, "titrant_name": titr, - "n_obs": gf.n_obs, "converged": gf.converged} - row.update(natural) - for name, e, s in zip(PHENO_PARAMS_TRANSFORMED, - gf.est_t[gf.pheno_slice], std_t): - row[f"{name}_t"] = e - row[f"{name}_t_std"] = s - rows.append(row) - - if not rows: - warnings.warn("fit_phenotypes produced no fits (empty/filtered input).") - - results_df = pd.DataFrame(rows) - return results_df, fits diff --git a/src/tfscreen/simulate/scripts/build_empirical_cli.py b/src/tfscreen/simulate/scripts/build_empirical_cli.py index a3ea018..7dafa38 100644 --- a/src/tfscreen/simulate/scripts/build_empirical_cli.py +++ b/src/tfscreen/simulate/scripts/build_empirical_cli.py @@ -27,8 +27,8 @@ import os import warnings -from tfscreen.simulate.empirical.fit_phenotypes import ( - fit_phenotypes, _natural_from_transformed, +from tfscreen.tfmodel.genotype_fit.fit import ( + fit_phenotypes, fits_to_results_df, _natural_from_transformed, ) from tfscreen.simulate.empirical.population import fit_population from tfscreen.util.io import read_dataframe @@ -107,8 +107,10 @@ def build_empirical(growth_file, identifies the growth slope ``m`` from data (the in-library anchors). out_prefix : str Output prefix. Writes ``_model.npz`` (+ ``.names.json``), - ``_stage1_fits.csv``, and — unless ``calibration_file`` is - given — the ``_configure_*`` / ``_prefit_*`` intermediates. + ``_stage1_fits.csv`` (the RAW per-genotype fits), and — when + ``congression_lambda`` is given — ``_stage1p5_fits.csv`` (the + de-attenuated fits that feed Stage 2). Unless ``calibration_file`` is + given, also the ``_configure_*`` / ``_prefit_*`` intermediates. calibration_file : str, optional Skip the configure+prefit step and use this calibration directly — a prefit priors CSV or a wide ``condition_rep,growth_k,growth_m`` CSV. @@ -166,12 +168,18 @@ def build_empirical(growth_file, dk_geno_prior=dk_prior, min_obs=min_obs, num_workers=num_workers) # Stage 1.5 (optional): de-attenuate the bulk theta curves for congression. + # The raw Stage-1 table (results_df) is left as-is; the corrected fits are + # written to their own table so both are available and unambiguous. + stage1p5_df = None if congression_lambda is not None and float(congression_lambda) > 0: - from tfscreen.simulate.empirical.congression import deattenuate_congression + from tfscreen.tfmodel.genotype_fit.congression import ( + deattenuate_congression, + ) print(f"Stage 1.5: de-attenuating congression " f"(lambda={float(congression_lambda):g})...", flush=True) fits = deattenuate_congression( fits, growth_df, float(congression_lambda), spiked=spiked) + stage1p5_df = fits_to_results_df(fits) # Stage 2: turn the per-genotype fits into ONE generating distribution # (deconvolving estimation noise). This distribution is the deliverable. @@ -196,14 +204,26 @@ def build_empirical(growth_file, fits_path = os.path.abspath(f"{out_prefix}_stage1_fits.csv") results_df.to_csv(fits_path, index=False) + # The de-attenuated (Stage-1.5) fits, when congression was applied. The + # Stage-1 table above is always the *raw* (pre-de-attenuation) fit. + stage1p5_path = None + if stage1p5_df is not None: + stage1p5_path = os.path.abspath(f"{out_prefix}_stage1p5_fits.csv") + stage1p5_df.to_csv(stage1p5_path, index=False) + bar = "=" * 72 print(f"\n{bar}") print(f"Fit the generating distribution from {model.n_used} genotypes " f"(loglik={model.loglik:.4g}, {model.n_iter} EM iters).") print("\n Phenotype model (the distribution tfs-simulate samples from):") print(f" {model_path}") - print(" Per-genotype Stage-1 fits (diagnostic only, not used downstream):") + print(" Per-genotype Stage-1 fits, RAW / pre-de-attenuation " + "(diagnostic only, not used downstream):") print(f" {fits_path}") + if stage1p5_path is not None: + print(" Per-genotype Stage-1.5 fits, congression-de-attenuated " + "(what feeds Stage 2):") + print(f" {stage1p5_path}") print("\nTo simulate from it, add to your tfs-simulate config:") print(" phenotype_source: empirical") print(" empirical:") diff --git a/src/tfscreen/tfmodel/genotype_fit/__init__.py b/src/tfscreen/tfmodel/genotype_fit/__init__.py new file mode 100644 index 0000000..a6f3a1e --- /dev/null +++ b/src/tfscreen/tfmodel/genotype_fit/__init__.py @@ -0,0 +1,23 @@ +""" +Per-genotype MLE fitting of the growth model against real screen data. + +A non-Bayesian, per-genotype inference engine for the same growth model that +``tfmodel`` fits jointly. Exposed by ``tfs-fit-genotypes`` and reused as +Stages 1 / 1.5 of the empirical-phenotype simulation pipeline. +""" + +from tfscreen.tfmodel.genotype_fit.fit import ( # noqa: F401 + GenotypeFit, + PHENO_PARAMS_TRANSFORMED, + PHENO_PARAMS_NATURAL, + fit_phenotypes, + fit_one_genotype, + fits_to_results_df, + predict_theta, + hill_theta_from_fit, + read_calibration, +) +from tfscreen.tfmodel.genotype_fit.congression import ( # noqa: F401 + correct_theta_matrix, + deattenuate_congression, +) diff --git a/src/tfscreen/tfmodel/genotype_fit/congression.py b/src/tfscreen/tfmodel/genotype_fit/congression.py new file mode 100644 index 0000000..e7fa640 --- /dev/null +++ b/src/tfscreen/tfmodel/genotype_fit/congression.py @@ -0,0 +1,231 @@ +""" +Congression de-attenuation of per-genotype theta curves (co-transformation). + +A barcode observed in the bulk is really a cell that received a +zero-truncated-Poisson(lambda) number of plasmids. Under **dominant-max +occupancy** — the tightest-bound operator in a co-transformed cell sets the +effective theta, regardless of marker — the theta a monoclonal Hill fit +recovers for genotype ``g`` is inflated toward the population's high-occupancy +envelope: + + theta_obs = E[ max(theta_g, M) ], M = max of Poisson(lambda) co-residents + +drawn from the population theta distribution. This is exactly the inference's +own congression operator (``transformation._congression.update_thetas``, the +``E[max(x, M)]`` map with an empirical background CDF), and because a focal +barcode is size-biased into its cell, the co-resident count is Poisson at the +same ``lambda`` the simulator's ``transformation_poisson_lambda`` uses — so no +lambda conversion is needed. + +We want ``theta_true`` such that pushing it through that forward map (with the +background built from ``theta_true`` itself) reproduces ``theta_obs``. That is +a fixed point because the background *is* the corrected population, which +changes every pass. Per titrant concentration is an independent 1-D +correction; the Hill refit afterward re-links the concentrations into a curve. + +This is the standalone (``tfs-fit-genotypes``) and Stage-1.5 (empirical +simulation pipeline) de-attenuation layer; ``simulate/empirical/congression`` +re-exports it. + +Scope / assumptions +------------------- +* **Dominant-max occupancy only** (single ``E[max]`` operator; no min variant). +* Corrects only **bulk** genotypes; spiked genotypes are congression-free, so + they are excluded from both the correction and the background CDF and pass + through unchanged. +* Operates on the genotype × concentration theta *point estimates* (pooled over + replicate/library by Stage 1); ``dk_geno`` is untouched (it is a growth + parameter, not part of the theta curve). Estimation-noise deconvolution + stays in Stage 2 — this stage only removes the congression *bias*, and keeps + each genotype's Stage-1 covariance (a deliberate approximation: the bias + shift barely changes estimation precision). + +This is the θ-level analogue of the simulator's growth-level congression; the +two agree to first order (exact at ``lambda -> 0``), and the spiked-only +distribution is the external check on the residual. +""" + +import numpy as np +import jax.numpy as jnp +import pandas as pd + +from tfscreen.util.io import read_dataframe +from tfscreen.mle.fitters.least_squares import run_least_squares +from tfscreen.tfmodel.genotype_fit.fit import ( + _hill_theta, hill_theta_from_fit, _POWER_CLIP, _LOGIT_BOUND, +) +from tfscreen.tfmodel.generative.components.transformation._congression import ( + update_thetas, +) +from scipy.special import expit # noqa: F401 (kept for backward-compat imports) + +# Positions within the 5-element pheno block (PHENO_PARAMS_TRANSFORMED) of the +# theta-curve params: logit_theta_low, logit_theta_high, log_hill_K, log_hill_n. +# Index 0 (dk_geno) is a growth parameter and is left untouched. +_THETA_PARAM_IDX = np.array([1, 2, 3, 4]) + +_THETA_EPS = 1e-6 + + +def _theta_from_fit(fit, concs): + """Backward-compatible alias for :func:`fit.hill_theta_from_fit`.""" + return hill_theta_from_fit(fit, concs) + + +def correct_theta_matrix(theta_obs, lam, gain=1.0, tol=1e-4, max_iter=50, + n_grid=256, return_history=False): + """Fixed-point de-attenuation of an observed-theta matrix. + + Parameters + ---------- + theta_obs : np.ndarray, shape (n_conc, n_geno) + Observed theta per concentration (rows) and genotype (cols). Each row + is corrected against its own population CDF. + lam : float + Poisson co-resident rate (== the zero-truncated ``transformation_poisson_lambda``). + gain, tol, max_iter, n_grid : see module notes. + return_history : bool + If True, also return the list of intermediate ``theta_true`` matrices, + one per iteration (index 0 is the initial clamped ``theta_obs``, the + last entry is the converged estimate). Lets callers report the + convergence trajectory per concentration. + + Returns + ------- + theta_true : np.ndarray, shape (n_conc, n_geno) + n_iter : int + history : list of np.ndarray + Only when ``return_history`` is True. + """ + theta_obs = np.asarray(theta_obs, dtype=float) + theta_true = np.clip(theta_obs.copy(), _THETA_EPS, 1.0 - _THETA_EPS) + history = [theta_true.copy()] + + if lam is None or float(lam) <= 0: + out = theta_obs.copy() + return (out, 0, history) if return_history else (out, 0) + + n_iter = 0 + for n_iter in range(1, max_iter + 1): + theta_pred = np.asarray(update_thetas( + jnp.asarray(theta_true), (float(lam),), theta_dist="empirical", + population_theta=jnp.asarray(theta_true), n_grid=n_grid)) + resid = theta_obs - theta_pred + if np.nanmax(np.abs(resid)) < tol: + break + theta_true = np.clip(theta_true + gain * resid, _THETA_EPS, 1.0 - _THETA_EPS) + history.append(theta_true.copy()) + + return (theta_true, n_iter, history) if return_history else (theta_true, n_iter) + + +def _refit_hill_theta(concs, theta, guess_pheno): + """Refit the 4 transformed theta-Hill params to a corrected theta curve. + + ``guess_pheno`` is the genotype's current pheno block (used to seed). + Returns ``(logit_low, logit_high, log_K, log_n)``. + """ + def model(p_t, x): + low = expit(p_t[0]) + high = expit(p_t[1]) + log_K = p_t[2] + n = np.exp(p_t[3]) + return _hill_theta(x, low, high, log_K, n) + + guess = np.asarray(guess_pheno)[_THETA_PARAM_IDX].astype(float) + lower = np.array([-_LOGIT_BOUND, -_LOGIT_BOUND, np.log(1e-12), np.log(0.05)]) + upper = np.array([_LOGIT_BOUND, _LOGIT_BOUND, np.log(1e6), np.log(_POWER_CLIP)]) + est, _std, _cov, _fit = run_least_squares( + some_model=model, + obs=np.asarray(theta, dtype=float), + obs_std=np.ones_like(theta, dtype=float), + guesses=guess, lower_bounds=lower, upper_bounds=upper, + args=(np.asarray(concs, dtype=float),)) + return est + + +def deattenuate_congression(fits, growth_df, lam, spiked=None, + gain=1.0, tol=1e-4, max_iter=50, n_grid=256, + return_theta_history=False): + """Congression-correct the bulk theta curves in a Stage-1 ``fits`` dict. + + Parameters + ---------- + fits : dict + ``{(genotype, titrant_name): GenotypeFit}`` from ``fit_phenotypes``. + growth_df : pandas.DataFrame or str + The real ln_cfu data (supplies the per-titrant concentration grid the + theta curves are corrected on). + lam : float or None + Zero-truncated Poisson congression rate. ``None``/``<=0`` -> no-op. + spiked : iterable of str or None + Congression-free genotypes to exclude from correction and background. + gain, tol, max_iter, n_grid : fixed-point controls. + return_theta_history : bool + If True, also return a long-form DataFrame of the fixed-point theta + trajectory (columns ``genotype, titrant_name, titrant_conc, iter, + theta``), for diagnosing the convergence. + + Returns + ------- + corrected : dict + A new fits dict: bulk genotypes' theta-Hill params de-attenuated (Hill + refit; dk_geno and covariance unchanged); spiked genotypes untouched. + history_df : pandas.DataFrame + Only when ``return_theta_history`` is True. + """ + empty_hist = pd.DataFrame( + columns=["genotype", "titrant_name", "titrant_conc", "iter", "theta"]) + + if lam is None or float(lam) <= 0: + out = dict(fits) + return (out, empty_hist) if return_theta_history else out + + spiked = set(spiked or []) + growth_df = read_dataframe(growth_df) + corrected = dict(fits) + hist_rows = [] + + for titrant_name, sub in growth_df.groupby("titrant_name", observed=True): + concs = np.sort(sub["titrant_conc"].unique().astype(float)) + + keys = [k for k in fits + if k[1] == titrant_name and k[0] not in spiked + and np.all(np.isfinite(fits[k].est_t))] + if len(keys) < 2: + continue # need a population to build a background CDF + + # (n_conc, n_geno) observed theta. + theta_obs = np.stack([hill_theta_from_fit(fits[k], concs) for k in keys], + axis=1) + result = correct_theta_matrix( + theta_obs, float(lam), gain=gain, tol=tol, max_iter=max_iter, + n_grid=n_grid, return_history=return_theta_history) + + if return_theta_history: + theta_true, _, history = result + for it, mat in enumerate(history): + for i_c, c in enumerate(concs): + for j, k in enumerate(keys): + hist_rows.append({ + "genotype": k[0], "titrant_name": titrant_name, + "titrant_conc": float(c), "iter": it, + "theta": float(mat[i_c, j])}) + else: + theta_true, _ = result + + for j, k in enumerate(keys): + fit = fits[k] + new_theta_t = _refit_hill_theta( + concs, theta_true[:, j], fit.est_t[fit.pheno_slice]) + est_t = np.array(fit.est_t, dtype=float) + pheno = est_t[fit.pheno_slice].copy() + pheno[_THETA_PARAM_IDX] = new_theta_t + est_t[fit.pheno_slice] = pheno + corrected[k] = fit._replace(est_t=est_t) + + if return_theta_history: + history_df = (pd.DataFrame(hist_rows, columns=empty_hist.columns) + if hist_rows else empty_hist) + return corrected, history_df + return corrected diff --git a/src/tfscreen/tfmodel/genotype_fit/fit.py b/src/tfscreen/tfmodel/genotype_fit/fit.py new file mode 100644 index 0000000..a8416df --- /dev/null +++ b/src/tfscreen/tfmodel/genotype_fit/fit.py @@ -0,0 +1,643 @@ +""" +Per-genotype MLE fitting of the growth model against *real* experimental data. + +This is a non-Bayesian, per-genotype inference engine for the same generative +growth model that ``tfmodel`` fits jointly by SVI/NUTS. Given a processed +``ln_cfu`` DataFrame (the output of ``tfs-process-counts``) and a set of +*frozen* per-condition growth-calibration parameters (``k``, ``m`` from +``tfs-prefit-calibration``), it fits an independent, lightly regularized +nonlinear least-squares model to each genotype's ~O(100) observations: + + ln_cfu = ln_cfu0 + + (k_pre + dk_geno + m_pre * theta) * t_pre + + (k_sel + dk_geno + m_sel * theta) * t_sel + +where ``theta`` is a 4-parameter Hill curve in the titrant concentration +(``theta_low``, ``theta_high``, ``log_hill_K``, ``hill_n``) and activity is +asserted to be 1 (appropriate for a repressor: it blocks transcription or it +does not; leaky binding is absorbed into ``theta_low``). + +It is exposed directly by ``tfs-fit-genotypes`` and is also Stage 1 of the +empirical-phenotype simulation pipeline (``simulate/empirical/``), whose +``fit_phenotypes`` re-exports this module. In the simulation use, the purpose +is *not* genotype-specific truth — the per-genotype fits are confounded (see +the module notes) and, on bulk data, congression-attenuated — but to harvest a +realistic *joint distribution* of per-genotype phenotype parameters that a +later stage resamples. For that, each fit returns both a point estimate **and** +its covariance, both in the transformed coordinate system in which the +downstream distribution is estimated and sampled. + +Design / identifiability notes +------------------------------ +* The fit is done **per genotype, calibration frozen** — there is no + cross-genotype coupling and the condition ``k``/``m`` never re-float. This + is deliberate: a joint/global fit would let parameters soak up variance and + re-open the k/dk_geno slide. +* ``dk_geno`` and the vertical placement of the theta curve (``theta_low``) + are only softly separated. What is robustly identified is the titration + *shape* (``log_hill_K``, ``hill_n``) and *amplitude* + (``theta_high - theta_low``). The absolute level is separated only by the + contrast between the two selective markers (different ``m_sel``); a weak + prior on ``dk_geno`` (``dk_geno_prior_sd``) is applied as belt-and-suspenders. +* ``theta_low``/``theta_high`` are fit through a logit transform, so they are + intrinsically confined to ``(0, 1)`` and cannot run off to the + ``logit ~= +-16`` artefact that a raw-space fit produces. +* Per-tube starting-abundance variation is handled with a nuisance intercept + per ``intercept_cols`` group (default: one ``ln_cfu0`` per ``replicate``). + These intercepts are marginalized away for the downstream distribution. + +Assumptions to confirm against real data (flagged intentionally) +---------------------------------------------------------------- +1. Titrant concentration is constant within a sample across the pre- and + sel-phases, so a single ``theta`` enters both phase terms. +2. ``condition_pre`` / ``condition_sel`` strings match the ``condition_rep`` + keys used by the calibration table verbatim (this is the same identity + used on the fit side; see ``model_orchestrator._build_growth_tm``). +3. The Hill functional form here mirrors the ``hill_geno`` theta component so + that resampled parameters reproduce the fitted curves in Stage 3. +""" + +import numpy as np +import pandas as pd +from scipy.special import expit, logit +import tqdm +import os +import warnings +from collections import namedtuple +from concurrent.futures import ProcessPoolExecutor + +from tfscreen.util.io import read_dataframe +from tfscreen.util.dataframe import check_columns, get_scaled_cfu +from tfscreen.mle.fitters.least_squares import run_least_squares + +# Numerical guards, matched to tfscreen.mle.curve_models.models so the fitted +# Hill is numerically identical to the empirical-curve library. +_EXP_CLIP = 700.0 +_POWER_CLIP = 25.0 + +# Zero-concentration substitution, matched to simulate.binding_params so the +# Hill evaluated when *fitting* here is identical to the one used when the +# resampled parameters are *injected* in Stage 3 (and to hill_geno inference). +_ZERO_CONC_SENTINEL = 1e-20 + +# Logit-space bound on theta_low/theta_high (mirrors FitManager's stability +# bound): keeps theta strictly inside (expit(-16), expit(16)) so the fit +# cannot float-saturate to exactly 0 or 1 when the dk_geno/theta_low +# degeneracy pushes the vertical placement to an asymptote. +_LOGIT_BOUND = 16.0 + +# Phenotype parameters carried forward to the distribution-estimation stage, +# in their transformed (fit) coordinates. The nuisance intercepts are *not* +# in this list. +PHENO_PARAMS_TRANSFORMED = [ + "dk_geno", + "logit_theta_low", + "logit_theta_high", + "log_hill_K", + "log_hill_n", +] + +# Natural-space names, aligned index-for-index with PHENO_PARAMS_TRANSFORMED. +PHENO_PARAMS_NATURAL = [ + "dk_geno", + "theta_low", + "theta_high", + "log_hill_K", + "hill_n", +] + +_Design = namedtuple( + "_Design", + ["conc", "t_pre", "t_sel", "k_pre", "m_pre", "k_sel", "m_sel", + "intercept_onehot", "n_intercept", "prior_idx"], +) + +# Per-genotype fit result. ``est_t`` / ``cov_t`` are the *full* transformed +# parameter vector and covariance (intercepts first, then the five phenotype +# params in PHENO_PARAMS_TRANSFORMED order); ``pheno_slice`` indexes the +# phenotype block for the downstream stage. +GenotypeFit = namedtuple( + "GenotypeFit", + ["genotype", "titrant_name", "n_obs", "converged", + "param_names_t", "est_t", "cov_t", "pheno_slice"], +) + + +def _hill_theta(conc, theta_low, theta_high, log_K, n): + """Hill occupancy at ``conc`` (mirrors curve_models.models._hill). + + ``theta = theta_low + (theta_high - theta_low) * x^n / (x^n + K^n)`` with + ``x = conc`` and ``K = exp(log_K)``. For a repressor induced by titrant, + ``theta_high < theta_low`` (occupancy falls as titrant rises). + + This is identical to ``hill_geno.run_model``'s + ``sigmoid(n * (ln x - ln K))`` occupancy and to + ``simulate.binding_params._hill_theta`` (the Stage-3 injection path); zero + concentrations use the same ``_ZERO_CONC_SENTINEL`` substitution as the + injection path so fitted and injected curves coincide. + """ + conc = np.asarray(conc, dtype=float) + x_safe = np.where(conc == 0.0, _ZERO_CONC_SENTINEL, conc) + n_safe = np.clip(n, -_POWER_CLIP, _POWER_CLIP) + ln_K_to_n = np.clip(n_safe * log_K, -_EXP_CLIP, _EXP_CLIP) + ln_x_to_n = np.clip(n_safe * np.log(x_safe), -_EXP_CLIP, _EXP_CLIP) + K_to_n = np.exp(ln_K_to_n) + x_to_n = np.exp(ln_x_to_n) + fx = x_to_n / (x_to_n + K_to_n) + return theta_low + (theta_high - theta_low) * fx + + +def hill_theta_from_fit(fit, concs): + """Evaluate a genotype's fitted Hill curve at ``concs`` (natural conc). + + Reads the theta-curve params out of a :class:`GenotypeFit`'s phenotype + block and evaluates :func:`_hill_theta`. Used by :func:`predict_theta` and + by the congression de-attenuation stage. + """ + pheno = np.asarray(fit.est_t)[fit.pheno_slice] + theta_low = expit(pheno[1]) + theta_high = expit(pheno[2]) + log_K = pheno[3] + n = np.exp(pheno[4]) + return _hill_theta(concs, theta_low, theta_high, log_K, n) + + +def _forward(p_t, design): + """Predict ln_cfu (and prior pseudo-observations) from transformed params. + + Parameter layout in ``p_t``: + [ln_cfu0_1 ... ln_cfu0_G, dk_geno, logit_theta_low, + logit_theta_high, log_hill_K, log_hill_n] + where ``G = design.n_intercept``. + """ + n_int = design.n_intercept + ln_cfu0_vec = p_t[:n_int] + dk_geno = p_t[n_int + 0] + theta_low = expit(p_t[n_int + 1]) + theta_high = expit(p_t[n_int + 2]) + log_K = p_t[n_int + 3] + n = np.exp(p_t[n_int + 4]) + + theta = _hill_theta(design.conc, theta_low, theta_high, log_K, n) + rate_pre = design.k_pre + dk_geno + design.m_pre * theta + rate_sel = design.k_sel + dk_geno + design.m_sel * theta + + ln_cfu0_row = design.intercept_onehot @ ln_cfu0_vec + pred = ln_cfu0_row + rate_pre * design.t_pre + rate_sel * design.t_sel + + # Append the regularized parameter values as pseudo-observations so a + # Gaussian prior enters as extra (Tikhonov) residuals in the same + # least-squares problem. + if design.prior_idx is not None: + pred = np.concatenate([pred, p_t[design.prior_idx]]) + + return pred + + +# prefit priors-CSV parameter names -> wide calibration columns (linear model). +# The prefit writes these via _csv_row_name = "growth.{component}.{field}". +_CALIB_PRIORS_MAP = { + "growth.condition_growth.k_loc": "growth_k", + "growth.condition_growth.m_loc": "growth_m", +} + + +def _pivot_priors_calibration(df): + """Reshape a prefit priors CSV (long form) to wide (condition_rep, k, m).""" + if "value" not in df.columns: + raise ValueError("priors CSV lacks a 'value' column.") + + sub = df[df["parameter"].isin(list(_CALIB_PRIORS_MAP))].copy() + if sub.empty: + raise ValueError( + "priors CSV has no 'growth.condition_growth.k_loc' / " + "'growth.condition_growth.m_loc' rows; run tfs-prefit-calibration on " + "a linear condition_growth model first (the empirical pipeline " + "assumes the linear growth model).") + + if "condition_rep" not in sub.columns or sub["condition_rep"].isna().all(): + raise ValueError( + "growth k/m priors are present but carry no per-condition " + "'condition_rep' labels — this looks like a fresh configure. Run " + "tfs-prefit-calibration to write the per-condition calibration first.") + + sub = sub.dropna(subset=["condition_rep"]) + sub["_field"] = sub["parameter"].map(_CALIB_PRIORS_MAP) + wide = (sub.pivot_table(index="condition_rep", columns="_field", + values="value", aggfunc="first") + .reset_index()) + wide.columns.name = None + + missing = {"growth_k", "growth_m"} - set(wide.columns) + if missing: + raise ValueError( + f"priors CSV is missing calibration rows for: {sorted(missing)} " + f"(expected both condition_growth_k_loc and condition_growth_m_loc).") + + wide["growth_k"] = wide["growth_k"].astype(float) + wide["growth_m"] = wide["growth_m"].astype(float) + return wide[["condition_rep", "growth_k", "growth_m"]] + + +def read_calibration(calib): + """Normalize a calibration source to wide ``(condition_rep, growth_k, growth_m)``. + + Accepts, transparently: + + * the **priors CSV** written by ``tfs-prefit-calibration`` (long form: a + ``parameter`` column with ``growth.condition_growth.k_loc`` / + ``growth.condition_growth.m_loc`` rows carrying a ``condition_rep`` label), or + * an already-wide table (columns ``condition_rep``, ``growth_k``, + ``growth_m``), + + as either a path or a DataFrame. + """ + df = read_dataframe(calib) + + if {"condition_rep", "growth_k", "growth_m"}.issubset(df.columns): + out = df[["condition_rep", "growth_k", "growth_m"]].copy() + out["growth_k"] = out["growth_k"].astype(float) + out["growth_m"] = out["growth_m"].astype(float) + return out + + if "parameter" in df.columns: + return _pivot_priors_calibration(df) + + raise ValueError( + "calibration must be either a wide table with columns " + "[condition_rep, growth_k, growth_m] or a prefit priors CSV with " + "'parameter' rows growth.condition_growth.k_loc / " + "growth.condition_growth.m_loc.") + + +def _build_calib_lookup(calib): + """Return (k_map, m_map): dicts condition_rep -> growth_k / growth_m. + + ``calib`` may be a mapping ``{condition_rep: {"growth_k": .., + "growth_m": ..}}``, or anything :func:`read_calibration` accepts (a path or + DataFrame in either the prefit priors form or the wide form). + """ + if isinstance(calib, dict): + k_map, m_map = {}, {} + for cond, d in calib.items(): + k_map[cond] = d["growth_k"] + m_map[cond] = d["growth_m"] + return k_map, m_map + + calib_df = read_calibration(calib) + k_map = dict(zip(calib_df["condition_rep"], calib_df["growth_k"])) + m_map = dict(zip(calib_df["condition_rep"], calib_df["growth_m"])) + return k_map, m_map + + +def _lookup_km(conditions, k_map, m_map, which): + """Vectorized (k, m) lookup with a fail-fast on unknown conditions.""" + conditions = np.asarray(conditions, dtype=object) + missing = sorted({c for c in conditions if c not in k_map}) + if missing: + raise ValueError( + f"condition_{which} value(s) absent from the calibration table: " + f"{missing}. The per-genotype fit requires frozen k/m for every " + f"condition; run tfs-prefit-calibration first.") + k = np.array([k_map[c] for c in conditions], dtype=float) + m = np.array([m_map[c] for c in conditions], dtype=float) + return k, m + + +def _initial_guess_transformed(sub, n_int): + """Transformed-space initial guesses for one genotype group.""" + pos_conc = sub["titrant_conc"].to_numpy(dtype=float) + pos_conc = pos_conc[pos_conc > 0] + log_K0 = np.log(np.median(pos_conc)) if pos_conc.size else 0.0 + + guess = np.empty(n_int + 5, dtype=float) + guess[:n_int] = float(np.nanmedian(sub["ln_cfu"].to_numpy(dtype=float))) + guess[n_int + 0] = 0.0 # dk_geno + guess[n_int + 1] = logit(0.9) # theta_low (repressed at 0) + guess[n_int + 2] = logit(0.1) # theta_high (induced) + guess[n_int + 3] = log_K0 # log_hill_K + guess[n_int + 4] = 0.0 # log_hill_n (n = 1) + return guess + + +def fit_one_genotype(sub, k_map, m_map, intercept_cols, dk_geno_prior=(0.0, 1.0)): + """Fit the growth model to a single (genotype, titrant_name) group. + + Parameters + ---------- + sub : pandas.DataFrame + Rows for one genotype/titrant with columns ``ln_cfu``, ``ln_cfu_std``, + ``t_pre``, ``t_sel``, ``titrant_conc``, ``condition_pre``, + ``condition_sel`` and every column in ``intercept_cols``. + k_map, m_map : dict + ``condition_rep -> growth_k`` and ``-> growth_m`` (frozen calibration). + intercept_cols : list of str + Columns whose unique combinations each get their own nuisance + ``ln_cfu0``. Empty list -> a single shared intercept. + dk_geno_prior : (loc, sd) or None + Weak Gaussian prior on ``dk_geno`` (natural == transformed space). + ``None`` disables regularization. + + Returns + ------- + GenotypeFit + """ + # Drop unusable observations (dead / zero-cfu rows carry NaN ln_cfu or a + # non-finite / non-positive std that run_least_squares cannot weight). + finite = (np.isfinite(sub["ln_cfu"].to_numpy(dtype=float)) + & np.isfinite(sub["ln_cfu_std"].to_numpy(dtype=float)) + & (sub["ln_cfu_std"].to_numpy(dtype=float) > 0)) + sub = sub.loc[finite] + + n_obs = len(sub) + + # Build the nuisance-intercept one-hot design. + if intercept_cols: + keys = sub[intercept_cols].astype(str).agg("|".join, axis=1) + cats = pd.Categorical(keys) + onehot = np.zeros((n_obs, len(cats.categories)), dtype=float) + onehot[np.arange(n_obs), cats.codes] = 1.0 + else: + onehot = np.ones((n_obs, 1), dtype=float) + n_int = onehot.shape[1] + + param_names_t = ([f"ln_cfu0[{i}]" for i in range(n_int)] + + PHENO_PARAMS_TRANSFORMED) + pheno_slice = slice(n_int, n_int + 5) + + # A group with fewer observations than parameters cannot be fit. + n_params = n_int + 5 + if n_obs < n_params + 1: + return GenotypeFit( + genotype=sub["genotype"].iloc[0] if n_obs else None, + titrant_name=sub["titrant_name"].iloc[0] if n_obs else None, + n_obs=n_obs, converged=False, param_names_t=param_names_t, + est_t=np.full(n_params, np.nan), + cov_t=np.full((n_params, n_params), np.nan), + pheno_slice=pheno_slice) + + k_pre, m_pre = _lookup_km(sub["condition_pre"], k_map, m_map, "pre") + k_sel, m_sel = _lookup_km(sub["condition_sel"], k_map, m_map, "sel") + + # Assemble prior pseudo-observations (currently only dk_geno). + if dk_geno_prior is not None: + prior_idx = np.array([n_int + 0]) + prior_loc = np.array([dk_geno_prior[0]], dtype=float) + prior_sd = np.array([dk_geno_prior[1]], dtype=float) + else: + prior_idx = None + prior_loc = np.array([], dtype=float) + prior_sd = np.array([], dtype=float) + + design = _Design( + conc=sub["titrant_conc"].to_numpy(dtype=float), + t_pre=sub["t_pre"].to_numpy(dtype=float), + t_sel=sub["t_sel"].to_numpy(dtype=float), + k_pre=k_pre, m_pre=m_pre, k_sel=k_sel, m_sel=m_sel, + intercept_onehot=onehot, n_intercept=n_int, prior_idx=prior_idx) + + obs = np.concatenate([sub["ln_cfu"].to_numpy(dtype=float), prior_loc]) + obs_std = np.concatenate([sub["ln_cfu_std"].to_numpy(dtype=float), prior_sd]) + + guess = _initial_guess_transformed(sub, n_int) + + # Bounds: intercepts and dk_geno free; theta logits clamped to keep theta + # in (0, 1); log_hill_K to a wide but finite window; log_hill_n to the + # component's power-clip range. + lower = np.full(n_int + 5, -np.inf) + upper = np.full(n_int + 5, np.inf) + lower[n_int + 1:n_int + 3] = -_LOGIT_BOUND + upper[n_int + 1:n_int + 3] = _LOGIT_BOUND + lower[n_int + 3], upper[n_int + 3] = np.log(1e-12), np.log(1e6) + lower[n_int + 4], upper[n_int + 4] = np.log(0.05), np.log(_POWER_CLIP) + + est_t, _std_t, cov_t, fit = run_least_squares( + some_model=_forward, obs=obs, obs_std=obs_std, + guesses=guess, lower_bounds=lower, upper_bounds=upper, args=(design,)) + + converged = bool(getattr(fit, "success", False)) + + return GenotypeFit( + genotype=sub["genotype"].iloc[0], + titrant_name=sub["titrant_name"].iloc[0], + n_obs=n_obs, converged=converged, param_names_t=param_names_t, + est_t=est_t, cov_t=cov_t, pheno_slice=pheno_slice) + + +def _natural_from_transformed(est_t, pheno_slice): + """Back-transform the 5-element phenotype block to natural space.""" + dk, lo_t, hi_t, logK, logn = est_t[pheno_slice] + return { + "dk_geno": dk, + "theta_low": expit(lo_t), + "theta_high": expit(hi_t), + "log_hill_K": logK, + "hill_n": np.exp(logn), + } + + +def fits_to_results_df(fits): + """Assemble the per-genotype results table from a ``fits`` dict. + + ``fits`` is ``{(genotype, titrant_name): GenotypeFit}`` (as returned by + :func:`fit_phenotypes` or produced by the congression de-attenuation + stage). Returns one row per fit with ``n_obs``, ``converged``, the five + natural-space phenotype parameters, and their transformed-space + estimate/std (``_t``, ``_t_std``). Dict insertion order is + preserved. + + Because this reads only ``est_t``/``cov_t``, it produces the same schema + whether the fits are raw (Stage 1) or de-attenuated (Stage 1.5), which is + how ``tfs-fit-genotypes`` emits the raw and corrected parameter tables. + """ + rows = [] + for (geno, titr), gf in fits.items(): + natural = _natural_from_transformed(gf.est_t, gf.pheno_slice) + # A near-singular least-squares fit can return a covariance with tiny + # negative diagonal entries (non-PSD); clamp at 0 before sqrt so the + # diagnostic std is 0 rather than NaN and no RuntimeWarning is raised. + # Stage-2 repairs these covariances properly via _nearest_psd. + var_t = np.clip(np.diag(gf.cov_t)[gf.pheno_slice], 0.0, None) + std_t = np.sqrt(var_t) + row = {"genotype": geno, "titrant_name": titr, + "n_obs": gf.n_obs, "converged": gf.converged} + row.update(natural) + for name, e, s in zip(PHENO_PARAMS_TRANSFORMED, + gf.est_t[gf.pheno_slice], std_t): + row[f"{name}_t"] = e + row[f"{name}_t_std"] = s + rows.append(row) + + if not rows: + warnings.warn("fits_to_results_df received no fits (empty input).") + + return pd.DataFrame(rows) + + +def predict_theta(fits, growth_df, theta_col="theta"): + """Long-form theta predictions from fitted Hill curves. + + Evaluates each genotype's fitted Hill curve on its titrant's + sorted-unique concentration grid (taken from ``growth_df``). + + Parameters + ---------- + fits : dict + ``{(genotype, titrant_name): GenotypeFit}``. + growth_df : pandas.DataFrame or str + The ln_cfu data (supplies the per-titrant concentration grid). + theta_col : str + Name of the output theta column (default ``"theta"``). Pass a + distinct name to concatenate/merge raw and de-attenuated predictions. + + Returns + ------- + pandas.DataFrame + Columns ``[genotype, titrant_name, titrant_conc, ]``, one + row per (genotype, titrant_name, titrant_conc). Non-finite fits emit + NaN theta rather than being dropped. + """ + growth_df = read_dataframe(growth_df) + grids = {t: np.sort(sub["titrant_conc"].unique().astype(float)) + for t, sub in growth_df.groupby("titrant_name", observed=True)} + + rows = [] + for (geno, titr), fit in fits.items(): + concs = grids.get(titr) + if concs is None: + continue + if np.all(np.isfinite(fit.est_t)): + theta = hill_theta_from_fit(fit, concs) + else: + theta = np.full(concs.shape, np.nan) + for c, th in zip(concs, theta): + rows.append({"genotype": geno, "titrant_name": titr, + "titrant_conc": float(c), theta_col: float(th)}) + + return pd.DataFrame( + rows, columns=["genotype", "titrant_name", "titrant_conc", theta_col]) + + +# Read-only per-worker state for the process pool, populated once per worker by +# ``_init_worker`` so the (small) calibration maps are not re-pickled per task. +_WORKER_STATE = {} + + +def _init_worker(k_map, m_map, intercept_cols, dk_geno_prior): + _WORKER_STATE.update(k_map=k_map, m_map=m_map, + intercept_cols=intercept_cols, + dk_geno_prior=dk_geno_prior) + + +def _fit_one_task(sub): + """Pool worker: fit one genotype group using the shared worker state.""" + return fit_one_genotype(sub, _WORKER_STATE["k_map"], _WORKER_STATE["m_map"], + _WORKER_STATE["intercept_cols"], + dk_geno_prior=_WORKER_STATE["dk_geno_prior"]) + + +def _resolve_workers(num_workers): + """joblib-style worker count: 1 -> serial, -1 -> cpu_count-1, N -> N.""" + if num_workers is None or int(num_workers) == 1: + return 1 + if int(num_workers) < 0: + return max(1, (os.cpu_count() or 2) - 1) + return int(num_workers) + + +def fit_phenotypes(growth_df, + calib, + intercept_cols=("replicate",), + dk_geno_prior=(0.0, 1.0), + min_obs=None, + progress=True, + num_workers=1): + """Fit the growth model to every genotype in a real ``ln_cfu`` DataFrame. + + Parameters + ---------- + growth_df : pandas.DataFrame or str + Processed ln_cfu data (``tfs-process-counts`` output) or a path to it. + Required columns: ``genotype``, ``titrant_name``, ``titrant_conc``, + ``condition_pre``, ``condition_sel``, ``t_pre``, ``t_sel``, ``ln_cfu``, + ``ln_cfu_std``, plus every column in ``intercept_cols``. + calib : pandas.DataFrame or dict + Frozen per-condition growth calibration keyed by ``condition_rep``, + with ``growth_k`` and ``growth_m`` (see ``_build_calib_lookup``). + intercept_cols : sequence of str + Columns whose unique combinations each get a nuisance ``ln_cfu0``. + Default one intercept per ``replicate``; pass ``()`` for a single + shared intercept. + dk_geno_prior : (loc, sd) or None + Weak Gaussian prior on ``dk_geno``. ``None`` disables it. + min_obs : int or None + Skip genotype groups with fewer than this many usable observations. + progress : bool + Show a tqdm progress bar. + num_workers : int + Per-genotype fits are independent, so they can run in parallel over a + process pool. ``1`` (default) fits serially; ``-1`` uses + ``os.cpu_count() - 1`` workers; ``N`` uses ``N`` workers. + + Returns + ------- + results_df : pandas.DataFrame + One row per (genotype, titrant_name): ``n_obs``, ``converged``, the + five natural-space phenotype parameters, and their transformed-space + estimate/std (``_t``, ``_t_std``) for the downstream + distribution stage. + fits : dict + ``(genotype, titrant_name) -> GenotypeFit`` carrying the full + transformed estimate and covariance (the Stage-2 deconvolution input). + """ + growth_df = read_dataframe(growth_df) + # Real tfs-process-counts output carries ln_cfu + ln_cfu_var; derive + # ln_cfu / ln_cfu_std here exactly as the fit side (model_orchestrator) does. + growth_df = get_scaled_cfu(growth_df, need_columns=["ln_cfu", "ln_cfu_std"]) + intercept_cols = list(intercept_cols) + + required = ["genotype", "titrant_name", "titrant_conc", + "condition_pre", "condition_sel", "t_pre", "t_sel", + "ln_cfu", "ln_cfu_std"] + intercept_cols + check_columns(growth_df, required_columns=required) + + k_map, m_map = _build_calib_lookup(calib) + + workers = _resolve_workers(num_workers) + + # For the parallel path, drop the genotype Categorical: it carries the full + # category list on every group, so pickling each sub-frame to a worker would + # be O(num_genotype) per task (O(N^2) overall). Plain strings pickle in + # O(rows). fit_one_genotype reads the genotype via ``.iloc[0]``, so a str + # column behaves identically. + if workers != 1 and str(growth_df["genotype"].dtype) == "category": + growth_df = growth_df.copy() + growth_df["genotype"] = growth_df["genotype"].astype(str) + + # Collect the per-genotype work items (each is an independent fit). + keys, subs = [], [] + for key, sub in growth_df.groupby(["genotype", "titrant_name"], + observed=True, sort=False): + if min_obs is not None and len(sub) < min_obs: + continue + keys.append(key) + subs.append(sub) + + if workers == 1: + it = tqdm.tqdm(subs, desc="fitting genotypes") if progress else subs + gfs = [fit_one_genotype(sub, k_map, m_map, intercept_cols, + dk_geno_prior=dk_geno_prior) for sub in it] + else: + chunksize = max(1, len(subs) // (workers * 8)) if subs else 1 + with ProcessPoolExecutor( + max_workers=workers, initializer=_init_worker, + initargs=(k_map, m_map, intercept_cols, dk_geno_prior)) as ex: + mapped = ex.map(_fit_one_task, subs, chunksize=chunksize) + if progress: + mapped = tqdm.tqdm(mapped, total=len(subs), + desc=f"fitting genotypes ({workers} workers)") + gfs = list(mapped) + + fits = {key: gf for key, gf in zip(keys, gfs)} + results_df = fits_to_results_df(fits) + return results_df, fits diff --git a/src/tfscreen/tfmodel/scripts/fit_genotypes_cli.py b/src/tfscreen/tfmodel/scripts/fit_genotypes_cli.py new file mode 100644 index 0000000..e94aea6 --- /dev/null +++ b/src/tfscreen/tfmodel/scripts/fit_genotypes_cli.py @@ -0,0 +1,172 @@ +""" +tfs-fit-genotypes: per-genotype MLE fits of the growth model against real data. + +A non-Bayesian, per-genotype alternative to the joint ``tfs-fit-model``: given +processed ``ln_cfu`` data and a *frozen* per-condition growth calibration +(``k``/``m`` from ``tfs-prefit-calibration``), it independently fits each +genotype's phenotype ``(dk_geno, theta_low, theta_high, log_hill_K, hill_n)`` +by nonlinear least squares (see :mod:`tfscreen.tfmodel.genotype_fit.fit`). + +An optional congression de-attenuation pass (``--congression_lambda``) removes +the co-transformation bias from the *bulk* genotypes' theta curves via the +iterative fixed point (predict theta -> de-attenuate against the population -> +refit Hill). It is inherently a population operation: the correction couples +all bulk genotypes through their shared background occupancy distribution. +Spiked genotypes (``--spiked_file``) are congression-free and pass through +untouched. + +Outputs (under ``--out_prefix``) +-------------------------------- +* ``_params.csv`` — one row per (genotype, titrant_name): the RAW MLE + fit (natural + transformed params, transformed-space std). +* ``_params_deattenuated.csv`` — same schema for the de-attenuated + fits (only when ``--congression_lambda`` is given). +* ``_theta.csv`` — long form ``[genotype, titrant_name, titrant_conc, + theta_raw]`` (plus ``theta_deattenuated`` when congression ran): the fitted + Hill curves evaluated on each titrant's concentration grid. +* ``_theta_history.csv`` — long form ``[genotype, titrant_name, + titrant_conc, iter, theta]``: the congression fixed-point trajectory (only + with ``--congression_lambda`` and ``--save_theta_history``). +""" + +import os + +from tfscreen.tfmodel.genotype_fit.fit import ( + fit_phenotypes, fits_to_results_df, predict_theta, +) +from tfscreen.util.io import read_dataframe +from tfscreen.util.cli import read_lines +from tfscreen.util.cli.generalized_main import generalized_main + + +def fit_genotypes(growth_file, + calibration_file, + out_prefix="tfs_mle", + congression_lambda=None, + spiked_file=None, + intercept_cols="replicate", + dk_geno_prior_sd=1.0, + min_obs=None, + num_workers=1, + save_theta_history=False): + """ + Fit the growth model to each genotype independently (frozen calibration). + + Parameters + ---------- + growth_file : str + Processed ``ln_cfu`` CSV (``tfs-process-counts`` output). + calibration_file : str + Frozen per-condition growth calibration: a ``tfs-prefit-calibration`` + priors CSV, or a wide ``condition_rep,growth_k,growth_m`` CSV. + out_prefix : str + Output prefix (see the module docstring for the files written). + congression_lambda : float, optional + Zero-truncated Poisson congression rate (the same lambda as the + simulator's ``transformation_poisson_lambda``). When given, run the + de-attenuation pass; omit for raw MLE fits only. + spiked_file : str, optional + Text file of congression-free (spiked) genotype names, one per line. + These are excluded from the de-attenuation correction and background. + Only relevant with ``--congression_lambda``. + intercept_cols : str + Comma-separated columns whose unique combinations each get a nuisance + ``ln_cfu0`` (default ``"replicate"``; empty string -> single intercept). + dk_geno_prior_sd : float + Std of the weak Normal prior on ``dk_geno`` (<=0 disables it). + min_obs : int, optional + Skip genotypes with fewer than this many usable observations. + num_workers : int + Parallelize the per-genotype fits over a process pool: ``1`` (default) + serial; ``-1`` uses ``os.cpu_count() - 1``; ``N`` uses ``N``. + save_theta_history : bool + Also write the congression fixed-point theta trajectory + (``_theta_history.csv``). Only meaningful with + ``--congression_lambda``. + """ + growth_df = read_dataframe(growth_file) + spiked = read_lines(spiked_file) if spiked_file else None + + icols = [c.strip() for c in str(intercept_cols).split(",") if c.strip()] + + dk_prior = None + if dk_geno_prior_sd is not None and float(dk_geno_prior_sd) > 0: + dk_prior = (0.0, float(dk_geno_prior_sd)) + + # --- Raw per-genotype MLE fits. --------------------------------------- + print("Fitting each genotype independently (frozen calibration)...", + flush=True) + results_df, fits = fit_phenotypes( + growth_df, calibration_file, intercept_cols=icols, + dk_geno_prior=dk_prior, min_obs=min_obs, num_workers=num_workers) + + theta_df = predict_theta(fits, growth_df, theta_col="theta_raw") + + # --- Optional congression de-attenuation. ----------------------------- + deatt_df = None + history_df = None + do_congression = (congression_lambda is not None + and float(congression_lambda) > 0) + if do_congression: + from tfscreen.tfmodel.genotype_fit.congression import ( + deattenuate_congression, + ) + print(f"De-attenuating congression " + f"(lambda={float(congression_lambda):g})...", flush=True) + result = deattenuate_congression( + fits, growth_df, float(congression_lambda), spiked=spiked, + return_theta_history=save_theta_history) + if save_theta_history: + corrected_fits, history_df = result + else: + corrected_fits = result + + deatt_df = fits_to_results_df(corrected_fits) + theta_deatt = predict_theta(corrected_fits, growth_df, + theta_col="theta_deattenuated") + theta_df = theta_df.merge( + theta_deatt, on=["genotype", "titrant_name", "titrant_conc"], + how="left") + + # --- Write outputs. --------------------------------------------------- + params_path = os.path.abspath(f"{out_prefix}_params.csv") + results_df.to_csv(params_path, index=False) + + theta_path = os.path.abspath(f"{out_prefix}_theta.csv") + theta_df.to_csv(theta_path, index=False) + + deatt_path = None + if deatt_df is not None: + deatt_path = os.path.abspath(f"{out_prefix}_params_deattenuated.csv") + deatt_df.to_csv(deatt_path, index=False) + + history_path = None + if history_df is not None: + history_path = os.path.abspath(f"{out_prefix}_theta_history.csv") + history_df.to_csv(history_path, index=False) + + bar = "=" * 72 + print(f"\n{bar}") + print(f"Fit {len(fits)} (genotype, titrant_name) groups.") + print("\n Per-genotype parameters (raw MLE fit):") + print(f" {params_path}") + if deatt_path is not None: + print(" Per-genotype parameters (congression-de-attenuated):") + print(f" {deatt_path}") + print(" Predicted theta vs (genotype, titrant_name, titrant_conc):") + print(f" {theta_path}") + if history_path is not None: + print(" Congression fixed-point theta trajectory:") + print(f" {history_path}") + print(bar) + + +def main(): + return generalized_main( + fit_genotypes, + manual_arg_types={"spiked_file": str, "congression_lambda": float, + "min_obs": int, "num_workers": int}) + + +if __name__ == "__main__": + main() diff --git a/tests/tfscreen/tfmodel/genotype_fit/__init__.py b/tests/tfscreen/tfmodel/genotype_fit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tfscreen/tfmodel/genotype_fit/test_congression.py b/tests/tfscreen/tfmodel/genotype_fit/test_congression.py new file mode 100644 index 0000000..4ea0820 --- /dev/null +++ b/tests/tfscreen/tfmodel/genotype_fit/test_congression.py @@ -0,0 +1,111 @@ +"""Tests for the congression de-attenuation history/trajectory outputs.""" + +import numpy as np +import jax.numpy as jnp +import pandas as pd +from scipy.special import expit, logit + +from tfscreen.tfmodel.generative.components.transformation._congression import ( + update_thetas, +) +from tfscreen.tfmodel.genotype_fit.congression import ( + correct_theta_matrix, + deattenuate_congression, +) +from tfscreen.tfmodel.genotype_fit.fit import GenotypeFit, PHENO_PARAMS_TRANSFORMED + + +def _forward(theta_true, lam): + return np.asarray(update_thetas( + jnp.asarray(theta_true), (float(lam),), theta_dist="empirical", + population_theta=jnp.asarray(theta_true))) + + +def _make_fit(genotype, theta_low, theta_high, log_K, n, dk=0.0): + est_t = np.array([5.0, dk, logit(theta_low), logit(theta_high), + log_K, np.log(n)]) + return GenotypeFit( + genotype=genotype, titrant_name="iptg", n_obs=100, converged=True, + param_names_t=["ln_cfu0[0]"] + list(PHENO_PARAMS_TRANSFORMED), + est_t=est_t, cov_t=np.eye(6) * 0.01, pheno_slice=slice(1, 6)) + + +def _build_fits(rng, n=25): + fits = {} + for i in range(n): + fits[(f"A{i}V", "iptg")] = _make_fit( + f"A{i}V", theta_low=rng.uniform(0.7, 0.98), + theta_high=rng.uniform(0.02, 0.5), + log_K=rng.uniform(np.log(1e-3), np.log(0.3)), n=rng.uniform(0.8, 2.0)) + fits[("wt", "iptg")] = _make_fit("wt", 0.99, 0.01, np.log(0.017), 2.0) + return fits + + +_CONCS = [0.0, 1e-3, 1e-2, 1e-1, 1.0] +_GROWTH_DF = pd.DataFrame({"titrant_name": ["iptg"] * len(_CONCS), + "titrant_conc": _CONCS}) + + +def test_correct_theta_matrix_history_matches_final(): + rng = np.random.default_rng(0) + theta_true = expit(rng.normal(0.0, 1.5, size=(2, 200))) + theta_obs = _forward(theta_true, lam=1.0) + + rec, n_iter, history = correct_theta_matrix( + theta_obs, lam=1.0, tol=1e-6, max_iter=200, return_history=True) + + # First entry is the clamped observed matrix; last is the returned estimate. + assert np.allclose(history[0], np.clip(theta_obs, 1e-6, 1 - 1e-6)) + assert np.allclose(history[-1], rec) + # Two-return form is unchanged (backward compatible). + rec2, n_iter2 = correct_theta_matrix(theta_obs, lam=1.0, tol=1e-6, + max_iter=200) + assert np.allclose(rec2, rec) + assert n_iter2 == n_iter + # The trajectory monotonically closes the gap to the observed population. + err0 = np.nanmax(np.abs(_forward(history[0], 1.0) - theta_obs)) + errN = np.nanmax(np.abs(_forward(history[-1], 1.0) - theta_obs)) + assert errN <= err0 + + +def test_correct_theta_matrix_history_lambda_zero(): + theta_obs = expit(np.random.default_rng(1).normal(0, 1, size=(1, 20))) + rec, n_iter, history = correct_theta_matrix(theta_obs, lam=0.0, + return_history=True) + assert n_iter == 0 + assert np.allclose(rec, theta_obs) + assert len(history) == 1 + + +def test_deattenuate_returns_history_df(): + rng = np.random.default_rng(3) + fits = _build_fits(rng) + bulk_keys = [k for k in fits if k[0] != "wt"] + + corrected, history = deattenuate_congression( + fits, _GROWTH_DF, lam=1.0, spiked={"wt"}, return_theta_history=True) + + assert list(history.columns) == [ + "genotype", "titrant_name", "titrant_conc", "iter", "theta"] + # History covers the bulk genotypes at every concentration, never wt. + assert set(history["genotype"]) == {k[0] for k in bulk_keys} + assert "wt" not in set(history["genotype"]) + assert set(history["titrant_conc"]) == set(_CONCS) + # iter starts at 0 and the correction actually iterated. + assert history["iter"].min() == 0 + assert history["iter"].max() >= 1 + + # The corrected fits match the no-history call. + corrected_only = deattenuate_congression( + fits, _GROWTH_DF, lam=1.0, spiked={"wt"}) + for k in fits: + assert np.allclose(corrected[k].est_t, corrected_only[k].est_t) + + +def test_deattenuate_history_noop_when_lambda_none(): + fits = _build_fits(np.random.default_rng(4)) + corrected, history = deattenuate_congression( + fits, _GROWTH_DF, lam=None, spiked={"wt"}, return_theta_history=True) + assert history.empty + for k in fits: + assert np.allclose(corrected[k].est_t, fits[k].est_t) diff --git a/tests/tfscreen/tfmodel/genotype_fit/test_fit.py b/tests/tfscreen/tfmodel/genotype_fit/test_fit.py new file mode 100644 index 0000000..f377d25 --- /dev/null +++ b/tests/tfscreen/tfmodel/genotype_fit/test_fit.py @@ -0,0 +1,114 @@ +"""Tests for the per-genotype MLE fitter's new reusable helpers.""" + +import numpy as np +import pandas as pd +import pytest + +from tfscreen.tfmodel.genotype_fit.fit import ( + _hill_theta, + fit_phenotypes, + fits_to_results_df, + predict_theta, + hill_theta_from_fit, +) + +# Frozen calibration: two selective markers with *different* m so dk_geno is +# separable from theta_low; one non-selective outgrowth. +CALIB = pd.DataFrame({ + "condition_rep": ["outgrowth", "kan", "phes"], + "growth_k": [0.02, 0.0, 0.0], + "growth_m": [0.0, -0.03, -0.06], +}) + +TRUTH = { + "wt": dict(theta_low=0.90, theta_high=0.08, log_K=np.log(3e-3), n=1.4, dk_geno=0.0), + "A1V": dict(theta_low=0.70, theta_high=0.20, log_K=np.log(3e-2), n=1.0, dk_geno=0.006), + "A2V": dict(theta_low=0.60, theta_high=0.30, log_K=np.log(1e-2), n=1.2, dk_geno=-0.004), +} +LN_CFU0 = {1: 10.0, 2: 10.5} +CONCS = [0.0, 1e-4, 1e-3, 1e-2, 1e-1, 1.0] +T_PRE = 5.0 +T_SELS = [0.0, 2.0, 4.0] +REPS = [1, 2] + + +def build_growth_df(truth=TRUTH, noise_sd=0.0, seed=0): + """Synthesize a real-shaped ln_cfu DataFrame from a TRUTH dict.""" + rng = np.random.default_rng(seed) + k = dict(zip(CALIB["condition_rep"], CALIB["growth_k"])) + m = dict(zip(CALIB["condition_rep"], CALIB["growth_m"])) + rows = [] + for geno, p in truth.items(): + for rep in REPS: + for conc in CONCS: + theta = _hill_theta(conc, p["theta_low"], p["theta_high"], + p["log_K"], p["n"]) + for cond_sel in ["outgrowth", "kan", "phes"]: + rate_pre = k["outgrowth"] + p["dk_geno"] + m["outgrowth"] * theta + rate_sel = k[cond_sel] + p["dk_geno"] + m[cond_sel] * theta + for t_sel in T_SELS: + ln_cfu = (LN_CFU0[rep] + rate_pre * T_PRE + + rate_sel * t_sel + + rng.normal(0.0, noise_sd)) + rows.append({ + "genotype": geno, "titrant_name": "iptg", + "titrant_conc": conc, "condition_pre": "outgrowth", + "condition_sel": cond_sel, "t_pre": T_PRE, + "t_sel": t_sel, "replicate": rep, + "ln_cfu": ln_cfu, "ln_cfu_std": 0.1}) + return pd.DataFrame(rows) + + +def test_fits_to_results_df_matches_fit_phenotypes(): + """Rebuilding the table from the fits dict reproduces results_df exactly.""" + df = build_growth_df(noise_sd=0.0) + results, fits = fit_phenotypes(df, CALIB, dk_geno_prior=None, progress=False) + rebuilt = fits_to_results_df(fits) + pd.testing.assert_frame_equal(results, rebuilt) + + +def test_fits_to_results_df_empty_warns(): + with pytest.warns(UserWarning, match="no fits"): + out = fits_to_results_df({}) + assert len(out) == 0 + + +def test_predict_theta_shape_and_values(): + df = build_growth_df(noise_sd=0.0) + _results, fits = fit_phenotypes(df, CALIB, dk_geno_prior=None, progress=False) + + theta_df = predict_theta(fits, df, theta_col="theta_raw") + + concs = np.sort(df["titrant_conc"].unique()) + assert list(theta_df.columns) == [ + "genotype", "titrant_name", "titrant_conc", "theta_raw"] + # One row per (genotype, titrant_name, titrant_conc). + assert len(theta_df) == len(TRUTH) * len(concs) + assert set(theta_df["genotype"]) == set(TRUTH) + + # Values match hill_theta_from_fit for a spot-checked genotype. + key = ("wt", "iptg") + got = (theta_df[theta_df["genotype"] == "wt"] + .sort_values("titrant_conc")["theta_raw"].to_numpy()) + exp = hill_theta_from_fit(fits[key], np.sort(concs)) + assert np.allclose(got, exp) + # Recovered theta is near the ground-truth Hill (noiseless). + truth_theta = _hill_theta(np.sort(concs), **{ + "theta_low": TRUTH["wt"]["theta_low"], + "theta_high": TRUTH["wt"]["theta_high"], + "log_K": TRUTH["wt"]["log_K"], "n": TRUTH["wt"]["n"]}) + assert np.allclose(got, truth_theta, atol=2e-3) + + +def test_predict_theta_nonfinite_fit_emits_nan(): + """A genotype whose fit failed (NaN est_t) yields NaN theta, not a drop.""" + df = build_growth_df(noise_sd=0.0) + _results, fits = fit_phenotypes(df, CALIB, dk_geno_prior=None, progress=False) + key = ("A1V", "iptg") + gf = fits[key] + fits[key] = gf._replace(est_t=np.full_like(gf.est_t, np.nan)) + + theta_df = predict_theta(fits, df, theta_col="theta_raw") + a1v = theta_df[theta_df["genotype"] == "A1V"] + assert len(a1v) == len(np.unique(df["titrant_conc"])) + assert a1v["theta_raw"].isna().all() diff --git a/tests/tfscreen/tfmodel/genotype_fit/test_fit_genotypes_cli.py b/tests/tfscreen/tfmodel/genotype_fit/test_fit_genotypes_cli.py new file mode 100644 index 0000000..c7b964b --- /dev/null +++ b/tests/tfscreen/tfmodel/genotype_fit/test_fit_genotypes_cli.py @@ -0,0 +1,122 @@ +"""End-to-end tests for the tfs-fit-genotypes CLI entry function.""" + +import numpy as np +import pandas as pd +import pytest + +from tfscreen.tfmodel.genotype_fit.fit import _hill_theta +from tfscreen.tfmodel.scripts.fit_genotypes_cli import fit_genotypes + +CALIB = pd.DataFrame({ + "condition_rep": ["outgrowth", "kan", "phes"], + "growth_k": [0.02, 0.0, 0.0], + "growth_m": [0.0, -0.03, -0.06], +}) +# wt is spiked; A1V/A2V are bulk (>=2 needed to build a congression background). +TRUTH = { + "wt": dict(theta_low=0.90, theta_high=0.08, log_K=np.log(3e-3), n=1.4, dk_geno=0.0), + "A1V": dict(theta_low=0.70, theta_high=0.20, log_K=np.log(3e-2), n=1.0, dk_geno=0.006), + "A2V": dict(theta_low=0.60, theta_high=0.30, log_K=np.log(1e-2), n=1.2, dk_geno=-0.004), +} +LN_CFU0 = {1: 10.0, 2: 10.5} +CONCS = [0.0, 1e-4, 1e-3, 1e-2, 1e-1, 1.0] + + +def _growth_df(): + k = dict(zip(CALIB["condition_rep"], CALIB["growth_k"])) + m = dict(zip(CALIB["condition_rep"], CALIB["growth_m"])) + rows = [] + for geno, p in TRUTH.items(): + for rep in (1, 2): + for conc in CONCS: + theta = _hill_theta(conc, p["theta_low"], p["theta_high"], + p["log_K"], p["n"]) + for cond_sel in ["outgrowth", "kan", "phes"]: + r_pre = k["outgrowth"] + p["dk_geno"] + m["outgrowth"] * theta + r_sel = k[cond_sel] + p["dk_geno"] + m[cond_sel] * theta + for t_sel in (0.0, 2.0, 4.0): + rows.append({ + "genotype": geno, "titrant_name": "iptg", + "titrant_conc": conc, "condition_pre": "outgrowth", + "condition_sel": cond_sel, "t_pre": 5.0, + "t_sel": t_sel, "replicate": rep, + "ln_cfu": LN_CFU0[rep] + r_pre * 5.0 + r_sel * t_sel, + "ln_cfu_std": 0.1}) + return pd.DataFrame(rows) + + +@pytest.fixture +def paths(tmp_path): + growth = tmp_path / "growth.csv" + _growth_df().to_csv(growth, index=False) + calib = tmp_path / "calib.csv" + CALIB.to_csv(calib, index=False) + return str(growth), str(calib), tmp_path + + +def test_raw_only_writes_params_and_theta(paths): + growth, calib, tmp_path = paths + prefix = str(tmp_path / "run") + fit_genotypes(growth, calib, out_prefix=prefix, dk_geno_prior_sd=0.0) + + params = pd.read_csv(f"{prefix}_params.csv") + theta = pd.read_csv(f"{prefix}_theta.csv") + + assert set(params["genotype"]) == set(TRUTH) + for col in ("theta_low", "theta_high", "log_hill_K", "hill_n", "dk_geno"): + assert col in params.columns + assert list(theta.columns) == [ + "genotype", "titrant_name", "titrant_conc", "theta_raw"] + + # No congression artefacts. + assert not (tmp_path / "run_params_deattenuated.csv").exists() + assert not (tmp_path / "run_theta_history.csv").exists() + + # Noiseless -> recovers ground-truth theta_low. + res = params.set_index("genotype") + for geno, p in TRUTH.items(): + assert res.loc[geno, "theta_low"] == pytest.approx(p["theta_low"], abs=2e-3) + + +def test_congression_writes_deattenuated_and_history(paths): + growth, calib, tmp_path = paths + spiked = tmp_path / "spiked.txt" + spiked.write_text("wt\n") + prefix = str(tmp_path / "run") + + fit_genotypes(growth, calib, out_prefix=prefix, dk_geno_prior_sd=0.0, + congression_lambda=1.0, spiked_file=str(spiked), + save_theta_history=True) + + deatt = pd.read_csv(f"{prefix}_params_deattenuated.csv") + theta = pd.read_csv(f"{prefix}_theta.csv") + history = pd.read_csv(f"{prefix}_theta_history.csv") + + assert set(deatt["genotype"]) == set(TRUTH) + assert "theta_deattenuated" in theta.columns + assert "theta_raw" in theta.columns + + # wt is spiked -> its de-attenuated theta equals its raw theta. + wt = theta[theta["genotype"] == "wt"] + assert np.allclose(wt["theta_raw"], wt["theta_deattenuated"]) + + # Bulk genotypes were corrected: at least one theta_deattenuated differs. + bulk = theta[theta["genotype"] != "wt"] + assert not np.allclose(bulk["theta_raw"], bulk["theta_deattenuated"]) + + # History covers only the bulk genotypes and iterates. + assert set(history["genotype"]) == {"A1V", "A2V"} + assert history["iter"].max() >= 1 + + +def test_congression_without_history_skips_history_file(paths): + growth, calib, tmp_path = paths + spiked = tmp_path / "spiked.txt" + spiked.write_text("wt\n") + prefix = str(tmp_path / "run") + + fit_genotypes(growth, calib, out_prefix=prefix, dk_geno_prior_sd=0.0, + congression_lambda=1.0, spiked_file=str(spiked)) + + assert (tmp_path / "run_params_deattenuated.csv").exists() + assert not (tmp_path / "run_theta_history.csv").exists() From b59f66c2ff9c77090e9f10932a7bb320c1cb317b Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Thu, 16 Jul 2026 09:59:37 -0700 Subject: [PATCH 04/26] updated batch sizing to avoid occasional OOM --- src/tfscreen/tfmodel/analysis/batch_sizing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tfscreen/tfmodel/analysis/batch_sizing.py b/src/tfscreen/tfmodel/analysis/batch_sizing.py index b070f3d..b5c1c61 100644 --- a/src/tfscreen/tfmodel/analysis/batch_sizing.py +++ b/src/tfscreen/tfmodel/analysis/batch_sizing.py @@ -16,7 +16,7 @@ # was ~6.5x the raw formula; this is rounded up for headroom, since GPU-side # overhead (not measurable on this CPU-only calibration) is expected to be # higher. Lower this if a genotype_batch_size sized with it still OOMs on GPU. -_DEFAULT_OVERHEAD_MULTIPLIER = 8.0 +_DEFAULT_OVERHEAD_MULTIPLIER = 6.5 # Fraction of total device memory to treat as usable budget, leaving # headroom for the host process, other tensors already resident, and the From 10c107ef3985513378b91da8d012ed3473c281d7 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Thu, 16 Jul 2026 20:44:51 -0700 Subject: [PATCH 05/26] added genotype subsetting on growth prediction --- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 8 +- reports/junit/junit.xml | 2 +- .../tfmodel/scripts/predict_growth_cli.py | 62 +++++- .../scripts/test_predict_growth_cli.py | 203 ++++++++++++++++++ 5 files changed, 269 insertions(+), 8 deletions(-) diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index fb3e408..25c50e6 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4028tests4028 \ No newline at end of file +tests: 4039tests4039 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 2bd51b9..26b1b43 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -3621,8 +3621,8 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:72: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:525:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:527:1: E305 expected 2 blank lines after class or function definition, found 1 -./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:124:27: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:139:5: F841 local variable 'binding_set' is assigned to but never used +./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:11:1: C901 'predict_growth' is too complex (15) +./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:139:27: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) ./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:129:23: E221 multiple spaces before operator ./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:130:23: E221 multiple spaces before operator @@ -7643,7 +7643,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -51 C901 'cat_fit' is too complex (19) +52 C901 'cat_fit' is too complex (19) 15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7686,7 +7686,7 @@ 232 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 -41 F841 local variable 'e' is assigned to but never used +40 F841 local variable 'e' is assigned to but never used 620 W291 trailing whitespace 85 W292 no newline at end of file 1582 W293 blank line contains whitespace diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 4140e86..c4b80b4 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py index ebd4498..0fbae4c 100644 --- a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py @@ -1,3 +1,4 @@ +import numpy as np import pandas as pd from tfscreen.tfmodel.configuration_io import read_configuration from tfscreen.tfmodel.inference.checkpoint_io import resolve_param_file @@ -16,7 +17,9 @@ def predict_growth(config_file, only_files=False, num_samples=0, num_marginal_samples=None, - genotype_batch_size=None): + genotype_batch_size=None, + subset_genotypes=False, + subset_seed=None): """ Predict growth signal (ln_cfu) from a fitted hierarchical model. @@ -85,6 +88,18 @@ def predict_growth(config_file, one JAX re-compilation per batch. If None (default), a batch size is estimated automatically from the available device memory and the per-genotype tensor cost; pass an explicit value to override. + subset_genotypes : bool, optional + If True, predict only a single memory-fit block of genotypes instead + of every genotype. The block size is the auto-sized (or explicit) + genotype_batch_size; the block always includes the binding genotypes, + the spiked genotypes, and any genotypes supplied via genotypes_file, + with the remainder of the block filled by a random sample of the other + genotypes. Intended for quickly assessing the input/output ln_cfu + correlation without paying for a full prediction sweep. Default False. + subset_seed : int or None, optional + Seed for the random draw used by subset_genotypes, making the sampled + block reproducible. Ignored unless subset_genotypes is True. Default + None (non-deterministic draw). """ file_genotypes = read_lines(genotypes_file) if genotypes_file else [] titrant_names = read_lines(titrant_names_file) if titrant_names_file else None @@ -138,6 +153,47 @@ def predict_growth(config_file, binding_genos = [] binding_set = set(binding_genos) + # Subset mode: predict a single memory-fit block of genotypes rather than + # every genotype, for a fast input/output ln_cfu correlation check. The + # block always keeps the binding, spiked, and file-specified genotypes; + # the rest of the block is a random sample of the remaining genotypes. + if subset_genotypes: + try: + spiked_list = orchestrator.settings.get("spiked_genotypes") or [] + spiked_set = set(str(g) for g in spiked_list) + except Exception: + spiked_set = set() + + universe = list(genotypes) + universe_set = set(universe) + # Mandatory-keep genotypes, restricted to those actually in the + # universe (file genotypes may be novel/absent under some paths). + keep_set = (binding_set | spiked_set | set(file_genotypes)) & universe_set + keep = [g for g in universe if g in keep_set] # preserve order + remaining = [g for g in universe if g not in keep_set] + + block = genotype_batch_size + n_random = max(0, block - len(keep)) + rng = np.random.default_rng(subset_seed) + if n_random < len(remaining): + idx = rng.choice(len(remaining), size=n_random, replace=False) + sampled = [remaining[i] for i in sorted(idx)] + else: + sampled = remaining + + genotypes = keep + sampled + + if len(keep) > block: + print(f"WARNING: {len(keep)} mandatory (binding/spiked/file) " + f"genotypes exceed the auto-sized block of {block}; " + f"predicting all of them in one call anyway.", flush=True) + print(f"Subset mode: predicting {len(genotypes)} genotypes " + f"({len(keep)} mandatory + {len(sampled)} random) in a single " + f"block.", flush=True) + + # Force a single predict() call: the subset is already one block. + genotype_batch_size = None + q_to_get = [0.5] if is_map else None print("Running growth predictions...", flush=True) @@ -197,7 +253,9 @@ def main(): "titrant_concs_file": str, "num_marginal_samples": int, "genotype_batch_size": int, - "only_files": bool}) + "subset_seed": int, + "only_files": bool, + "subset_genotypes": bool}) if __name__ == "__main__": diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py index 64658c9..10ca03d 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py @@ -639,3 +639,206 @@ def test_binding_geno_not_in_requested_genotypes_still_augments( # contain A1B rows. df = pd.read_csv(f"{tmp_path / 'out'}.csv") assert set(df["genotype"].unique()) == {"A1B"} + + +# --------------------------------------------------------------------------- +# --subset_genotypes: single memory-fit block for fast correlation checks +# --------------------------------------------------------------------------- + +@pytest.fixture +def mock_orchestrator_subset(): + """Orchestrator with 6 training genotypes, 'wt' binding, 'A1B' spiked.""" + orchestrator = MagicMock() + genos = ["wt", "A1B", "C2D", "E3F", "G4H", "I5K"] + orchestrator.growth_df = _make_growth_df(genos, [0.0, 1.0]) + orchestrator.growth_tm.tensor_shape = (1, 1, 1, 1, 1, 1, len(genos)) + orchestrator.binding_df = pd.DataFrame({ + "genotype": ["wt", "wt"], + "titrant_name": ["IPTG", "IPTG"], + "titrant_conc": [0.0, 1.0], + "theta_obs": [0.1, 0.9], + "theta_std": [0.01, 0.01], + }) + # settings is a real dict so .get("spiked_genotypes") works. + orchestrator.settings = {"spiked_genotypes": ["A1B"]} + return orchestrator + + +class TestPredictGrowthSubset: + """Tests for --subset_genotypes single-block sampling.""" + + def _make_fake_predict(self, orchestrator): + all_calls = [] + + def fake_predict(**kwargs): + all_calls.append(dict(kwargs)) + genotypes = kwargs.get("genotypes") or orchestrator.growth_df["genotype"].unique().tolist() + concs = kwargs.get("titrant_conc") or orchestrator.growth_df["titrant_conc"].unique().tolist() + rows = [{"genotype": g, "titrant_name": "IPTG", "titrant_conc": c, + "q0.5": 10.0} + for g in genotypes for c in concs] + return pd.DataFrame(rows) + + return fake_predict, all_calls + + def _patch_stack(self, orchestrator, fake_predict): + return [ + patch("tfscreen.tfmodel.scripts.predict_growth_cli.read_configuration", + return_value=(orchestrator, {})), + patch("tfscreen.tfmodel.scripts.predict_growth_cli.resolve_param_file", + side_effect=lambda pf, orch, op: pf), + patch("tfscreen.tfmodel.scripts.predict_growth_cli.predict", + side_effect=fake_predict), + patch("tfscreen.tfmodel.scripts.predict_growth_cli.load_posteriors", + side_effect=_fake_load_posteriors), + ] + + def test_subset_single_predict_call(self, mock_orchestrator_subset, tmp_path): + """Subset mode issues exactly one predict() call (no batching loop).""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=3) + assert len(all_calls) == 1 + + def test_subset_size_capped_at_block(self, mock_orchestrator_subset, tmp_path): + """The predicted genotype count does not exceed the block size.""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=3) + assert len(all_calls[0]["genotypes"]) == 3 + + def test_subset_always_includes_binding_and_spiked(self, mock_orchestrator_subset, tmp_path): + """Binding ('wt') and spiked ('A1B') genotypes are always in the block.""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=3, + subset_seed=0) + genos = set(all_calls[0]["genotypes"]) + assert "wt" in genos # binding + assert "A1B" in genos # spiked + + def test_subset_seed_is_deterministic(self, mock_orchestrator_subset, tmp_path): + """Same subset_seed → identical sampled block across runs.""" + results = [] + for _ in range(2): + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=3, + subset_seed=42) + results.append(sorted(all_calls[0]["genotypes"])) + assert results[0] == results[1] + + def test_subset_different_seeds_can_differ(self, mock_orchestrator_subset, tmp_path): + """Different seeds draw different random members (the two mandatory + genotypes are fixed; the one random slot should vary across seeds).""" + sampled = set() + for seed in range(6): + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=3, + subset_seed=seed) + # the non-mandatory member of the block + extra = set(all_calls[0]["genotypes"]) - {"wt", "A1B"} + sampled |= extra + assert len(sampled) > 1 + + def test_subset_block_larger_than_total_returns_all(self, mock_orchestrator_subset, tmp_path): + """Block >= number of genotypes → every genotype is predicted.""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=100) + assert set(all_calls[0]["genotypes"]) == set( + mock_orchestrator_subset.growth_df["genotype"].unique()) + + def test_subset_auto_size_predicts_single_block(self, mock_orchestrator_subset, tmp_path): + """With no explicit batch size, subset mode auto-sizes and still issues + a single predict call (auto block on CPU is large → all genotypes).""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True) + assert len(all_calls) == 1 + + def test_subset_keeps_file_genotypes(self, mock_orchestrator_subset, tmp_path): + """File-specified genotypes are always kept in the block. With a block + of 3 and 3 mandatory genotypes (binding + spiked + file), no random + members are drawn.""" + gf = str(tmp_path / "genos.txt") + _write_lines(gf, ["G4H"]) + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + genotypes_file=gf, + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=3) + assert set(all_calls[0]["genotypes"]) == {"wt", "A1B", "G4H"} + + def test_subset_mandatory_exceeds_block_still_single_call(self, mock_orchestrator_subset, tmp_path): + """When mandatory genotypes exceed the block, all are still predicted in + one call (binding genotypes cannot be dropped).""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=1) + assert len(all_calls) == 1 + genos = set(all_calls[0]["genotypes"]) + # both mandatory genotypes present despite block of 1 + assert {"wt", "A1B"} <= genos + + def test_subset_in_training_data_column(self, mock_orchestrator_subset, tmp_path): + """Sampled genotypes are all from training data → in_training_data==1.""" + fake_predict, _ = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + out = str(tmp_path / "out") + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=out, + subset_genotypes=True, + genotype_batch_size=3, + subset_seed=0) + df = pd.read_csv(f"{out}.csv") + assert (df["in_training_data"] == 1).all() + + def test_no_subset_predicts_all(self, mock_orchestrator_subset, tmp_path): + """subset_genotypes=False (default) predicts every genotype.""" + fake_predict, all_calls = self._make_fake_predict(mock_orchestrator_subset) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + out = str(tmp_path / "out") + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=out, + genotype_batch_size=100) + df = pd.read_csv(f"{out}.csv") + assert set(df["genotype"].unique()) == set( + mock_orchestrator_subset.growth_df["genotype"].unique()) From 335b83551ae86ca2f0c21819148b96f22fbdb34c Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Fri, 17 Jul 2026 09:49:53 -0700 Subject: [PATCH 06/26] better OOM handling in predict growth --- .../tfmodel/scripts/predict_growth_cli.py | 64 +++++++++- .../scripts/test_predict_growth_cli.py | 116 ++++++++++++++++++ 2 files changed, 176 insertions(+), 4 deletions(-) diff --git a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py index 0fbae4c..556abf2 100644 --- a/src/tfscreen/tfmodel/scripts/predict_growth_cli.py +++ b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py @@ -8,6 +8,60 @@ from tfscreen.util.cli import generalized_main, read_lines +def _is_oom_error(exc): + """True if an exception looks like a GPU/TPU out-of-memory error. + + JAX surfaces device OOM as a ``JaxRuntimeError`` whose message contains + ``RESOURCE_EXHAUSTED``; match on the message so we don't need to import the + specific error type (which varies across JAX versions). + """ + msg = str(exc) + return ("RESOURCE_EXHAUSTED" in msg + or "Out of memory" in msg + or "out of memory" in msg) + + +def _predict_subset_with_backoff(predict_kwargs, keep, extra): + """Run predict() on ``keep + extra`` genotypes, halving ``extra`` on OOM. + + The mandatory ``keep`` genotypes (binding/spiked/file) are always retained; + only the randomly-sampled ``extra`` genotypes are dropped when the device + runs out of memory. This makes subset mode robust to an over-optimistic + genotype_batch_size estimate: the memory-fit block is a sample already, so + shrinking it on OOM still yields a valid input/output correlation check. + Re-raises any non-OOM error, or an OOM that persists once ``extra`` is + empty (the mandatory anchors alone don't fit). + + Parameters + ---------- + predict_kwargs : dict + Keyword arguments forwarded to predict() (without ``genotypes``). + keep : list of str + Mandatory genotypes, always predicted. + extra : list of str + Optional genotypes, shrunk by half on each OOM retry. + + Returns + ------- + pandas.DataFrame + The predict() result for the largest block that fit. + """ + keep = list(keep) + extra = list(extra) + while True: + genotypes = keep + extra + try: + return predict(**predict_kwargs, genotypes=genotypes) + except Exception as exc: + if not _is_oom_error(exc) or len(extra) == 0: + raise + new_n = len(extra) // 2 + print(f" GPU out of memory at {len(genotypes)} genotypes; " + f"retrying with {len(keep) + new_n} " + f"({new_n} sampled + {len(keep)} mandatory)...", flush=True) + extra = extra[:new_n] + + def predict_growth(config_file, param_file, out_prefix="tfs_pred_growth", @@ -191,9 +245,6 @@ def predict_growth(config_file, f"({len(keep)} mandatory + {len(sampled)} random) in a single " f"block.", flush=True) - # Force a single predict() call: the subset is already one block. - genotype_batch_size = None - q_to_get = [0.5] if is_map else None print("Running growth predictions...", flush=True) @@ -207,7 +258,12 @@ def predict_growth(config_file, q_to_get=q_to_get, ) - if genotype_batch_size is not None and genotypes is not None and len(genotypes) > genotype_batch_size: + if subset_genotypes: + # Subset mode is always a single block; retry with fewer sampled + # genotypes if the device runs out of memory (the estimate is only a + # guess and can overshoot on GPU). + result_df = _predict_subset_with_backoff(predict_kwargs, keep, sampled) + elif genotype_batch_size is not None and genotypes is not None and len(genotypes) > genotype_batch_size: batches = [genotypes[i:i + genotype_batch_size] for i in range(0, len(genotypes), genotype_batch_size)] n_batches = len(batches) diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py index 10ca03d..7069c69 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py @@ -842,3 +842,119 @@ def test_no_subset_predicts_all(self, mock_orchestrator_subset, tmp_path): df = pd.read_csv(f"{out}.csv") assert set(df["genotype"].unique()) == set( mock_orchestrator_subset.growth_df["genotype"].unique()) + + +# --------------------------------------------------------------------------- +# --subset_genotypes: OOM back-off (shrink sampled block, keep mandatory) +# --------------------------------------------------------------------------- + +class _OOMError(RuntimeError): + """Stand-in for jax.errors.JaxRuntimeError; message matches _is_oom_error.""" + def __init__(self): + super().__init__("RESOURCE_EXHAUSTED: Out of memory while trying to " + "allocate 3446432000 bytes.") + + +class TestPredictGrowthSubsetBackoff: + """Tests for the OOM back-off in subset mode.""" + + def _make_oom_then_ok_predict(self, orchestrator, max_ok): + """Fake predict that OOMs while len(genotypes) > max_ok, else succeeds.""" + all_calls = [] + + def fake_predict(**kwargs): + all_calls.append(dict(kwargs)) + genotypes = kwargs.get("genotypes") + if genotypes is not None and len(genotypes) > max_ok: + raise _OOMError() + concs = kwargs.get("titrant_conc") or orchestrator.growth_df["titrant_conc"].unique().tolist() + rows = [{"genotype": g, "titrant_name": "IPTG", "titrant_conc": c, + "q0.5": 10.0} + for g in genotypes for c in concs] + return pd.DataFrame(rows) + + return fake_predict, all_calls + + def _patch_stack(self, orchestrator, fake_predict): + return [ + patch("tfscreen.tfmodel.scripts.predict_growth_cli.read_configuration", + return_value=(orchestrator, {})), + patch("tfscreen.tfmodel.scripts.predict_growth_cli.resolve_param_file", + side_effect=lambda pf, orch, op: pf), + patch("tfscreen.tfmodel.scripts.predict_growth_cli.predict", + side_effect=fake_predict), + patch("tfscreen.tfmodel.scripts.predict_growth_cli.load_posteriors", + side_effect=_fake_load_posteriors), + ] + + def test_backoff_shrinks_until_it_fits(self, mock_orchestrator_subset, tmp_path): + """A block that OOMs is retried with fewer genotypes until it fits.""" + # keep = {wt, A1B} (2); OOM whenever > 3 genotypes are requested. + fake_predict, all_calls = self._make_oom_then_ok_predict( + mock_orchestrator_subset, max_ok=3) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + out = str(tmp_path / "out") + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=out, + subset_genotypes=True, + genotype_batch_size=6, + subset_seed=0) + # More than one attempt was made, and the final (successful) call fit. + assert len(all_calls) > 1 + assert len(all_calls[-1]["genotypes"]) <= 3 + df = pd.read_csv(f"{out}.csv") + assert len(df) > 0 + + def test_backoff_retains_mandatory_genotypes(self, mock_orchestrator_subset, tmp_path): + """Binding/spiked genotypes survive every back-off step.""" + fake_predict, all_calls = self._make_oom_then_ok_predict( + mock_orchestrator_subset, max_ok=2) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + out = str(tmp_path / "out") + with patches[0], patches[1], patches[2], patches[3]: + predict_growth("cfg.yaml", "post.h5", + out_prefix=out, + subset_genotypes=True, + genotype_batch_size=6, + subset_seed=0) + # Every attempted call must contain the mandatory anchors. + for call in all_calls: + assert "wt" in call["genotypes"] + assert "A1B" in call["genotypes"] + df = pd.read_csv(f"{out}.csv") + assert {"wt", "A1B"} <= set(df["genotype"].unique()) + + def test_backoff_reraises_when_mandatory_alone_ooms(self, mock_orchestrator_subset, tmp_path): + """If even the mandatory anchors OOM, the error propagates.""" + # max_ok=1 → keep of 2 never fits; back-off exhausts extra then re-raises. + fake_predict, all_calls = self._make_oom_then_ok_predict( + mock_orchestrator_subset, max_ok=1) + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + with pytest.raises(_OOMError): + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=6, + subset_seed=0) + # Last attempt was the mandatory-only block (extra exhausted). + assert len(all_calls[-1]["genotypes"]) == 2 + + def test_non_oom_error_is_not_swallowed(self, mock_orchestrator_subset, tmp_path): + """A non-OOM error from predict() propagates immediately (no retry).""" + all_calls = [] + + def fake_predict(**kwargs): + all_calls.append(dict(kwargs)) + raise ValueError("something else went wrong") + + patches = self._patch_stack(mock_orchestrator_subset, fake_predict) + with patches[0], patches[1], patches[2], patches[3]: + with pytest.raises(ValueError, match="something else"): + predict_growth("cfg.yaml", "post.h5", + out_prefix=str(tmp_path / "out"), + subset_genotypes=True, + genotype_batch_size=6, + subset_seed=0) + assert len(all_calls) == 1 # no retries on a non-OOM error From 914738cbc909a54c04f0ce17ba7ae6f3d35f253a Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Fri, 17 Jul 2026 21:41:19 -0700 Subject: [PATCH 07/26] updated parallel cat response code --- CLAUDE.md | 1 + docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- docs/source/analysis.rst | 6 +- .../simulate-empirical/simulate_config.yaml | 178 ++++++++++++++++ examples/tfmodel/run.srun | 2 +- pyproject.toml | 1 + reports/flake.txt | 42 ++-- reports/junit/junit.xml | 2 +- .../cat_response/scripts/cat_response_cli.py | 68 ++++-- src/tfscreen/analysis/extract_epistasis.py | 7 + src/tfscreen/analysis/scripts/__init__.py | 0 .../analysis/scripts/extract_epistasis_cli.py | 98 +++++++++ src/tfscreen/tfmodel/genotype_fit/fit.py | 11 +- src/tfscreen/util/__init__.py | 4 + src/tfscreen/util/parallel.py | 27 +++ .../scripts/test_cat_response_cli.py | 193 +++++++++++------- .../scripts/test_extract_epistasis_cli.py | 164 +++++++++++++++ .../analysis/test_extract_epistasis.py | 21 +- tests/tfscreen/util/test_parallel.py | 44 ++++ 20 files changed, 742 insertions(+), 131 deletions(-) create mode 100644 examples/simulate-empirical/simulate_config.yaml create mode 100644 src/tfscreen/analysis/scripts/__init__.py create mode 100644 src/tfscreen/analysis/scripts/extract_epistasis_cli.py create mode 100644 src/tfscreen/util/parallel.py create mode 100644 tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py create mode 100644 tests/tfscreen/util/test_parallel.py diff --git a/CLAUDE.md b/CLAUDE.md index 806f2cd..90b76ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,7 @@ tfs-extract-params # Extract parameters from checkpoint tfs-predict-growth # Predict growth from fitted model tfs-predict-theta # Predict operator occupancy tfs-cat-response # Fit categorical response curves +tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table tfs-diagnose-nan # Diagnose NaN issues in inference tfs-simulate # Simulate a full experiment tfs-report-cfu0 # Report average ln_cfu0 by genotype class from a simulate config diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 9b5a50e..92cb9a7 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.13%coverage96.13% \ No newline at end of file +coverage: 96.17%coverage96.17% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 25c50e6..36c3443 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4039tests4039 \ No newline at end of file +tests: 4071tests4071 \ No newline at end of file diff --git a/docs/source/analysis.rst b/docs/source/analysis.rst index bbf1fab..554a9ca 100644 --- a/docs/source/analysis.rst +++ b/docs/source/analysis.rst @@ -232,7 +232,11 @@ Fits categorical response curve models to the *θ*-vs-titrant output of tfs-cat-response \ tfs_theta_pred.csv \ - --workers 8 + --num_workers -1 + +The per-pair fits are embarrassingly parallel. ``--num_workers`` defaults to +``-1`` (use ``os.cpu_count() - 1`` processes); pass ``1`` to run serially or a +positive integer to pin the worker count. Output (default ``--out_prefix tfs_cat_response``): diff --git a/examples/simulate-empirical/simulate_config.yaml b/examples/simulate-empirical/simulate_config.yaml new file mode 100644 index 0000000..72c9530 --- /dev/null +++ b/examples/simulate-empirical/simulate_config.yaml @@ -0,0 +1,178 @@ +# =========================================================================== +# Empirical-phenotype simulation config +# =========================================================================== +# +# This is an ordinary tfs-simulate config with one change: instead of drawing +# each genotype's phenotype (theta curve + dk_geno) from made-up priors, it +# RESAMPLES them from a PopulationModel that tfs-build-empirical fit +# to REAL screen data. The simulated library therefore has a phenotype +# distribution shaped like reality, while ground truth stays known. +# +# Workflow: +# # 1. real data -> empirical generating distribution (writes emp_phenotype_model.json) +# tfs-build-empirical growth.csv 42 \ +# --binding_file binding.csv --spiked_file spiked.txt --out_prefix emp +# # 2. simulate from it +# tfs-simulate simulate_config.yaml out_dir/ +# +# See the "Empirical phenotype pipeline" section of CLAUDE.md for the full story. + +# --------------------------------------------------------------------------- +# Empirical phenotype source (the only empirical-specific keys) +# --------------------------------------------------------------------------- + +# Resample every library genotype's phenotype from the fitted distribution +# rather than prior-predictive sampling. Omit this key (or set "prior") to get +# the ordinary fully-synthetic simulation. +phenotype_source: empirical + +empirical: + # The single self-contained JSON model file that tfs-build-empirical + # writes (_phenotype_model.json) -- it prints this exact path at + # the end of its run. Use an ABSOLUTE path so it resolves no matter which + # directory you run tfs-simulate from (no copying files around). + phenotype_model: /abs/path/to/tfs_empirical_phenotype_model.json + +# What empirical mode overrides vs. what this config still controls: +# OVERRIDDEN by the resampled model (values below are ignored): +# - theta curves (no theta_sim_priors / theta_priors needed) +# - per-genotype dk_geno (the dk_geno_hyper_* keys are still read by the +# loader but their values do not matter) +# - activity is forced to 1 (A is absorbed into the fitted theta for a +# repressor; activity_wt / activity_mut_scale ignored) +# STILL CONTROLLED by this config: +# - the growth linkage (m/b per condition, below) -- MUST be the calibrated +# values (see the growth block) +# - conditions, library composition, sequencing/noise, congression, binding +# coverage, etc. + +# --------------------------------------------------------------------------- +# Library genetic information (match your real experiment) +# --------------------------------------------------------------------------- +reading_frame: 0 +first_amplicon_residue: 27 +wt_seq: gccagccacgtttctgcgaaaacgcgggaaaaagtggaagcggcgatggcggagctgaattacattcccaaccgcgtggcacaacaactggcgggcaaacagtcgttgctgattggcgttgccacctccagtctggccctgcacgcgccgtcgcaaattgtcgcggcgattaaatctcgcgccgatcaactgggtgccagcgtggtggtgtcgatg +degen_sites: ......nntnnt.............................................................................................nntnnt......................................................................................................... +tiles: ......111111.............................................................................................222222......................................................................................................... +expected_5p: cgcgtggtgaaccag +expected_3p: gtagaacgaagcggc +tile_combos: ["single-1", "single-2", "double-1-2"] +spiked_seqs: +- gccagccacgtttctgcgaaaacgcgggaaaaagtggaagcggcgatggcggagctgaattacattcccaaccgcgtggcacaacaactggcgggcaaacagtcgttgctgattggcgttgccacctccagtctggccctggcggcgccgtcgcaaattgtcgcggcgattctgtctcgcgccgatcaactgggtgccagcgtggtggtgtcgatg # k84l +- gccagccacgtttctgcgaaaacgcgggaaaaagtggaagcggcgattgcggagctgaattacattcccaaccgcgtggcacaacaactggcgggcaaacagtcgttgctgattggcgttgccacctccagtctggccctgcacgcgccgtcgcaaattgtcgcggcgattctgtctcgcgccgatcaactgggtgccagcgtggtggtgtcgatg # m42i/k84l + +# --------------------------------------------------------------------------- +# Phenotype calculation +# --------------------------------------------------------------------------- + +# In empirical mode this is FORCED to "hill_geno" (the resampled phenotypes are +# per-genotype Hill curves); any other value is ignored with a warning. Set it +# to hill_geno to avoid the warning and skip an expensive discarded prior draw. +theta_component: "hill_geno" + +# --------------------------------------------------------------------------- +# Conditions (match the conditions the calibration was fit on) +# --------------------------------------------------------------------------- +condition_blocks: + - library: "kanR" + titrant_name: "iptg" + titrant_conc: [0, 0.0001, 0.001, 0.003] + condition_pre: "kanR-kan" + t_pre: 30 + condition_sel: "kanR+kan" + t_sel: [160, 180, 200] + + - library: "kanR" + titrant_name: "iptg" + titrant_conc: [0.01, 0.03, 0.1, 1.0] + condition_pre: "kanR-kan" + t_pre: 30 + condition_sel: "kanR+kan" + t_sel: [145, 165, 180] + + - library: "pheS" + titrant_name: "iptg" + titrant_conc: [0, 0.0001, 0.001, 0.003, 0.01, 0.03, 0.1, 1.0] + condition_pre: "pheS-4CP" + t_pre: 30 + condition_sel: "pheS+4CP" + t_sel: [95, 110, 125] + +# --------------------------------------------------------------------------- +# Growth rate parameters +# --------------------------------------------------------------------------- + +# Linear growth linkage per condition: k = m * theta + b. +# +# IMPORTANT: for a self-consistent empirical simulation these MUST be the same +# calibrated values the phenotype fit used -- i.e. the per-condition values the +# pre-fit wrote into _configure_priors.csv. For each condition_rep: +# b = value of parameter growth.condition_growth.k_loc (baseline) +# m = value of parameter growth.condition_growth.m_loc (slope) +# (The numbers below are placeholders -- replace them with your calibration.) +growth: + kanR+kan: {m: -0.009933, b: 0.000000} + kanR-kan: {m: 0.000808, b: 0.004670} + pheS+4CP: {m: 0.006226, b: 0.010741} + pheS-4CP: {m: -0.000344, b: 0.017862} + +# Required by the config loader, but OVERRIDDEN per genotype in empirical mode +# (dk_geno comes from the resampled model). Values here have no effect. +dk_geno_hyper_loc: -3.5 +dk_geno_hyper_scale: 1.0 +dk_geno_hyper_shift: 0.02 + +# --------------------------------------------------------------------------- +# Experimental simulation parameters (tune to match your real library) +# --------------------------------------------------------------------------- +transform_sizes: + single-1: 10_000 + single-2: 10_000 + double-1-2: 1_000_000 + spiked: 1000 + +library_mixture: + single-1: 100 + single-2: 100 + double-1-2: 5 + spiked: 4 + +lib_assembly_skew_sigma: 1.25 + +# Congression: plasmids per cell ~ zero-truncated Poisson(lambda). This is the +# attenuation your empirical distribution is (currently) uncorrected for, so +# keep it matched to the real library. +transformation_poisson_lambda: 1.5 +multi_plasmid_combine_fcn: + kanR: "min" + pheS: "max" + +cfu0: 65_000_000.0 +tube_noise_sigma: 0.002 +total_num_reads: 25_000_000 +prob_index_hop: 0.05 + +# Random seed for the simulation (independent of the fit's seed). +seed: null + +# --------------------------------------------------------------------------- +# Binding data (regenerated from the resampled phenotypes) +# --------------------------------------------------------------------------- + +# In empirical mode the binding CSV is rebuilt from each resampled genotype's +# Hill curve (this block chooses WHICH genotypes are measured and the noise; +# the theta VALUES come from the model, not a separate draw). Keep coverage + +# noise matched to your real binding assay -- the library_binding anchors are +# what identify the growth slope m from data. +binding_data: + titrant_name: iptg + titrant_conc: [0, 0.0001, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0] + noise: 0.02 + # Clean, monoclonal controls (pool = spiked_seqs). + spiked_binding: + choose_by: stratified + num: 4 + # In-library controls from the bulk (congression-affected). Pool = non-spiked. + library_binding: + num: 20 + choose_by: stratified diff --git a/examples/tfmodel/run.srun b/examples/tfmodel/run.srun index c4b9261..ea86e6e 100644 --- a/examples/tfmodel/run.srun +++ b/examples/tfmodel/run.srun @@ -94,4 +94,4 @@ tfs-predict-theta \ echo ">>> Categorize response" tfs-cat-response \ tfs_theta_pred.csv \ - --workers 8 + --num_workers -1 diff --git a/pyproject.toml b/pyproject.toml index 24cb4fb..d204731 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ tfs-extract-params = "tfscreen.tfmodel.scripts.extract_params_cli:main" tfs-predict-growth = "tfscreen.tfmodel.scripts.predict_growth_cli:main" tfs-predict-theta = "tfscreen.tfmodel.scripts.predict_theta_cli:main" tfs-cat-response = "tfscreen.analysis.cat_response.scripts.cat_response_cli:main" +tfs-extract-epistasis = "tfscreen.analysis.scripts.extract_epistasis_cli:main" tfs-diagnose-nan = "tfscreen.tfmodel.scripts.diagnose_nan_cli:main" tfs-simulate = "tfscreen.simulate.scripts.simulate_cli:main" tfs-report-cfu0 = "tfscreen.simulate.scripts.report_cfu0_cli:main" diff --git a/reports/flake.txt b/reports/flake.txt index 26b1b43..7d81f74 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -70,9 +70,8 @@ ./src/tfscreen/analysis/cat_response/cat_response.py:134:34: E231 missing whitespace after ',' ./src/tfscreen/analysis/cat_response/cat_response.py:138:42: E231 missing whitespace after ',' ./src/tfscreen/analysis/cat_response/cat_response.py:141:56: W292 no newline at end of file -./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:12:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:21:1: C901 'cat_response' is too complex (11) -./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:91:56: E127 continuation line over-indented for visual indent +./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:38:1: C901 'cat_response' is too complex (14) +./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:111:56: E127 continuation line over-indented for visual indent ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/extract_epistasis.py:33:77: W291 trailing whitespace @@ -83,7 +82,7 @@ ./src/tfscreen/analysis/extract_epistasis.py:100:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:113:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:125:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:129:1: C901 'extract_epistasis' is too complex (12) +./src/tfscreen/analysis/extract_epistasis.py:129:1: C901 'extract_epistasis' is too complex (13) ./src/tfscreen/analysis/extract_epistasis.py:133:47: E252 missing whitespace around parameter equals ./src/tfscreen/analysis/extract_epistasis.py:133:48: E252 missing whitespace around parameter equals ./src/tfscreen/analysis/extract_epistasis.py:159:75: W291 trailing whitespace @@ -91,16 +90,16 @@ ./src/tfscreen/analysis/extract_epistasis.py:184:27: E231 missing whitespace after ',' ./src/tfscreen/analysis/extract_epistasis.py:187:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:192:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:201:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:208:55: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:208:60: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:208:65: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:210:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:216:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:208:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:215:55: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:215:60: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:215:65: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:217:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:223:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:229:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:231:10: W291 trailing whitespace -./src/tfscreen/analysis/extract_epistasis.py:233:30: W291 trailing whitespace +./src/tfscreen/analysis/extract_epistasis.py:230:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:236:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:238:10: W291 trailing whitespace +./src/tfscreen/analysis/extract_epistasis.py:240:30: W291 trailing whitespace ./src/tfscreen/analysis/stats_test_suite.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/stats_test_suite.py:8:31: E231 missing whitespace after ',' ./src/tfscreen/analysis/stats_test_suite.py:8:42: E231 missing whitespace after ',' @@ -3621,8 +3620,8 @@ ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:524:72: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:525:74: E231 missing whitespace after ':' ./src/tfscreen/tfmodel/scripts/fit_model_cli.py:527:1: E305 expected 2 blank lines after class or function definition, found 1 -./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:11:1: C901 'predict_growth' is too complex (15) -./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:139:27: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:65:1: C901 'predict_growth' is too complex (16) +./src/tfscreen/tfmodel/scripts/predict_growth_cli.py:193:27: E128 continuation line under-indented for visual indent ./src/tfscreen/tfmodel/scripts/predict_theta_cli.py:12:1: C901 'predict_theta' is too complex (11) ./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:129:23: E221 multiple spaces before operator ./src/tfscreen/tfmodel/scripts/prefit_calibration_cli.py:130:23: E221 multiple spaces before operator @@ -3729,7 +3728,7 @@ ./src/tfscreen/tfmodel/tensors/tensor_manager.py:512:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/tensors/tensor_manager.py:517:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/tensors/tensor_manager.py:528:1: W391 blank line at end of file -./src/tfscreen/util/__init__.py:71:2: W292 no newline at end of file +./src/tfscreen/util/__init__.py:75:2: W292 no newline at end of file ./src/tfscreen/util/cli/__init__.py:16:1: W391 blank line at end of file ./src/tfscreen/util/cli/generalized_main.py:1:11: W291 trailing whitespace ./src/tfscreen/util/cli/generalized_main.py:5:1: C901 'generalized_main' is too complex (15) @@ -4066,9 +4065,6 @@ ./tests/smoke-tests/test_run_growth_analysis_smoke.py:37:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:45:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:48:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py:14:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py:19:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py:24:1: E305 expected 2 blank lines after class or function definition, found 1 ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:4:1: F401 'pandas as pd' imported but unused ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:10:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:17:1: E302 expected 2 blank lines, found 1 @@ -4217,7 +4213,7 @@ ./tests/tfscreen/analysis/test_extract_epistasis.py:220:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:226:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:228:74: E231 missing whitespace after ',' -./tests/tfscreen/analysis/test_extract_epistasis.py:229:55: W292 no newline at end of file +./tests/tfscreen/analysis/test_extract_epistasis.py:248:28: W292 no newline at end of file ./tests/tfscreen/analysis/test_stats_test_suite.py:6:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_stats_test_suite.py:9:52: E261 at least two spaces before inline comment ./tests/tfscreen/analysis/test_stats_test_suite.py:10:1: W293 blank line contains whitespace @@ -7670,9 +7666,9 @@ 2 E266 too many leading '#' for block comment 149 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -780 E302 expected 2 blank lines, found 1 +777 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) -20 E305 expected 2 blank lines after class or function definition, found 1 +19 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line 38 E402 module level import not at top of file @@ -7691,4 +7687,4 @@ 85 W292 no newline at end of file 1582 W293 blank line contains whitespace 30 W391 blank line at end of file -7645 +7641 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index c4b80b4..21f16d6 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py b/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py index 236f58f..f3702b6 100644 --- a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py @@ -3,27 +3,44 @@ """ import pandas as pd -from concurrent.futures import ProcessPoolExecutor, as_completed +from concurrent.futures import ProcessPoolExecutor from tfscreen.analysis.cat_response.cat_fit import cat_fit from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.util import resolve_workers from tfscreen.util.cli.generalized_main import generalized_main -def _fit_one(args): - """Worker: run cat_fit for one (genotype, titrant_name) pair.""" - genotype, titrant_name, x, y, y_std, models_to_run = args +# Number of (genotype, titrant_name) pairs bundled into each worker task. Larger +# chunks amortize the per-task pickle/IPC overhead of ProcessPoolExecutor, which +# matters a lot when there are hundreds of thousands of pairs. +_CHUNK_SIZE = 200 + + +def _fit_one(genotype, titrant_name, x, y, y_std, models_to_run): + """Run cat_fit for one (genotype, titrant_name) pair.""" flat_out, _ = cat_fit(x, y, y_std, models_to_run=models_to_run) flat_out["genotype"] = genotype flat_out["titrant_name"] = titrant_name return flat_out +def _fit_chunk(chunk): + """Worker: run cat_fit for a list of work items, preserving order.""" + return [_fit_one(*item) for item in chunk] + + +def _iter_chunks(work_items, chunk_size): + """Yield successive length-``chunk_size`` slices of ``work_items``.""" + for start in range(0, len(work_items), chunk_size): + yield work_items[start:start + chunk_size] + + def cat_response(theta_file, out_prefix="tfs_cat_response", theta_col=None, sigma_col=None, models=None, - workers=1): + num_workers=-1): """ Classify each genotype's theta-vs-titrant curve using categorical response models. @@ -51,8 +68,11 @@ def cat_response(theta_file, upper_std and lower_std columns to be present. models : list of str or None, optional Response models to fit. Defaults to all models in MODEL_LIBRARY. - workers : int, optional - Number of parallel worker processes (default 1). + num_workers : int, optional + Number of parallel worker processes. ``1`` runs serially in-process; + ``-1`` (the default) uses ``os.cpu_count() - 1``; ``N`` uses ``N`` + processes. The per-pair fits are embarrassingly parallel, so this scales + nearly linearly on large libraries. """ if models is None: models = list(MODEL_LIBRARY.keys()) @@ -95,21 +115,33 @@ def cat_response(theta_file, work_items.append((genotype, titrant_name, x, y, y_std, models)) n_total = len(work_items) - results = [None] * n_total - idx_map = {id(item): i for i, item in enumerate(work_items)} + workers = resolve_workers(num_workers) + chunks = list(_iter_chunks(work_items, _CHUNK_SIZE)) print(f" Fitting {n_total} (genotype, titrant_name) pairs " f"with {workers} worker(s)...", flush=True) - with ProcessPoolExecutor(max_workers=workers) as executor: - futures = {executor.submit(_fit_one, item): idx_map[id(item)] - for item in work_items} - n_done = 0 - for future in as_completed(futures): - results[futures[future]] = future.result() - n_done += 1 - if n_done % 5000 == 0 or n_done == n_total: - print(f" {n_done}/{n_total} fits complete", flush=True) + results = [] + n_done = 0 + + def _report(n_done): + if n_done % 5000 < _CHUNK_SIZE or n_done == n_total: + print(f" {n_done}/{n_total} fits complete", flush=True) + + if workers == 1: + # Serial fast-path: run in-process, no pickling/IPC overhead. + for chunk in chunks: + results.extend(_fit_chunk(chunk)) + n_done += len(chunk) + _report(n_done) + else: + # executor.map preserves input order, so results stay aligned with + # work_items without an explicit index map. + with ProcessPoolExecutor(max_workers=workers) as executor: + for chunk_result in executor.map(_fit_chunk, chunks): + results.extend(chunk_result) + n_done += len(chunk_result) + _report(n_done) out_df = pd.DataFrame(results) id_cols = ["genotype", "titrant_name"] diff --git a/src/tfscreen/analysis/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index 616b2d1..7e58dc7 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -194,6 +194,13 @@ def extract_epistasis( cycles = mutant_cycle_pivot(df, extract_columns=extract_columns, condition_selector=condition_selector) + + # No valid mutant cycles were found (e.g. no double mutants, or no wt). The + # pivot returns an empty frame with no cycle columns, so short-circuit before + # attempting to select/compute on columns that do not exist. + if cycles.empty: + return cycles + # Drop extra columns if not keep_extra: diff --git a/src/tfscreen/analysis/scripts/__init__.py b/src/tfscreen/analysis/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py new file mode 100644 index 0000000..115ee21 --- /dev/null +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -0,0 +1,98 @@ +""" +CLI for calculating second-order epistasis from a long-form observable table. +""" + +import pandas as pd + +from tfscreen.analysis.extract_epistasis import ( + extract_epistasis as _extract_epistasis, +) +from tfscreen.util.cli import generalized_main + + +def extract_epistasis(data_file, + y_obs, + out_prefix="tfs_epistasis", + y_std=None, + condition_selector=None, + scale="add", + keep_extra=False): + """ + Calculate second-order epistasis for pairs of mutations. + + Reads a long-form CSV (one row per genotype, with genotypes in the format + 'MUT1/MUT2') and, for every double mutant, builds a mutant cycle from its + two single-mutant parents and the wildtype. Epistasis is calculated on the + requested observable and written, one row per double mutant, to + {out_prefix}.csv. + + Parameters + ---------- + data_file : str + Path to the input CSV. Must contain a 'genotype' column, the column + named by y_obs, and (if given) the columns named by y_std and + condition_selector. + y_obs : str + Name of the column holding the observable for which epistasis is + calculated (e.g. 'fitness', 'dG'). + out_prefix : str, optional + Prefix for the output CSV file, written to {out_prefix}.csv. + Default 'tfs_epistasis'. + y_std : str or None, optional + Name of the column holding the standard error of y_obs. If given, the + epistasis error (ep_std) is propagated and written. + condition_selector : list of str or None, optional + One or more column names that define a unique experimental condition. + Epistasis is calculated independently within each condition. If omitted, + the whole table is treated as a single condition. + scale : {"add", "mult"}, optional + Epistatic scale. "add" (default): (Y11 - Y10) - (Y01 - Y00). + "mult": (Y11 / Y10) / (Y01 / Y00). + keep_extra : bool, optional + If True, retain all columns from the input CSV in the output. If False + (default), keep only the identifier columns and the calculated epistasis + values. + """ + print(f"Reading {data_file}...", flush=True) + df = pd.read_csv(data_file) + + # Fail fast on missing columns rather than deep inside the pivot. + required = ["genotype", y_obs] + if y_std is not None: + required.append(y_std) + if condition_selector is not None: + required.extend(condition_selector) + + missing = [c for c in required if c not in df.columns] + if missing: + raise ValueError( + f"Input file '{data_file}' is missing required column(s): {missing}. " + f"Available columns: {list(df.columns)}" + ) + + result = _extract_epistasis(df, + y_obs=y_obs, + y_std=y_std, + condition_selector=condition_selector, + scale=scale, + keep_extra=keep_extra) + + out_file = f"{out_prefix}.csv" + if result.empty: + print("Warning: no valid mutant cycles found; writing empty output.", + flush=True) + + result.to_csv(out_file, index=False) + print(f"Wrote {len(result)} rows to {out_file}", flush=True) + + +def main(): + generalized_main(extract_epistasis, + manual_arg_types={"y_std": str, + "condition_selector": str, + "scale": str}, + manual_arg_nargs={"condition_selector": "+"}) + + +if __name__ == "__main__": + main() diff --git a/src/tfscreen/tfmodel/genotype_fit/fit.py b/src/tfscreen/tfmodel/genotype_fit/fit.py index a8416df..c2c85de 100644 --- a/src/tfscreen/tfmodel/genotype_fit/fit.py +++ b/src/tfscreen/tfmodel/genotype_fit/fit.py @@ -61,13 +61,13 @@ import pandas as pd from scipy.special import expit, logit import tqdm -import os import warnings from collections import namedtuple from concurrent.futures import ProcessPoolExecutor from tfscreen.util.io import read_dataframe from tfscreen.util.dataframe import check_columns, get_scaled_cfu +from tfscreen.util import resolve_workers as _resolve_workers from tfscreen.mle.fitters.least_squares import run_least_squares # Numerical guards, matched to tfscreen.mle.curve_models.models so the fitted @@ -536,15 +536,6 @@ def _fit_one_task(sub): dk_geno_prior=_WORKER_STATE["dk_geno_prior"]) -def _resolve_workers(num_workers): - """joblib-style worker count: 1 -> serial, -1 -> cpu_count-1, N -> N.""" - if num_workers is None or int(num_workers) == 1: - return 1 - if int(num_workers) < 0: - return max(1, (os.cpu_count() or 2) - 1) - return int(num_workers) - - def fit_phenotypes(growth_df, calib, intercept_cols=("replicate",), diff --git a/src/tfscreen/util/__init__.py b/src/tfscreen/util/__init__.py index e33eba3..5034d5a 100644 --- a/src/tfscreen/util/__init__.py +++ b/src/tfscreen/util/__init__.py @@ -16,6 +16,10 @@ broadcast_args ) +from .parallel import ( # noqa: F401 + resolve_workers +) + from .dataframe import ( # noqa: F401 check_columns ) diff --git a/src/tfscreen/util/parallel.py b/src/tfscreen/util/parallel.py new file mode 100644 index 0000000..8fe0951 --- /dev/null +++ b/src/tfscreen/util/parallel.py @@ -0,0 +1,27 @@ +""" +Shared helpers for CPU-parallel work dispatch across CLIs. +""" + +import os + + +def resolve_workers(num_workers): + """ + Resolve a joblib-style worker count to a concrete number of processes. + + Parameters + ---------- + num_workers : int or None + ``None`` or ``1`` -> serial (returns 1); ``-1`` (or any negative) -> + ``os.cpu_count() - 1`` (at least 1); ``N`` -> ``N``. + + Returns + ------- + int + Concrete, positive worker count (>= 1). + """ + if num_workers is None or int(num_workers) == 1: + return 1 + if int(num_workers) < 0: + return max(1, (os.cpu_count() or 2) - 1) + return int(num_workers) diff --git a/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py index d47051a..656df54 100644 --- a/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py @@ -1,33 +1,22 @@ """ -Tests for cat_response_cli.py — theta_col auto-detection and error handling. +Tests for cat_response_cli.py — theta_col auto-detection, error handling, and +the serial/parallel/chunked dispatch machinery. """ import pytest +import numpy as np import pandas as pd from unittest.mock import patch -from tfscreen.analysis.cat_response.scripts.cat_response_cli import cat_response - - -# Run ProcessPoolExecutor synchronously so cat_fit mocks work in-process. -from concurrent.futures import Future as _Future - -class _SyncFuture(_Future): - def __init__(self, result_val): - super().__init__() - self.set_result(result_val) - -class _SyncExecutor: - def __enter__(self): return self - def __exit__(self, *a): pass - def submit(self, fn, *args): return _SyncFuture(fn(*args)) - -_SYNC_EXECUTOR = patch( - "tfscreen.analysis.cat_response.scripts.cat_response_cli.ProcessPoolExecutor", - return_value=_SyncExecutor(), +from tfscreen.analysis.cat_response.scripts import cat_response_cli +from tfscreen.analysis.cat_response.scripts.cat_response_cli import ( + cat_response, + _fit_one, + _fit_chunk, + _iter_chunks, ) -_FLAT_RESULT = {"genotype": "wt", "titrant_name": "IPTG", - "best_model": "flat", "status": "success"} + +_FLAT_RESULT = {"best_model": "flat", "status": "success"} def _make_theta_df(center_col): @@ -42,85 +31,56 @@ def _make_theta_df(center_col): class TestThetaColAutoDetect: + """theta_col / sigma_col resolution. Driven serially (num_workers=1) so the + cat_fit mock runs in-process.""" - def test_autodetects_q0_5(self, tmp_path): - """Values from q0.5 column are passed to the fitter.""" + def _run_capture(self, tmp_path, df, **kwargs): f = str(tmp_path / "theta.csv") captured = {} def fake_fit(x, y, y_std, models_to_run): captured["y"] = list(y) - return (_FLAT_RESULT, None) + captured["y_std"] = list(y_std) + return (dict(_FLAT_RESULT), None) - df = _make_theta_df("q0.5") with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", return_value=df), \ patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - side_effect=fake_fit), \ - _SYNC_EXECUTOR: - cat_response(f, out_prefix=str(tmp_path / "out")) + side_effect=fake_fit): + cat_response(f, out_prefix=str(tmp_path / "out"), + num_workers=1, **kwargs) + return captured + def test_autodetects_q0_5(self, tmp_path): + """Values from q0.5 column are passed to the fitter.""" + captured = self._run_capture(tmp_path, _make_theta_df("q0.5")) assert captured["y"] == pytest.approx([0.3, 0.7]) def test_autodetects_point_est_values(self, tmp_path): - """Values from point_est column are passed to the fitter when median is absent.""" - f = str(tmp_path / "theta.csv") - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - - df = _make_theta_df("point_est") - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", - return_value=df), \ - patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - side_effect=fake_fit), \ - _SYNC_EXECUTOR: - cat_response(f, out_prefix=str(tmp_path / "out")) - + """point_est is used when q0.5 is absent.""" + captured = self._run_capture(tmp_path, _make_theta_df("point_est")) assert captured["y"] == pytest.approx([0.3, 0.7]) def test_explicit_theta_col_overrides(self, tmp_path): - """Explicit theta_col is used even when median is also present.""" - f = str(tmp_path / "theta.csv") - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - + """Explicit theta_col is used even when q0.5 is also present.""" df = _make_theta_df("q0.5").copy() df["my_col"] = [0.11, 0.22] - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", - return_value=df), \ - patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - side_effect=fake_fit), \ - _SYNC_EXECUTOR: - cat_response(f, theta_col="my_col", out_prefix=str(tmp_path / "out")) - + captured = self._run_capture(tmp_path, df, theta_col="my_col") assert captured["y"] == pytest.approx([0.11, 0.22]) def test_q0_5_preferred_over_point_est(self, tmp_path): """When both q0.5 and point_est are present, q0.5 wins.""" - f = str(tmp_path / "theta.csv") - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - return (_FLAT_RESULT, None) - df = _make_theta_df("q0.5").copy() df["point_est"] = [0.9, 0.8] - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", - return_value=df), \ - patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - side_effect=fake_fit), \ - _SYNC_EXECUTOR: - cat_response(f, out_prefix=str(tmp_path / "out")) - + captured = self._run_capture(tmp_path, df) assert captured["y"] == pytest.approx([0.3, 0.7]) + def test_sigma_from_quantiles(self, tmp_path): + """When sigma_col is absent, sigma = (q0.841 - q0.159) / 2.""" + captured = self._run_capture(tmp_path, _make_theta_df("q0.5")) + # (0.4-0.2)/2 = 0.1 ; (0.8-0.6)/2 = 0.1 + assert captured["y_std"] == pytest.approx([0.1, 0.1]) + def test_raises_when_no_theta_col(self, tmp_path): """ValueError when neither q0.5 nor point_est is present.""" f = str(tmp_path / "theta.csv") @@ -135,3 +95,88 @@ def test_raises_when_no_theta_col(self, tmp_path): return_value=df): with pytest.raises(ValueError, match="No theta column found"): cat_response(f, out_prefix=str(tmp_path / "out")) + + +class TestChunkHelpers: + + def test_iter_chunks_partitions_exactly(self): + items = list(range(10)) + chunks = list(_iter_chunks(items, 3)) + assert chunks == [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + def test_iter_chunks_empty(self): + assert list(_iter_chunks([], 3)) == [] + + def test_iter_chunks_larger_than_input(self): + assert list(_iter_chunks([1, 2], 100)) == [[1, 2]] + + def test_fit_one_tags_genotype_and_titrant(self): + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", + return_value=(dict(_FLAT_RESULT), None)): + out = _fit_one("g1", "IPTG", + np.array([0.0, 1.0]), np.array([0.2, 0.8]), + np.array([0.1, 0.1]), ["flat"]) + assert out["genotype"] == "g1" + assert out["titrant_name"] == "IPTG" + assert out["best_model"] == "flat" + + def test_fit_chunk_preserves_order(self): + # cat_fit echoes the number of points so we can verify ordering. + def fake_fit(x, y, y_std, models_to_run): + return ({"n": len(x)}, None) + + items = [ + ("g0", "IPTG", np.zeros(2), np.zeros(2), np.ones(2), ["flat"]), + ("g1", "IPTG", np.zeros(3), np.zeros(3), np.ones(3), ["flat"]), + ] + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", + side_effect=fake_fit): + out = _fit_chunk(items) + assert [r["genotype"] for r in out] == ["g0", "g1"] + assert [r["n"] for r in out] == [2, 3] + + +class TestDispatchEquivalence: + """Serial and parallel paths must produce identical, correctly-ordered + output across chunk boundaries. Uses the real (deterministic) cat_fit so the + ProcessPoolExecutor path is exercised end-to-end (a parent-process monkeypatch + would not reach spawned workers).""" + + def _many_genotype_df(self, n_geno): + # Four concentrations per genotype: enough points for the fitters, and a + # different (rising) curve per genotype so rows are distinguishable. + concs = np.array([0.0, 1.0, 10.0, 100.0]) + rows = [] + for i in range(n_geno): + center = 0.1 + 0.7 * (concs / concs.max()) + 0.001 * i + for c, mid in zip(concs, center): + rows.append({"genotype": f"g{i}", "titrant_name": "IPTG", + "titrant_conc": float(c), "q0.5": float(mid), + "q0.841": float(mid + 0.05), + "q0.159": float(mid - 0.05)}) + return pd.DataFrame(rows) + + def _run(self, tmp_path, df, num_workers, out_name): + f = str(tmp_path / "theta.csv") + out_prefix = str(tmp_path / out_name) + with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", + return_value=df): + cat_response(f, out_prefix=out_prefix, num_workers=num_workers) + return pd.read_csv(out_prefix + ".csv") + + def test_serial_row_order(self, tmp_path): + # Force multiple chunks by shrinking the chunk size. + df = self._many_genotype_df(7) + with patch.object(cat_response_cli, "_CHUNK_SIZE", 2): + out = self._run(tmp_path, df, num_workers=1, out_name="serial") + assert list(out["genotype"]) == [f"g{i}" for i in range(7)] + assert len(out) == 7 + + def test_parallel_matches_serial(self, tmp_path): + # Multiple chunks spread across 2 real worker processes. + df = self._many_genotype_df(7) + with patch.object(cat_response_cli, "_CHUNK_SIZE", 2): + serial = self._run(tmp_path, df, num_workers=1, out_name="serial") + parallel = self._run(tmp_path, df, num_workers=2, out_name="parallel") + pd.testing.assert_frame_equal(serial, parallel) + assert list(parallel["genotype"]) == [f"g{i}" for i in range(7)] diff --git a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py new file mode 100644 index 0000000..488b9d5 --- /dev/null +++ b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py @@ -0,0 +1,164 @@ +""" +Tests for extract_epistasis_cli.py -- CSV IO, column validation, argument wiring. +""" +import sys + +import pytest +import pandas as pd + +from tfscreen.analysis.scripts.extract_epistasis_cli import ( + extract_epistasis, + main, +) + + +def _write_csv(tmp_path, rows, name="data.csv"): + path = str(tmp_path / name) + pd.DataFrame(rows).to_csv(path, index=False) + return path + + +def _single_cycle_rows(condition=None): + """wt/singles/double with a clean additive cycle (ep = 1.0).""" + rows = [ + {"genotype": "wt", "y": 1.0, "y_err": 0.1}, + {"genotype": "A15G", "y": 2.0, "y_err": 0.1}, + {"genotype": "P75K", "y": 3.0, "y_err": 0.1}, + {"genotype": "A15G/P75K", "y": 5.0, "y_err": 0.1}, + ] + if condition is not None: + for r in rows: + r["condition"] = condition + return rows + + +class TestHappyPath: + + def test_additive_ep_obs(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv") + assert len(out) == 1 + assert out["ep_obs"].iloc[0] == pytest.approx(1.0) + # No y_std passed -> no ep_std column. + assert "ep_std" not in out.columns + + def test_y_std_propagates(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", y_std="y_err", out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv") + # sqrt(4 * 0.1**2) = 0.2 + assert out["ep_std"].iloc[0] == pytest.approx(0.2) + + def test_multiplicative_scale(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", scale="mult", out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv") + # (5/3) / (2/1) = 0.83333... + assert out["ep_obs"].iloc[0] == pytest.approx((5.0 / 3.0) / (2.0 / 1.0)) + + def test_condition_selector_groups_independently(self, tmp_path): + rows = _single_cycle_rows(condition="c1") + _single_cycle_rows(condition="c2") + # Make c2's double mutant produce a different epistasis (ep = 0.0). + for r in rows: + if r["condition"] == "c2" and r["genotype"] == "A15G/P75K": + r["y"] = 4.0 # (4-3) - (2-1) = 0.0 + data = _write_csv(tmp_path, rows) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", + condition_selector=["condition"], + out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv").sort_values("condition") + assert len(out) == 2 + by_cond = dict(zip(out["condition"], out["ep_obs"])) + assert by_cond["c1"] == pytest.approx(1.0) + assert by_cond["c2"] == pytest.approx(0.0) + + def test_keep_extra_retains_input_columns(self, tmp_path): + rows = _single_cycle_rows() + for r in rows: + r["note"] = "keepme" + data = _write_csv(tmp_path, rows) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", keep_extra=True, out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv") + assert "note" in out.columns + + +class TestValidation: + + def test_missing_y_obs_raises(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + with pytest.raises(ValueError, match="missing required column"): + extract_epistasis(data, y_obs="does_not_exist", + out_prefix=str(tmp_path / "out")) + + def test_missing_y_std_raises(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + with pytest.raises(ValueError, match="missing required column"): + extract_epistasis(data, y_obs="y", y_std="nope", + out_prefix=str(tmp_path / "out")) + + def test_missing_condition_selector_raises(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + with pytest.raises(ValueError, match="missing required column"): + extract_epistasis(data, y_obs="y", condition_selector=["nope"], + out_prefix=str(tmp_path / "out")) + + +class TestEmptyResult: + + def test_no_cycles_writes_empty(self, tmp_path, capsys): + # No double mutant -> no cycles. + rows = [ + {"genotype": "wt", "y": 1.0}, + {"genotype": "A15G", "y": 2.0}, + ] + data = _write_csv(tmp_path, rows) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", out_prefix=out_prefix) + + import os + assert os.path.exists(f"{out_prefix}.csv") + captured = capsys.readouterr().out + assert "no valid mutant cycles" in captured + assert "Wrote 0 rows" in captured + + +class TestArgWiring: + + def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): + data = _write_csv(tmp_path, + _single_cycle_rows(condition="c1") + + _single_cycle_rows(condition="c2")) + out_prefix = str(tmp_path / "out") + + argv = ["extract_epistasis", data, "y", + "--out_prefix", out_prefix, + "--y_std", "y_err", + "--condition_selector", "condition", + "--scale", "add", + "--keep_extra"] + monkeypatch.setattr(sys, "argv", argv) + + main() + + out = pd.read_csv(f"{out_prefix}.csv") + assert len(out) == 2 + assert "ep_std" in out.columns + # keep_extra retained the original observable column. + assert "y" in out.columns diff --git a/tests/tfscreen/analysis/test_extract_epistasis.py b/tests/tfscreen/analysis/test_extract_epistasis.py index 36fe8b5..f4321bb 100644 --- a/tests/tfscreen/analysis/test_extract_epistasis.py +++ b/tests/tfscreen/analysis/test_extract_epistasis.py @@ -226,4 +226,23 @@ def test_keep_extra_columns(self, base_df_for_epistasis): # keep_extra = False (default) should drop 'extra_col' result_false = extract_epistasis(base_df_for_epistasis, "fitness",condition_selector="condition") - assert "extra_col" not in result_false.columns \ No newline at end of file + assert "extra_col" not in result_false.columns + + def test_returns_empty_when_no_valid_cycles(self): + """No double mutant -> empty result rather than a KeyError. + + Regression: with keep_extra=False the column selection ran even when the + pivot returned an empty (column-less) frame, raising a KeyError. + """ + df = pd.DataFrame({ + "genotype": ["wt", "A10G"], + "fitness": [1.0, 0.8], + "error": [0.05, 0.04], + }) + result = extract_epistasis(df, "fitness", y_std="error") + assert result.empty + + def test_returns_empty_for_empty_input(self): + """An empty input frame returns an empty frame (no KeyError).""" + result = extract_epistasis(pd.DataFrame({"genotype": []}), "fitness") + assert result.empty \ No newline at end of file diff --git a/tests/tfscreen/util/test_parallel.py b/tests/tfscreen/util/test_parallel.py new file mode 100644 index 0000000..063106b --- /dev/null +++ b/tests/tfscreen/util/test_parallel.py @@ -0,0 +1,44 @@ +""" +Tests for tfscreen.util.parallel.resolve_workers. +""" +from unittest.mock import patch + +from tfscreen.util.parallel import resolve_workers +from tfscreen.util import resolve_workers as resolve_workers_reexport + + +def test_none_is_serial(): + assert resolve_workers(None) == 1 + + +def test_one_is_serial(): + assert resolve_workers(1) == 1 + + +def test_explicit_count_passes_through(): + assert resolve_workers(4) == 4 + + +def test_negative_uses_cpu_count_minus_one(): + with patch("tfscreen.util.parallel.os.cpu_count", return_value=8): + assert resolve_workers(-1) == 7 + + +def test_negative_other_values_also_cpu_count_minus_one(): + with patch("tfscreen.util.parallel.os.cpu_count", return_value=8): + assert resolve_workers(-4) == 7 + + +def test_negative_floors_at_one(): + with patch("tfscreen.util.parallel.os.cpu_count", return_value=1): + assert resolve_workers(-1) == 1 + + +def test_cpu_count_none_falls_back(): + # os.cpu_count() can return None; fallback keeps it >= 1. + with patch("tfscreen.util.parallel.os.cpu_count", return_value=None): + assert resolve_workers(-1) == 1 + + +def test_reexported_from_util_package(): + assert resolve_workers_reexport is resolve_workers From 4ab6083186a345667f6ac18f3ded5ee045b9d223 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Fri, 17 Jul 2026 22:03:51 -0700 Subject: [PATCH 08/26] bug fixes in cat and epistasis cli --- .../cat_response/scripts/cat_response_cli.py | 3 +- .../analysis/scripts/extract_epistasis_cli.py | 83 +++++++++++++++++++ .../scripts/test_extract_epistasis_cli.py | 37 +++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py b/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py index f3702b6..95cff55 100644 --- a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py @@ -156,7 +156,8 @@ def _report(n_done): def main(): generalized_main(cat_response, - manual_arg_types={"sigma_col": str, + manual_arg_types={"theta_col": str, + "sigma_col": str, "models": str}, manual_arg_nargs={"models": "+"}) diff --git a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py index 115ee21..ca7b94f 100644 --- a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -10,6 +10,85 @@ from tfscreen.util.cli import generalized_main +def _diagnose_empty_output(df, condition_selector, exclude=()): + """ + Explain why no mutant cycles were found and, when possible, suggest a fix. + + The most common cause is a table with one row per genotype *per condition* + (e.g. per titrant_conc) run without ``--condition_selector``. In that case + every genotype is non-unique within the single implicit group, so + ``mutant_cycle_pivot`` drops all rows as duplicates and returns nothing. This + inspects the input and, if that is what happened, names the column(s) that + would resolve it. + + Parameters + ---------- + df : pandas.DataFrame + The raw input table (with a 'genotype' column). + condition_selector : list of str or None + The condition columns that were used, if any. + exclude : iterable of str, optional + Columns that must never be suggested as condition selectors -- namely the + observable and its error column, whose values legitimately vary within a + genotype and would otherwise be flagged as spurious candidates. + + Returns + ------- + str or None + A hint to print, or None if no specific cause could be identified. + """ + if "genotype" not in df.columns: + return None + + used = list(condition_selector) if condition_selector is not None else [] + exclude = set(exclude) + + # Recreate the grouping the pivot used, and check for non-unique genotypes + # within any group -- that is the duplicate-drop trap. + if used: + groups = (g for _, g in df.groupby(used)) + else: + groups = (df,) + has_dupes = any(not g["genotype"].is_unique for g in groups) + if not has_dupes: + return None + + # Candidate condition columns: those whose value varies within a genotype + # (and that are not already being used as selectors). + candidates = [ + c for c in df.columns + if c != "genotype" and c not in used and c not in exclude + and (df.groupby("genotype")[c].nunique(dropna=False) > 1).any() + ] + + lines = [ + "Every genotype appears more than once within a condition group, so all " + "rows were dropped as duplicates before any cycle could be built." + ] + if candidates: + # Prefer a single column that alone makes genotypes unique; otherwise + # fall back to suggesting the full set. + single = [ + c for c in candidates + if not df.duplicated(subset=["genotype", c]).any() + ] + hint_cols = single if single else candidates + lines.append( + "This usually means the table has one row per genotype *per " + "condition*, and the condition column was not passed." + ) + lines.append(f" Columns that vary within a genotype: {candidates}") + lines.append( + f" Try: --condition_selector {' '.join(hint_cols)}" + ) + else: + lines.append( + "Check the input for genuinely duplicated genotype rows within a " + "condition." + ) + return "\n".join(lines) + + def extract_epistasis(data_file, y_obs, out_prefix="tfs_epistasis", @@ -81,6 +160,10 @@ def extract_epistasis(data_file, if result.empty: print("Warning: no valid mutant cycles found; writing empty output.", flush=True) + exclude = [c for c in (y_obs, y_std) if c is not None] + hint = _diagnose_empty_output(df, condition_selector, exclude=exclude) + if hint is not None: + print(hint, flush=True) result.to_csv(out_file, index=False) print(f"Wrote {len(result)} rows to {out_file}", flush=True) diff --git a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py index 488b9d5..ddb20e4 100644 --- a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py +++ b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py @@ -138,6 +138,43 @@ def test_no_cycles_writes_empty(self, tmp_path, capsys): assert "no valid mutant cycles" in captured assert "Wrote 0 rows" in captured + def test_forgot_condition_selector_hints_column(self, tmp_path, capsys): + # One row per genotype *per condition* (like titrant_conc), run without + # --condition_selector: every genotype is non-unique -> all dropped. + rows = [] + for conc in [0.0, 0.1, 1.0]: + for r in _single_cycle_rows(): + rows.append({**r, "titrant_conc": conc}) + data = _write_csv(tmp_path, rows) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", out_prefix=out_prefix) + + captured = capsys.readouterr().out + assert "dropped as duplicates" in captured + assert "--condition_selector titrant_conc" in captured + # With the suggested selector it succeeds. + extract_epistasis(data, y_obs="y", + condition_selector=["titrant_conc"], + out_prefix=out_prefix) + out = pd.read_csv(f"{out_prefix}.csv") + assert len(out) == 3 + + def test_no_cycles_does_not_hint_condition(self, tmp_path, capsys): + # Genotypes already unique -> the empty result is not a duplicate issue, + # so we must not emit a spurious condition-column hint. + rows = [ + {"genotype": "wt", "y": 1.0}, + {"genotype": "A15G", "y": 2.0}, + ] + data = _write_csv(tmp_path, rows) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", out_prefix=out_prefix) + + captured = capsys.readouterr().out + assert "--condition_selector" not in captured + class TestArgWiring: From 41ac4806f00279c79763bd642c6dc8b4c00cf5b6 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sat, 18 Jul 2026 08:38:28 -0700 Subject: [PATCH 09/26] cleaned up epistasis and cat cli --- CLAUDE.md | 5 +- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- docs/source/analysis.rst | 26 +- pyproject.toml | 2 +- reports/flake.txt | 96 +----- reports/junit/junit.xml | 2 +- .../analysis/cat_response/cat_response.py | 273 ++++++++------- .../analysis/cat_response/scripts/__init__.py | 0 .../cat_response/scripts/cat_response_cli.py | 166 --------- src/tfscreen/analysis/extract_epistasis.py | 30 +- .../analysis/scripts/cat_response_cli.py | 175 ++++++++++ .../analysis/scripts/extract_epistasis_cli.py | 30 +- .../plot/heatmap/epistasis_heatmap.py | 6 +- .../analysis/cat_response/scripts/__init__.py | 0 .../scripts/test_cat_response_cli.py | 182 ---------- .../cat_response/test_cat_response.py | 319 +++++++++++------- .../analysis/scripts/test_cat_response_cli.py | 171 ++++++++++ .../scripts/test_extract_epistasis_cli.py | 20 +- .../analysis/test_extract_epistasis.py | 12 +- tests/tfscreen/plot/heatmap/test_heatmaps.py | 2 +- 21 files changed, 797 insertions(+), 724 deletions(-) delete mode 100644 src/tfscreen/analysis/cat_response/scripts/__init__.py delete mode 100644 src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py create mode 100644 src/tfscreen/analysis/scripts/cat_response_cli.py delete mode 100644 tests/tfscreen/analysis/cat_response/scripts/__init__.py delete mode 100644 tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py create mode 100644 tests/tfscreen/analysis/scripts/test_cat_response_cli.py diff --git a/CLAUDE.md b/CLAUDE.md index 90b76ec..331a3d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -394,7 +394,8 @@ All scripts use `generalized_main` from `tfscreen.util.cli.generalized_main`. Th Positional argument order (use only what the script needs): 1. `config_file` — path to YAML config 2. `posterior_file` — path to posteriors `.h5`/`.npz` -3. `theta_file` — path to theta CSV (for `tfs-cat-response`) +3. `data_file` — path to a long-form observable CSV (e.g. `tfs-cat-response` + takes `data_file x_obs y_obs`; `tfs-extract-epistasis` takes `data_file y_obs`) ### Output flag @@ -410,4 +411,4 @@ When a list of genotypes, titrant names, or concentrations is needed, the `_cli` ### Registered entry points -All scripts under `tfmodel/scripts/` and `analysis/cat_response/` follow the `_cli.py` naming convention and are registered in `pyproject.toml`. +All scripts under `tfmodel/scripts/` and `analysis/scripts/` follow the `_cli.py` naming convention and are registered in `pyproject.toml`. diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 92cb9a7..7d57d56 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.17%coverage96.17% \ No newline at end of file +coverage: 96.16%coverage96.16% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 36c3443..286bd98 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4071tests4071 \ No newline at end of file +tests: 4077tests4077 \ No newline at end of file diff --git a/docs/source/analysis.rst b/docs/source/analysis.rst index 554a9ca..a18d84c 100644 --- a/docs/source/analysis.rst +++ b/docs/source/analysis.rst @@ -224,24 +224,38 @@ supports it (e.g. ``hill_mut``). Step 8: Categorise Response (``tfs-cat-response``) --------------------------------------------------- -Fits categorical response curve models to the *θ*-vs-titrant output of -``tfs-predict-theta`` and selects the best-fitting model per -(genotype, titrant_name) pair by AIC weight. +Fits categorical response curve models to a ``y_obs``-vs-``x_obs`` curve and +selects the best-fitting model per group by AIC weight. Groups are defined by +the ``genotype`` column plus any ``--group_by`` columns (mirroring +``tfs-extract-epistasis``). For the *θ*-vs-titrant output of +``tfs-predict-theta``, pass the concentration column as ``x_obs`` and a theta +column (e.g. ``q0.5``) as ``y_obs``. .. code-block:: bash tfs-cat-response \ tfs_theta_pred.csv \ + titrant_conc \ + q0.5 \ + --group_by titrant_name \ --num_workers -1 -The per-pair fits are embarrassingly parallel. ``--num_workers`` defaults to +If ``--y_std`` is omitted and the ``q0.841``/``q0.159`` quantile columns are +present, the per-row sigma is taken as ``(q0.841 - q0.159) / 2``; otherwise the +fit is unweighted. + +The per-group fits are embarrassingly parallel. ``--num_workers`` defaults to ``-1`` (use ``os.cpu_count() - 1`` processes); pass ``1`` to run serially or a positive integer to pin the worker count. Output (default ``--out_prefix tfs_cat_response``): -* ``tfs_cat_response.csv`` — one row per (genotype, titrant_name) with - ``best_model``, AIC weights, and fitted parameters for every model. +* ``tfs_cat_response.csv`` — one row per group with ``best_model``, AIC + weights, and fitted parameters for every model. +* ``tfs_cat_response_.csv`` — one file per model with that model's + parameter table and per-group fit statistics. +* ``tfs_cat_response_predictions.csv`` — predicted curves for every group and + model. Step 9: Summarise Fit (``tfs-summarize-fit``) ---------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d204731..a64b8af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ tfs-sample-prior = "tfscreen.tfmodel.scripts.sample_prior_cli:main" tfs-extract-params = "tfscreen.tfmodel.scripts.extract_params_cli:main" tfs-predict-growth = "tfscreen.tfmodel.scripts.predict_growth_cli:main" tfs-predict-theta = "tfscreen.tfmodel.scripts.predict_theta_cli:main" -tfs-cat-response = "tfscreen.analysis.cat_response.scripts.cat_response_cli:main" +tfs-cat-response = "tfscreen.analysis.scripts.cat_response_cli:main" tfs-extract-epistasis = "tfscreen.analysis.scripts.extract_epistasis_cli:main" tfs-diagnose-nan = "tfscreen.tfmodel.scripts.diagnose_nan_cli:main" tfs-simulate = "tfscreen.simulate.scripts.simulate_cli:main" diff --git a/reports/flake.txt b/reports/flake.txt index 7d81f74..b6d2a42 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -49,33 +49,9 @@ ./src/tfscreen/analysis/cat_response/cat_fit.py:209:1: W293 blank line contains whitespace ./src/tfscreen/analysis/cat_response/cat_fit.py:211:5: E303 too many blank lines (2) ./src/tfscreen/analysis/cat_response/cat_fit.py:211:32: W292 no newline at end of file -./src/tfscreen/analysis/cat_response/cat_response.py:9:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/analysis/cat_response/cat_response.py:23:73: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:26:78: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:44:74: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:45:75: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:46:26: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:51:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:57:43: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_response.py:73:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:79:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:90:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:97:43: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:98:26: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:100:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:103:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:115:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:118:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:130:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_response.py:134:34: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_response.py:138:42: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_response.py:141:56: W292 no newline at end of file -./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:38:1: C901 'cat_response' is too complex (14) -./src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py:111:56: E127 continuation line over-indented for visual indent +./src/tfscreen/analysis/cat_response/cat_response.py:49:1: C901 'cat_response' is too complex (12) ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/analysis/extract_epistasis.py:33:77: W291 trailing whitespace -./src/tfscreen/analysis/extract_epistasis.py:34:25: W291 trailing whitespace ./src/tfscreen/analysis/extract_epistasis.py:78:25: E231 missing whitespace after ',' ./src/tfscreen/analysis/extract_epistasis.py:90:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:96:1: W293 blank line contains whitespace @@ -83,14 +59,12 @@ ./src/tfscreen/analysis/extract_epistasis.py:113:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:125:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:129:1: C901 'extract_epistasis' is too complex (13) -./src/tfscreen/analysis/extract_epistasis.py:133:47: E252 missing whitespace around parameter equals -./src/tfscreen/analysis/extract_epistasis.py:133:48: E252 missing whitespace around parameter equals -./src/tfscreen/analysis/extract_epistasis.py:159:75: W291 trailing whitespace +./src/tfscreen/analysis/extract_epistasis.py:133:37: E252 missing whitespace around parameter equals +./src/tfscreen/analysis/extract_epistasis.py:133:38: E252 missing whitespace around parameter equals ./src/tfscreen/analysis/extract_epistasis.py:183:36: W291 trailing whitespace ./src/tfscreen/analysis/extract_epistasis.py:184:27: E231 missing whitespace after ',' ./src/tfscreen/analysis/extract_epistasis.py:187:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:192:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:208:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:215:55: E231 missing whitespace after ',' ./src/tfscreen/analysis/extract_epistasis.py:215:60: E231 missing whitespace after ',' ./src/tfscreen/analysis/extract_epistasis.py:215:65: E231 missing whitespace after ',' @@ -765,9 +739,6 @@ ./src/tfscreen/plot/heatmap/epistasis_heatmap.py:23:1: W293 blank line contains whitespace ./src/tfscreen/plot/heatmap/epistasis_heatmap.py:25:17: E231 missing whitespace after ',' ./src/tfscreen/plot/heatmap/epistasis_heatmap.py:33:25: E211 whitespace before '(' -./src/tfscreen/plot/heatmap/epistasis_heatmap.py:36:1: W293 blank line contains whitespace -./src/tfscreen/plot/heatmap/epistasis_heatmap.py:39:73: E261 at least two spaces before inline comment -./src/tfscreen/plot/heatmap/epistasis_heatmap.py:39:74: E262 inline comment should start with '# ' ./src/tfscreen/plot/heatmap/epistasis_heatmap.py:50:43: E231 missing whitespace after ',' ./src/tfscreen/plot/heatmap/epistasis_heatmap.py:52:19: W292 no newline at end of file ./src/tfscreen/plot/heatmap/heatmap_core.py:6:18: W291 trailing whitespace @@ -4131,38 +4102,6 @@ ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:243:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:246:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/cat_response/test_cat_fit.py:250:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:3:1: F401 'numpy as np' imported but unused -./tests/tfscreen/analysis/cat_response/test_cat_response.py:5:1: F401 'unittest.mock.MagicMock' imported but unused -./tests/tfscreen/analysis/cat_response/test_cat_response.py:9:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_response.py:20:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_response.py:24:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:28:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:31:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:46:39: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:46:52: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:46:69: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:48:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:51:29: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:57:29: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_response.py:63:39: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:63:51: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:63:68: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:65:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:70:17: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:74:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:77:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:81:44: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_response.py:83:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:92:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:94:29: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_response.py:98:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_response.py:102:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:105:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:111:90: E231 missing whitespace after ':' -./tests/tfscreen/analysis/cat_response/test_cat_response.py:112:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:113:59: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_response.py:114:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_response.py:119:1: W391 blank line at end of file ./tests/tfscreen/analysis/test_extract_epistasis.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:22:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:34:35: W291 trailing whitespace @@ -4200,16 +4139,16 @@ ./tests/tfscreen/analysis/test_extract_epistasis.py:178:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:183:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:189:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/test_extract_epistasis.py:191:98: E251 unexpected spaces around keyword / parameter equals -./tests/tfscreen/analysis/test_extract_epistasis.py:191:110: E231 missing whitespace after ',' +./tests/tfscreen/analysis/test_extract_epistasis.py:191:88: E251 unexpected spaces around keyword / parameter equals +./tests/tfscreen/analysis/test_extract_epistasis.py:191:100: E231 missing whitespace after ',' ./tests/tfscreen/analysis/test_extract_epistasis.py:192:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:196:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:199:50: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/test_extract_epistasis.py:204:101: W291 trailing whitespace +./tests/tfscreen/analysis/test_extract_epistasis.py:204:91: W291 trailing whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:210:77: E231 missing whitespace after ',' ./tests/tfscreen/analysis/test_extract_epistasis.py:211:59: E261 at least two spaces before inline comment ./tests/tfscreen/analysis/test_extract_epistasis.py:212:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/test_extract_epistasis.py:219:95: E231 missing whitespace after ',' +./tests/tfscreen/analysis/test_extract_epistasis.py:219:85: E231 missing whitespace after ',' ./tests/tfscreen/analysis/test_extract_epistasis.py:220:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:226:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:228:74: E231 missing whitespace after ',' @@ -7647,7 +7586,7 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -214 E127 continuation line over-indented for visual indent +213 E127 continuation line over-indented for visual indent 92 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' @@ -7657,16 +7596,15 @@ 1839 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1110 E231 missing whitespace after ',' +1100 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals -216 E261 at least two spaces before inline comment -1 E262 inline comment should start with '# ' +211 E261 at least two spaces before inline comment 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment 149 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -777 E302 expected 2 blank lines, found 1 +773 E302 expected 2 blank lines, found 1 50 E303 too many blank lines (2) 19 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 @@ -7679,12 +7617,12 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 21 E741 ambiguous variable name 'l' -232 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -620 W291 trailing whitespace -85 W292 no newline at end of file -1582 W293 blank line contains whitespace -30 W391 blank line at end of file -7641 +608 W291 trailing whitespace +84 W292 no newline at end of file +1558 W293 blank line contains whitespace +29 W391 blank line at end of file +7580 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 21f16d6..967d0d7 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index c345ee7..dd351c8 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -1,141 +1,174 @@ +""" +Generic per-group categorical-response engine. + +Fits a family of candidate models to a ``y_obs`` vs ``x_obs`` curve independently +within each group and selects the best by AIC weight. Grouping mirrors +``extract_epistasis``: the ``genotype`` column is always the primary axis, with +any additional ``group_by`` columns partitioning the analysis further. +""" + +from concurrent.futures import ProcessPoolExecutor + +import numpy as np +import pandas as pd from .cat_fit import cat_fit from tfscreen.mle.curve_models import MODEL_LIBRARY - +from tfscreen.util import resolve_workers from tfscreen.util.numerical import xfill -import pandas as pd +# Number of groups bundled into each worker task. Larger chunks amortize the +# per-task pickle/IPC overhead of ProcessPoolExecutor, which matters a lot when +# there are hundreds of thousands of groups. +_CHUNK_SIZE = 200 + + +def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, verbose): + """Run cat_fit for one group and tag the results with the group key.""" + flat_out, pred_df = cat_fit(x, y, y_std, + x_pred=x_pred, + models_to_run=models_to_run, + verbose=verbose) + for col, val in group_key.items(): + flat_out[col] = val + pred_df[col] = val + return flat_out, pred_df + + +def _fit_chunk(chunk): + """Worker: run cat_fit for a list of work items, preserving order.""" + return [_fit_one(*item) for item in chunk] + + +def _iter_chunks(work_items, chunk_size): + """Yield successive length-``chunk_size`` slices of ``work_items``.""" + for start in range(0, len(work_items), chunk_size): + yield work_items[start:start + chunk_size] + def cat_response(df, - x_column="titrant_conc", - y_column="theta_est", - y_std_column="theta_std", + x_obs, + y_obs, + y_std=None, + group_by=None, models_to_run=None, + num_workers=1, verbose=False): """ - Processes a DataFrame of genotype data, running fits for each genotype - and aggregating the results into summary and model-specific DataFrames. + Classify each group's ``y_obs`` vs ``x_obs`` curve using categorical models. + + The DataFrame is partitioned into groups keyed by ``genotype`` plus any + ``group_by`` columns. For each group, every model in ``models_to_run`` is fit + to the (``x_obs``, ``y_obs``, ``y_std``) data and the best is selected by AIC + weight. Fits are embarrassingly parallel across groups. Parameters ---------- - df : pd.DataFrame - A DataFrame containing the experimental data. Must include columns: - 'genotype', which it uses to break up individual samples to fit. - x_column, y_column, y_std_columns : str, optional - names of columns in dataframe corresponding to x (independent variable), - y (measured value), and y_std (standard error on the measured value). - models_to_run : list of str, optional - A list of model names to test. If None (default), all models in the - global MODEL_LIBRARY will be tested. + df : pandas.DataFrame + Long-form data. Must contain a 'genotype' column, ``x_obs``, ``y_obs``, + and (if given) ``y_std`` and every column in ``group_by``. + x_obs : str + Name of the column holding the independent variable. + y_obs : str + Name of the column holding the observable (dependent variable). + y_std : str or None, optional + Name of the column holding the standard error of ``y_obs``. If None + (default), an unweighted fit is performed (uniform weights). + group_by : list of str or None, optional + Additional column(s) that, together with 'genotype', define a group. One + curve is fit per group. If None (default), groups are defined by + 'genotype' alone. + models_to_run : list of str or None, optional + Model names to test. If None (default), all models in MODEL_LIBRARY. + num_workers : int, optional + Number of worker processes. ``1`` runs serially in-process; ``-1`` uses + ``os.cpu_count() - 1``; ``N`` uses ``N`` processes. Default 1. verbose : bool, optional - If True, prints warnings to the console when a model fails to fit. - Defaults to False. + If True, print progress and per-model fit warnings. Default False. Returns ------- - tuple of (dict, pd.DataFrame) - - model_dataframes: A dictionary where keys are model names and values - are DataFrames. Each DataFrame contains all genotypes as rows, with - columns for that model's parameter estimates, standard errors, and - overall fit statistics (is_best_model, R2, AIC_weight). - - summary_dataframe: A single DataFrame where each row is a genotype. - Columns include the best model name, its R2 and AIC weight, the - overall fit status, and the AIC weights for all tested models. - - pred_df : a single DataFrame where each row is a genotype/model - combination holding the predicted values of each model given the - fit parameters. + results_df : pandas.DataFrame + One row per group. Columns are the group-key columns followed by the flat + cat_fit output (``status``, ``best_model``, per-model ``AIC_weight|*`` / + ``R2|*`` weights, and ``||est`` / ``||std`` + parameter columns). Column names keep the ``|`` delimiter; presentation + (cleanup, per-model splitting) is left to callers. + predictions_df : pandas.DataFrame + Predicted curves, concatenated across groups. Columns are the group-key + columns followed by ``model``, ``x``, ``y``, ``y_std``, ``is_best_model``. """ - if models_to_run is None: models_to_run = list(MODEL_LIBRARY.keys()) - - # Loop over all genotypes - results_list = [] - pred_results_list = [] - get_columns = [x_column, y_column, y_std_column] - - x_pred = xfill(pd.unique(df[x_column]),num_points=100) - - for genotype in pd.unique(df["genotype"]): - this_data = df.loc[ - df["genotype"] == genotype, get_columns - ].values - - # Call the fitting function which returns a flat dictionary - fit_results, fit_pred_df = cat_fit( - this_data[:, 0], - this_data[:, 1], - this_data[:, 2], - x_pred=x_pred, - verbose=verbose, - models_to_run=models_to_run + + bad = [m for m in models_to_run if m not in MODEL_LIBRARY] + if bad: + raise ValueError(f"Unknown model(s): {bad}. Valid: {list(MODEL_LIBRARY)}") + + group_cols = ["genotype"] + (list(group_by) if group_by else []) + + needed = list(dict.fromkeys(group_cols + [x_obs, y_obs] + + ([y_std] if y_std is not None else []))) + missing = [c for c in needed if c not in df.columns] + if missing: + raise ValueError( + f"DataFrame is missing required column(s): {missing}. " + f"Available columns: {list(df.columns)}" ) - - fit_results['genotype'] = genotype - results_list.append(fit_results) - - fit_pred_df["genotype"] = genotype - pred_results_list.append(fit_pred_df) - - # Create a single "main" DataFrame from the list of dictionaries - main_df = pd.DataFrame(results_list).set_index("genotype") - - # Create the summary DataFrame - summary_cols = [ - "best_model", "best_model_R2", "best_model_AIC_weight", "status" - ] - aic_weight_cols = [c for c in main_df.columns if c.startswith('AIC_weight|')] - - summary_dataframe = main_df[summary_cols + aic_weight_cols].copy() - - # clean up AIC weight column names - summary_dataframe.rename( - columns={c: c.replace('AIC_weight|', 'w_') for c in aic_weight_cols}, - inplace=True - ) - - # Create the model-specific DataFrames - model_dataframes = {} - for model_name in models_to_run: - - # Select all columns related to this model - model_cols = [c for c in main_df.columns if c.startswith(f"{model_name}|")] - - # Create a new DataFrame for this model - model_df = main_df[model_cols].copy() - - # Clean up the column names - # from "model_name|param_name|est" -> "param_name_est" - new_colnames = {} - for col in model_df.columns: - parts = col.split('|') - new_name = f"{parts[1]}_{parts[2]}" - new_colnames[col] = new_name - model_df.rename(columns=new_colnames, inplace=True) - - # Add summary columns for this specific model - model_df['is_best_model'] = (main_df['best_model'] == model_name) - - # The following lines assume that cat_fit returns R2 and AIC_weight for - # each model, named like 'R2|model_name' and 'AIC_weight|model_name'. - model_df['R2'] = main_df.get(f'R2|{model_name}', pd.Series(index=main_df.index, dtype=float)) - model_df['AIC_weight'] = main_df[f'AIC_weight|{model_name}'] - - # Clean up the columns so they come out in est, std, stats order - param_names = MODEL_LIBRARY[model_name]["param_names"] - current_columns = list(model_df.columns) - for p in param_names: - current_columns.remove(f"{p}_est") - current_columns.remove(f"{p}_std") - - new_columns = [f"{p}_est" for p in param_names] - new_columns.extend([f"{p}_std" for p in param_names]) - new_columns.extend(current_columns) - model_df = model_df.loc[:,new_columns] - - model_dataframes[model_name] = model_df - - pred_df = pd.concat(pred_results_list,ignore_index=True) - pred_df.index = pred_df["genotype"] - - return model_dataframes, summary_dataframe, pred_df \ No newline at end of file + + # A shared prediction grid spanning all observed x-values, so every group's + # predicted curve is evaluated on the same axis. + x_pred = xfill(pd.unique(df[x_obs]), num_points=100) + + # observed=True so unused categorical combinations do not create empty groups. + work_items = [] + for keys, group in df.groupby(group_cols, sort=False, observed=True): + if not isinstance(keys, tuple): + keys = (keys,) + group_key = dict(zip(group_cols, keys)) + + x = group[x_obs].to_numpy(dtype=float) + y = group[y_obs].to_numpy(dtype=float) + if y_std is not None: + ys = group[y_std].to_numpy(dtype=float) + else: + ys = np.ones(len(x), dtype=float) + + work_items.append((group_key, x, y, ys, x_pred, models_to_run, verbose)) + + n_total = len(work_items) + if n_total == 0: + empty = pd.DataFrame(columns=group_cols) + return empty.copy(), empty.copy() + + workers = resolve_workers(num_workers) + chunks = list(_iter_chunks(work_items, _CHUNK_SIZE)) + + if verbose: + print(f"Fitting {n_total} group(s) with {workers} worker(s)...", + flush=True) + + results = [] + if workers == 1: + # Serial fast-path: run in-process, no pickling/IPC overhead. + for chunk in chunks: + results.extend(_fit_chunk(chunk)) + else: + # executor.map preserves input order, so results stay aligned with + # work_items without an explicit index map. + with ProcessPoolExecutor(max_workers=workers) as executor: + for chunk_result in executor.map(_fit_chunk, chunks): + results.extend(chunk_result) + + flat_list = [r[0] for r in results] + pred_list = [r[1] for r in results] + + results_df = pd.DataFrame(flat_list) + other = [c for c in results_df.columns if c not in group_cols] + results_df = results_df[group_cols + other] + + predictions_df = pd.concat(pred_list, ignore_index=True) + pred_other = [c for c in predictions_df.columns if c not in group_cols] + predictions_df = predictions_df[group_cols + pred_other] + + return results_df, predictions_df diff --git a/src/tfscreen/analysis/cat_response/scripts/__init__.py b/src/tfscreen/analysis/cat_response/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py b/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py deleted file mode 100644 index 95cff55..0000000 --- a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -CLI for fitting cat_response models to theta-vs-titrant data per genotype. -""" - -import pandas as pd -from concurrent.futures import ProcessPoolExecutor - -from tfscreen.analysis.cat_response.cat_fit import cat_fit -from tfscreen.mle.curve_models import MODEL_LIBRARY -from tfscreen.util import resolve_workers -from tfscreen.util.cli.generalized_main import generalized_main - -# Number of (genotype, titrant_name) pairs bundled into each worker task. Larger -# chunks amortize the per-task pickle/IPC overhead of ProcessPoolExecutor, which -# matters a lot when there are hundreds of thousands of pairs. -_CHUNK_SIZE = 200 - - -def _fit_one(genotype, titrant_name, x, y, y_std, models_to_run): - """Run cat_fit for one (genotype, titrant_name) pair.""" - flat_out, _ = cat_fit(x, y, y_std, models_to_run=models_to_run) - flat_out["genotype"] = genotype - flat_out["titrant_name"] = titrant_name - return flat_out - - -def _fit_chunk(chunk): - """Worker: run cat_fit for a list of work items, preserving order.""" - return [_fit_one(*item) for item in chunk] - - -def _iter_chunks(work_items, chunk_size): - """Yield successive length-``chunk_size`` slices of ``work_items``.""" - for start in range(0, len(work_items), chunk_size): - yield work_items[start:start + chunk_size] - - -def cat_response(theta_file, - out_prefix="tfs_cat_response", - theta_col=None, - sigma_col=None, - models=None, - num_workers=-1): - """ - Classify each genotype's theta-vs-titrant curve using categorical response models. - - Reads the CSV output of tfs-predict-theta and fits one or more response models - to every (genotype, titrant_name) group. For each group the best-fitting model - is selected by AIC weight. Results include best_model, AIC weights, and - parameter estimates for every fitted model. Writes one row per - (genotype, titrant_name) pair to {out_prefix}.csv. - - Parameters - ---------- - theta_file : str - Path to the CSV file produced by tfs-predict-theta. Must contain columns: - genotype, titrant_name, titrant_conc, and the theta and sigma columns. - out_prefix : str, optional - Prefix for the output CSV file. Written to {out_prefix}.csv. - Default 'tfs_cat_response'. - theta_col : str or None, optional - Name of the column holding theta values passed to the fitter. If - ``None`` (default), the column is auto-detected: ``median`` is used if - present, then ``point_est``. Pass explicitly to override. - sigma_col : str or None, optional - Name of the column holding per-row theta uncertainty (standard deviation). - If None, sigma is computed as (upper_std - lower_std) / 2, which requires - upper_std and lower_std columns to be present. - models : list of str or None, optional - Response models to fit. Defaults to all models in MODEL_LIBRARY. - num_workers : int, optional - Number of parallel worker processes. ``1`` runs serially in-process; - ``-1`` (the default) uses ``os.cpu_count() - 1``; ``N`` uses ``N`` - processes. The per-pair fits are embarrassingly parallel, so this scales - nearly linearly on large libraries. - """ - if models is None: - models = list(MODEL_LIBRARY.keys()) - - bad = [m for m in models if m not in MODEL_LIBRARY] - if bad: - raise ValueError(f"Unknown model(s): {bad}. Valid: {list(MODEL_LIBRARY)}") - - print(f"Reading {theta_file}...", flush=True) - df = pd.read_csv(theta_file) - - if theta_col is None: - if "q0.5" in df.columns: - theta_col = "q0.5" - elif "point_est" in df.columns: - theta_col = "point_est" - else: - raise ValueError( - "No theta column found. Expected 'q0.5' (posterior median) or " - "'point_est' (MAP). Use --theta_col to specify a column explicitly." - ) - - if sigma_col is None: - if "q0.841" in df.columns and "q0.159" in df.columns: - df = df.copy() - df["_sigma"] = (df["q0.841"] - df["q0.159"]) / 2 - sigma_col = "_sigma" - else: - raise ValueError( - "No sigma_col specified and df lacks q0.841/q0.159 columns. " - "Provide --sigma_col or ensure q0.841 and q0.159 are present." - ) - - work_items = [] - for (genotype, titrant_name), group in df.groupby(["genotype", "titrant_name"], - sort=False): - x = group["titrant_conc"].to_numpy(dtype=float) - y = group[theta_col].to_numpy(dtype=float) - y_std = group[sigma_col].to_numpy(dtype=float) - work_items.append((genotype, titrant_name, x, y, y_std, models)) - - n_total = len(work_items) - workers = resolve_workers(num_workers) - chunks = list(_iter_chunks(work_items, _CHUNK_SIZE)) - - print(f" Fitting {n_total} (genotype, titrant_name) pairs " - f"with {workers} worker(s)...", flush=True) - - results = [] - n_done = 0 - - def _report(n_done): - if n_done % 5000 < _CHUNK_SIZE or n_done == n_total: - print(f" {n_done}/{n_total} fits complete", flush=True) - - if workers == 1: - # Serial fast-path: run in-process, no pickling/IPC overhead. - for chunk in chunks: - results.extend(_fit_chunk(chunk)) - n_done += len(chunk) - _report(n_done) - else: - # executor.map preserves input order, so results stay aligned with - # work_items without an explicit index map. - with ProcessPoolExecutor(max_workers=workers) as executor: - for chunk_result in executor.map(_fit_chunk, chunks): - results.extend(chunk_result) - n_done += len(chunk_result) - _report(n_done) - - out_df = pd.DataFrame(results) - id_cols = ["genotype", "titrant_name"] - other_cols = [c for c in out_df.columns if c not in id_cols] - out_df = out_df[id_cols + other_cols].copy() - out_df.columns = [c.replace("|", "_") for c in out_df.columns] - - out_file = f"{out_prefix}.csv" - out_df.to_csv(out_file, index=False) - print(f"Wrote {len(out_df)} rows to {out_file}", flush=True) - - -def main(): - generalized_main(cat_response, - manual_arg_types={"theta_col": str, - "sigma_col": str, - "models": str}, - manual_arg_nargs={"models": "+"}) - - -if __name__ == "__main__": - main() diff --git a/src/tfscreen/analysis/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index 7e58dc7..b0042de 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -8,7 +8,7 @@ def mutant_cycle_pivot( df: pd.DataFrame, extract_columns: List[str], - condition_selector: List[str] | str | None = None, + group_by: List[str] | str | None = None, verbose: bool = False, ) -> pd.DataFrame: """ @@ -28,10 +28,10 @@ def mutant_cycle_pivot( extract_columns : list[str], A list of column names whose values will be extracted and placed into the new wide-format columns (e.g., 'fitness', 'expression'). - condition_selector : list[str] or str or None, optional + group_by : list[str] or str or None, optional Column name(s) to group the DataFrame by. The analysis is performed - independently on each group. If None, treat the whole dataframe in a - single analysis. + independently on each group. If None, treat the whole dataframe in a + single analysis. verbose : bool, default False If True, print status messages about skipped groups or dropped data. @@ -74,10 +74,10 @@ def mutant_cycle_pivot( else: df_proc["m1"] = None - if condition_selector is None: + if group_by is None: grouper = [(None,df_proc)] else: - grouper = df_proc.groupby(condition_selector) + grouper = df_proc.groupby(group_by) result_dfs = [] for group, sub_df in grouper: @@ -130,7 +130,7 @@ def extract_epistasis( df: pd.DataFrame, y_obs: str, y_std: Optional[str] = None, - condition_selector: List[str] | str | None=None, + group_by: List[str] | str | None=None, scale: str = "add", keep_extra: bool = False ) -> pd.DataFrame: @@ -154,9 +154,9 @@ def extract_epistasis( y_std : str, optional The name of the column containing the standard error for `y_obs`. If provided, the error on the epistasis (`ep_std`) will be calculated. - condition_selector : list[str] or str or None + group_by : list[str] or str or None Column name(s) that define a unique experimental condition. Epistasis - is calculated independently for each condition. If None, treat all + is calculated independently for each condition. If None, treat all conditions at once scale : {"add", "mult"}, default "add" The scale for calculating epistasis. @@ -193,7 +193,7 @@ def extract_epistasis( # Build a dataframe with mutant cycles cycles = mutant_cycle_pivot(df, extract_columns=extract_columns, - condition_selector=condition_selector) + group_by=group_by) # No valid mutant cycles were found (e.g. no double mutants, or no wt). The # pivot returns an empty frame with no cycle columns, so short-circuit before @@ -205,11 +205,11 @@ def extract_epistasis( if not keep_extra: keep = ["genotype"] - - if condition_selector is not None: - if isinstance(condition_selector, str): - condition_selector = [condition_selector] - keep.extend(condition_selector) + + if group_by is not None: + if isinstance(group_by, str): + group_by = [group_by] + keep.extend(group_by) for c in extract_columns: keep.extend([f"{mut}_{c}" for mut in ["00","01","10","11"]]) diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py new file mode 100644 index 0000000..9a60fa4 --- /dev/null +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -0,0 +1,175 @@ +""" +CLI for fitting categorical response models to a y-vs-x curve per group. +""" + +import pandas as pd + +from tfscreen.analysis.cat_response.cat_response import ( + cat_response as _cat_response, +) +from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.util.cli import generalized_main + + +def _write_per_model(results_df, models, group_cols, out_prefix): + """ + Explode the flat results table into one CSV per model. + + ``results_df`` carries every model's parameters in ``||est`` / + ``||std`` columns (plus ``AIC_weight|`` / ``R2|``). + For each model this pulls out its columns, renames them to ``_est`` / + ``_std``, appends per-model fit stats, and writes + ``{out_prefix}_{model}.csv``. + """ + for model in models: + + model_cols = [c for c in results_df.columns + if c.startswith(f"{model}|")] + model_df = results_df[group_cols + model_cols].copy() + + # "||est" -> "_est" + renamer = {} + for c in model_cols: + parts = c.split("|") + renamer[c] = f"{parts[1]}_{parts[2]}" + model_df = model_df.rename(columns=renamer) + + # Per-model fit statistics. + model_df["is_best_model"] = (results_df["best_model"] == model).values + model_df["R2"] = results_df.get( + f"R2|{model}", pd.Series(index=results_df.index, dtype=float) + ).values + model_df["AIC_weight"] = results_df.get( + f"AIC_weight|{model}", pd.Series(index=results_df.index, dtype=float) + ).values + + # Order columns: group keys, est..., std..., stats. + param_names = MODEL_LIBRARY[model]["param_names"] + ordered = list(group_cols) + ordered += [f"{p}_est" for p in param_names] + ordered += [f"{p}_std" for p in param_names] + ordered += ["is_best_model", "R2", "AIC_weight"] + model_df = model_df[ordered] + + model_df.to_csv(f"{out_prefix}_{model}.csv", index=False) + + +def cat_response(data_file, + x_obs, + y_obs, + out_prefix="tfs_cat_response", + y_std=None, + group_by=None, + models=None, + num_workers=-1): + """ + Classify each group's response curve using categorical response models. + + Reads a long-form CSV and fits one or more response models to every group. + Groups are defined by the 'genotype' column plus any --group_by columns. For + each group the best-fitting model is selected by AIC weight. + + Writes: + - {out_prefix}.csv one row per group; all models' weights and + parameter estimates. + - {out_prefix}_{model}.csv one file per model; that model's parameter + table and per-group fit statistics. + - {out_prefix}_predictions.csv predicted curves for every group and model. + + Parameters + ---------- + data_file : str + Path to the input CSV. Must contain a 'genotype' column, ``x_obs``, + ``y_obs``, and (if given) ``y_std`` and every column in ``group_by``. + x_obs : str + Name of the column holding the independent variable (e.g. 'titrant_conc'). + y_obs : str + Name of the column holding the observable (e.g. 'q0.5', 'point_est'). + out_prefix : str, optional + Prefix for the output CSV files. Default 'tfs_cat_response'. + y_std : str or None, optional + Name of the column holding per-row uncertainty (standard deviation). If + None (default) and both 'q0.841' and 'q0.159' are present, sigma is + computed as (q0.841 - q0.159) / 2; otherwise the fit is unweighted. + group_by : list of str or None, optional + Additional column(s) that, together with 'genotype', define a group. If + omitted, groups are defined by 'genotype' alone. + models : list of str or None, optional + Response models to fit. Defaults to all models in MODEL_LIBRARY. + num_workers : int, optional + Number of parallel worker processes. ``1`` runs serially; ``-1`` (the + default) uses ``os.cpu_count() - 1``; ``N`` uses ``N`` processes. + """ + if models is None: + models = list(MODEL_LIBRARY.keys()) + + bad = [m for m in models if m not in MODEL_LIBRARY] + if bad: + raise ValueError(f"Unknown model(s): {bad}. Valid: {list(MODEL_LIBRARY)}") + + print(f"Reading {data_file}...", flush=True) + df = pd.read_csv(data_file) + + group_cols = ["genotype"] + (list(group_by) if group_by else []) + + # Sigma fallback: symmetric quantile half-width when no explicit column and + # tfs-predict-theta's quantile columns are present. + if y_std is None and "q0.841" in df.columns and "q0.159" in df.columns: + df = df.copy() + df["_sigma"] = (df["q0.841"] - df["q0.159"]) / 2 + y_std = "_sigma" + + # Fail fast on missing columns rather than deep inside the group loop. + required = list(dict.fromkeys(group_cols + [x_obs, y_obs] + + ([y_std] if y_std is not None else []))) + missing = [c for c in required if c not in df.columns] + if missing: + raise ValueError( + f"Input file '{data_file}' is missing required column(s): {missing}. " + f"Available columns: {list(df.columns)}" + ) + + if y_std is None: + print("Warning: no y_std column found or specified; fitting unweighted.", + flush=True) + + print(f" Fitting groups defined by {group_cols} " + f"with {num_workers} worker(s)...", flush=True) + + results_df, predictions_df = _cat_response(df, + x_obs=x_obs, + y_obs=y_obs, + y_std=y_std, + group_by=group_by, + models_to_run=models, + num_workers=num_workers) + + # Main table: flat, one row per group, with clean column names. + main_df = results_df.copy() + main_df.columns = [c.replace("|", "_") for c in main_df.columns] + out_file = f"{out_prefix}.csv" + main_df.to_csv(out_file, index=False) + print(f"Wrote {len(main_df)} rows to {out_file}", flush=True) + + # Per-model parameter tables. + _write_per_model(results_df, models, group_cols, out_prefix) + print(f"Wrote {len(models)} per-model file(s) " + f"({out_prefix}_.csv)", flush=True) + + # Predicted curves. + pred_file = f"{out_prefix}_predictions.csv" + predictions_df.to_csv(pred_file, index=False) + print(f"Wrote {len(predictions_df)} rows to {pred_file}", flush=True) + + +def main(): + generalized_main(cat_response, + manual_arg_types={"y_std": str, + "group_by": str, + "models": str}, + manual_arg_nargs={"group_by": "+", + "models": "+"}) + + +if __name__ == "__main__": + main() diff --git a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py index ca7b94f..8388462 100644 --- a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -10,12 +10,12 @@ from tfscreen.util.cli import generalized_main -def _diagnose_empty_output(df, condition_selector, exclude=()): +def _diagnose_empty_output(df, group_by, exclude=()): """ Explain why no mutant cycles were found and, when possible, suggest a fix. The most common cause is a table with one row per genotype *per condition* - (e.g. per titrant_conc) run without ``--condition_selector``. In that case + (e.g. per titrant_conc) run without ``--group_by``. In that case every genotype is non-unique within the single implicit group, so ``mutant_cycle_pivot`` drops all rows as duplicates and returns nothing. This inspects the input and, if that is what happened, names the column(s) that @@ -25,8 +25,8 @@ def _diagnose_empty_output(df, condition_selector, exclude=()): ---------- df : pandas.DataFrame The raw input table (with a 'genotype' column). - condition_selector : list of str or None - The condition columns that were used, if any. + group_by : list of str or None + The grouping columns that were used, if any. exclude : iterable of str, optional Columns that must never be suggested as condition selectors -- namely the observable and its error column, whose values legitimately vary within a @@ -40,7 +40,7 @@ def _diagnose_empty_output(df, condition_selector, exclude=()): if "genotype" not in df.columns: return None - used = list(condition_selector) if condition_selector is not None else [] + used = list(group_by) if group_by is not None else [] exclude = set(exclude) # Recreate the grouping the pivot used, and check for non-unique genotypes @@ -79,7 +79,7 @@ def _diagnose_empty_output(df, condition_selector, exclude=()): ) lines.append(f" Columns that vary within a genotype: {candidates}") lines.append( - f" Try: --condition_selector {' '.join(hint_cols)}" + f" Try: --group_by {' '.join(hint_cols)}" ) else: lines.append( @@ -93,7 +93,7 @@ def extract_epistasis(data_file, y_obs, out_prefix="tfs_epistasis", y_std=None, - condition_selector=None, + group_by=None, scale="add", keep_extra=False): """ @@ -110,7 +110,7 @@ def extract_epistasis(data_file, data_file : str Path to the input CSV. Must contain a 'genotype' column, the column named by y_obs, and (if given) the columns named by y_std and - condition_selector. + group_by. y_obs : str Name of the column holding the observable for which epistasis is calculated (e.g. 'fitness', 'dG'). @@ -120,7 +120,7 @@ def extract_epistasis(data_file, y_std : str or None, optional Name of the column holding the standard error of y_obs. If given, the epistasis error (ep_std) is propagated and written. - condition_selector : list of str or None, optional + group_by : list of str or None, optional One or more column names that define a unique experimental condition. Epistasis is calculated independently within each condition. If omitted, the whole table is treated as a single condition. @@ -139,8 +139,8 @@ def extract_epistasis(data_file, required = ["genotype", y_obs] if y_std is not None: required.append(y_std) - if condition_selector is not None: - required.extend(condition_selector) + if group_by is not None: + required.extend(group_by) missing = [c for c in required if c not in df.columns] if missing: @@ -152,7 +152,7 @@ def extract_epistasis(data_file, result = _extract_epistasis(df, y_obs=y_obs, y_std=y_std, - condition_selector=condition_selector, + group_by=group_by, scale=scale, keep_extra=keep_extra) @@ -161,7 +161,7 @@ def extract_epistasis(data_file, print("Warning: no valid mutant cycles found; writing empty output.", flush=True) exclude = [c for c in (y_obs, y_std) if c is not None] - hint = _diagnose_empty_output(df, condition_selector, exclude=exclude) + hint = _diagnose_empty_output(df, group_by, exclude=exclude) if hint is not None: print(hint, flush=True) @@ -172,9 +172,9 @@ def extract_epistasis(data_file, def main(): generalized_main(extract_epistasis, manual_arg_types={"y_std": str, - "condition_selector": str, + "group_by": str, "scale": str}, - manual_arg_nargs={"condition_selector": "+"}) + manual_arg_nargs={"group_by": "+"}) if __name__ == "__main__": diff --git a/src/tfscreen/plot/heatmap/epistasis_heatmap.py b/src/tfscreen/plot/heatmap/epistasis_heatmap.py index 00ff42e..9b7a912 100644 --- a/src/tfscreen/plot/heatmap/epistasis_heatmap.py +++ b/src/tfscreen/plot/heatmap/epistasis_heatmap.py @@ -31,12 +31,12 @@ def epistasis_heatmap(df,r1,r2, # Make sure the genotypes are all unique if np.any(df[zero]["genotype"].duplicated()): raise ValueError ( - "condition_selector must be unique to plot an epistasis heat map." + "group_by must be unique to plot an epistasis heat map." ) - + # Extract epistasis ep_df = tfscreen.analysis.extract_epistasis(sub_df, - condition_selector=None, #condition_selector, + group_by=None, y_obs=value_column) ep_df = tfscreen.genetics.expand_genotype_columns(ep_df) diff --git a/tests/tfscreen/analysis/cat_response/scripts/__init__.py b/tests/tfscreen/analysis/cat_response/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py deleted file mode 100644 index 656df54..0000000 --- a/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -Tests for cat_response_cli.py — theta_col auto-detection, error handling, and -the serial/parallel/chunked dispatch machinery. -""" -import pytest -import numpy as np -import pandas as pd -from unittest.mock import patch - -from tfscreen.analysis.cat_response.scripts import cat_response_cli -from tfscreen.analysis.cat_response.scripts.cat_response_cli import ( - cat_response, - _fit_one, - _fit_chunk, - _iter_chunks, -) - - -_FLAT_RESULT = {"best_model": "flat", "status": "success"} - - -def _make_theta_df(center_col): - return pd.DataFrame({ - "genotype": ["wt", "wt"], - "titrant_name": ["IPTG", "IPTG"], - "titrant_conc": [0.0, 1.0], - center_col: [0.3, 0.7], - "q0.841": [0.4, 0.8], - "q0.159": [0.2, 0.6], - }) - - -class TestThetaColAutoDetect: - """theta_col / sigma_col resolution. Driven serially (num_workers=1) so the - cat_fit mock runs in-process.""" - - def _run_capture(self, tmp_path, df, **kwargs): - f = str(tmp_path / "theta.csv") - captured = {} - - def fake_fit(x, y, y_std, models_to_run): - captured["y"] = list(y) - captured["y_std"] = list(y_std) - return (dict(_FLAT_RESULT), None) - - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", - return_value=df), \ - patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - side_effect=fake_fit): - cat_response(f, out_prefix=str(tmp_path / "out"), - num_workers=1, **kwargs) - return captured - - def test_autodetects_q0_5(self, tmp_path): - """Values from q0.5 column are passed to the fitter.""" - captured = self._run_capture(tmp_path, _make_theta_df("q0.5")) - assert captured["y"] == pytest.approx([0.3, 0.7]) - - def test_autodetects_point_est_values(self, tmp_path): - """point_est is used when q0.5 is absent.""" - captured = self._run_capture(tmp_path, _make_theta_df("point_est")) - assert captured["y"] == pytest.approx([0.3, 0.7]) - - def test_explicit_theta_col_overrides(self, tmp_path): - """Explicit theta_col is used even when q0.5 is also present.""" - df = _make_theta_df("q0.5").copy() - df["my_col"] = [0.11, 0.22] - captured = self._run_capture(tmp_path, df, theta_col="my_col") - assert captured["y"] == pytest.approx([0.11, 0.22]) - - def test_q0_5_preferred_over_point_est(self, tmp_path): - """When both q0.5 and point_est are present, q0.5 wins.""" - df = _make_theta_df("q0.5").copy() - df["point_est"] = [0.9, 0.8] - captured = self._run_capture(tmp_path, df) - assert captured["y"] == pytest.approx([0.3, 0.7]) - - def test_sigma_from_quantiles(self, tmp_path): - """When sigma_col is absent, sigma = (q0.841 - q0.159) / 2.""" - captured = self._run_capture(tmp_path, _make_theta_df("q0.5")) - # (0.4-0.2)/2 = 0.1 ; (0.8-0.6)/2 = 0.1 - assert captured["y_std"] == pytest.approx([0.1, 0.1]) - - def test_raises_when_no_theta_col(self, tmp_path): - """ValueError when neither q0.5 nor point_est is present.""" - f = str(tmp_path / "theta.csv") - df = pd.DataFrame({ - "genotype": ["wt"], - "titrant_name": ["IPTG"], - "titrant_conc": [0.0], - "q0.841": [0.5], - "q0.159": [0.3], - }) - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", - return_value=df): - with pytest.raises(ValueError, match="No theta column found"): - cat_response(f, out_prefix=str(tmp_path / "out")) - - -class TestChunkHelpers: - - def test_iter_chunks_partitions_exactly(self): - items = list(range(10)) - chunks = list(_iter_chunks(items, 3)) - assert chunks == [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] - - def test_iter_chunks_empty(self): - assert list(_iter_chunks([], 3)) == [] - - def test_iter_chunks_larger_than_input(self): - assert list(_iter_chunks([1, 2], 100)) == [[1, 2]] - - def test_fit_one_tags_genotype_and_titrant(self): - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - return_value=(dict(_FLAT_RESULT), None)): - out = _fit_one("g1", "IPTG", - np.array([0.0, 1.0]), np.array([0.2, 0.8]), - np.array([0.1, 0.1]), ["flat"]) - assert out["genotype"] == "g1" - assert out["titrant_name"] == "IPTG" - assert out["best_model"] == "flat" - - def test_fit_chunk_preserves_order(self): - # cat_fit echoes the number of points so we can verify ordering. - def fake_fit(x, y, y_std, models_to_run): - return ({"n": len(x)}, None) - - items = [ - ("g0", "IPTG", np.zeros(2), np.zeros(2), np.ones(2), ["flat"]), - ("g1", "IPTG", np.zeros(3), np.zeros(3), np.ones(3), ["flat"]), - ] - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.cat_fit", - side_effect=fake_fit): - out = _fit_chunk(items) - assert [r["genotype"] for r in out] == ["g0", "g1"] - assert [r["n"] for r in out] == [2, 3] - - -class TestDispatchEquivalence: - """Serial and parallel paths must produce identical, correctly-ordered - output across chunk boundaries. Uses the real (deterministic) cat_fit so the - ProcessPoolExecutor path is exercised end-to-end (a parent-process monkeypatch - would not reach spawned workers).""" - - def _many_genotype_df(self, n_geno): - # Four concentrations per genotype: enough points for the fitters, and a - # different (rising) curve per genotype so rows are distinguishable. - concs = np.array([0.0, 1.0, 10.0, 100.0]) - rows = [] - for i in range(n_geno): - center = 0.1 + 0.7 * (concs / concs.max()) + 0.001 * i - for c, mid in zip(concs, center): - rows.append({"genotype": f"g{i}", "titrant_name": "IPTG", - "titrant_conc": float(c), "q0.5": float(mid), - "q0.841": float(mid + 0.05), - "q0.159": float(mid - 0.05)}) - return pd.DataFrame(rows) - - def _run(self, tmp_path, df, num_workers, out_name): - f = str(tmp_path / "theta.csv") - out_prefix = str(tmp_path / out_name) - with patch("tfscreen.analysis.cat_response.scripts.cat_response_cli.pd.read_csv", - return_value=df): - cat_response(f, out_prefix=out_prefix, num_workers=num_workers) - return pd.read_csv(out_prefix + ".csv") - - def test_serial_row_order(self, tmp_path): - # Force multiple chunks by shrinking the chunk size. - df = self._many_genotype_df(7) - with patch.object(cat_response_cli, "_CHUNK_SIZE", 2): - out = self._run(tmp_path, df, num_workers=1, out_name="serial") - assert list(out["genotype"]) == [f"g{i}" for i in range(7)] - assert len(out) == 7 - - def test_parallel_matches_serial(self, tmp_path): - # Multiple chunks spread across 2 real worker processes. - df = self._many_genotype_df(7) - with patch.object(cat_response_cli, "_CHUNK_SIZE", 2): - serial = self._run(tmp_path, df, num_workers=1, out_name="serial") - parallel = self._run(tmp_path, df, num_workers=2, out_name="parallel") - pd.testing.assert_frame_equal(serial, parallel) - assert list(parallel["genotype"]) == [f"g{i}" for i in range(7)] diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 79fe604..5c48db8 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -1,119 +1,208 @@ - -import pytest +""" +Tests for the generic cat_response core: grouping, column selection, validation, +prediction tagging, and serial/parallel equivalence. +""" import numpy as np import pandas as pd -from unittest.mock import MagicMock, patch - -from tfscreen.analysis.cat_response.cat_response import cat_response - -@pytest.fixture -def mock_df(): - # Create a DataFrame with 2 genotypes - data = { - "genotype": ["WT", "WT", "MUT", "MUT"], - "titrant_conc": [1.0, 2.0, 1.0, 2.0], - "theta_est": [10.0, 20.0, 5.0, 15.0], - "theta_std": [0.1, 0.1, 0.2, 0.2] - } - return pd.DataFrame(data) - -@patch("tfscreen.analysis.cat_response.cat_response.cat_fit") -@patch("tfscreen.analysis.cat_response.cat_response.MODEL_LIBRARY") -def test_cat_response_integration(mock_library, mock_cat_fit, mock_df): - """Test standard cat_response execution aggregating multiple genotypes.""" - - # Setup MODEL_LIBRARY to have one model - mock_library.keys.return_value = ["linear"] - mock_library.__getitem__.return_value = {"param_names": ["m", "b"]} - - # Setup cat_fit return values for each call (2 genotypes) - # We need to return (flat_output, pred_df) - - # WT Results - flat_WT = { - "status": "success", - "best_model": "linear", - "best_model_R2": 0.99, - "best_model_AIC_weight": 0.8, - "AIC_weight|linear": 0.8, - "R2|linear": 0.99, - "linear|m|est": 10.0, - "linear|m|std": 0.1, - "linear|b|est": 0.0, - "linear|b|std": 0.1 - } - pred_df_WT = pd.DataFrame({ - "model": ["linear"]*2, "x": [1,2], "y": [10,20], "y_std": [0,0], "is_best_model": [True]*2 - }) - - # MUT Results - flat_MUT = { - "status": "success", - "best_model": "linear", - "best_model_R2": 0.95, - "best_model_AIC_weight": 0.9, - "AIC_weight|linear": 0.9, - "R2|linear": 0.95, - "linear|m|est": 5.0, # Difference - "linear|m|std": 0.2, - "linear|b|est": 0.0, - "linear|b|std": 0.2 - } - pred_df_MUT = pd.DataFrame({ - "model": ["linear"]*2, "x": [1,2], "y": [5,15], "y_std": [0,0], "is_best_model": [True]*2 +import pytest +from unittest.mock import patch + +import importlib + +from tfscreen.analysis.cat_response.cat_response import ( + cat_response, + _iter_chunks, +) + +# The package __init__ rebinds the name ``cat_response`` to the function, which +# shadows the submodule for ``import ... as`` -- grab the module object directly +# so patch.object targets the module's cat_fit / _CHUNK_SIZE. +cat_response_mod = importlib.import_module( + "tfscreen.analysis.cat_response.cat_response" +) + + +# --- helpers ----------------------------------------------------------------- + +def _flat(best="flat"): + """A minimal flat cat_fit result dict.""" + return {"status": "success", "best_model": best} + + +def _pred(n=2): + """A minimal cat_fit prediction frame.""" + return pd.DataFrame({ + "model": ["flat"] * n, + "x": np.arange(n, dtype=float), + "y": np.zeros(n), + "y_std": np.zeros(n), + "is_best_model": [True] * n, }) - - mock_cat_fit.side_effect = [(flat_WT, pred_df_WT), (flat_MUT, pred_df_MUT)] - - # Run function - model_dfs, summary_df, pred_df = cat_response( - mock_df, - models_to_run=["linear"], - verbose=False - ) - - # 1. Verify cat_fit calls - assert mock_cat_fit.call_count == 2 - - # 2. Verify summary DataFrame - assert len(summary_df) == 2 - assert "best_model" in summary_df.columns - assert "w_linear" in summary_df.columns # Renamed from AIC_weight|linear - assert summary_df.loc["WT", "best_model_R2"] == 0.99 - - # 3. Verify model_dataframes - assert "linear" in model_dfs - lin_df = model_dfs["linear"] - assert len(lin_df) == 2 - # Check column renaming: linear|m|est -> m_est - assert "m_est" in lin_df.columns - assert lin_df.loc["WT", "m_est"] == 10.0 - assert lin_df.loc["MUT", "m_est"] == 5.0 - - # 4. Verify pred_df - assert len(pred_df) == 4 # 2 genotypes * 2 points - assert "genotype" in pred_df.columns - assert set(pred_df["genotype"]) == {"WT", "MUT"} - -@patch("tfscreen.analysis.cat_response.cat_response.cat_fit") -@patch("tfscreen.analysis.cat_response.cat_response.MODEL_LIBRARY") -def test_cat_response_default_models(mock_library, mock_cat_fit, mock_df): - """Test behavior when models_to_run is None (defaults to library keys).""" - - mock_library.keys.return_value = ["m1"] - mock_library.__getitem__.return_value = {"param_names": ["p"]} - - flat_res = { - "status": "success", "best_model": "m1", "best_model_R2": 1, "best_model_AIC_weight": 1.0, - "AIC_weight|m1": 1, "R2|m1": 1, - "m1|p|est": 0, "m1|p|std": 0 - } - mock_cat_fit.return_value = (flat_res, pd.DataFrame({"model": ["m1"], "is_best_model":[True]})) - - model_dfs, summary_df, pred_df = cat_response(mock_df) # models_to_run=None - - assert "m1" in model_dfs - # Verify cat_fit was called with models_to_run=["m1"] - _, kwargs = mock_cat_fit.call_args - assert kwargs["models_to_run"] == ["m1"] + +def _capturing_fit(store): + """A fake cat_fit that records the (x, y, y_std) it was handed per call.""" + def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): + store.append({"x": list(x), "y": list(y), "y_std": list(y_std)}) + return _flat(), _pred(len(x)) + return fake_fit + + +def _basic_df(): + """Two genotypes, two titrants, two points each.""" + rows = [] + for geno in ["wt", "m1"]: + for titr in ["IPTG", "aTc"]: + for conc, val in [(0.0, 0.3), (1.0, 0.7)]: + rows.append({"genotype": geno, "titrant_name": titr, + "titrant_conc": conc, "theta": val, + "theta_std": 0.1}) + return pd.DataFrame(rows) + + +# --- grouping ---------------------------------------------------------------- + +class TestGrouping: + + def test_groups_by_genotype_only_by_default(self): + store = [] + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + results, _ = cat_response(_basic_df(), x_obs="titrant_conc", + y_obs="theta", y_std="theta_std") + # 2 genotypes -> 2 groups (titrant_name is ignored without group_by). + assert len(results) == 2 + assert set(results["genotype"]) == {"wt", "m1"} + assert "titrant_name" not in results.columns + + def test_group_by_adds_a_column(self): + store = [] + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + results, _ = cat_response(_basic_df(), x_obs="titrant_conc", + y_obs="theta", y_std="theta_std", + group_by=["titrant_name"]) + # 2 genotypes x 2 titrants -> 4 groups. + assert len(results) == 4 + assert set(results.columns[:2]) == {"genotype", "titrant_name"} + combos = set(zip(results["genotype"], results["titrant_name"])) + assert combos == {("wt", "IPTG"), ("wt", "aTc"), + ("m1", "IPTG"), ("m1", "aTc")} + + def test_works_without_titrant_name_column(self): + """The original friction: a table with no titrant_name still groups.""" + df = _basic_df().drop(columns=["titrant_name"]) + store = [] + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + results, _ = cat_response(df, x_obs="titrant_conc", + y_obs="theta", y_std="theta_std") + assert len(results) == 2 + assert set(results["genotype"]) == {"wt", "m1"} + + +# --- column selection -------------------------------------------------------- + +class TestColumnSelection: + + def test_passes_selected_columns_to_fitter(self): + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", group_by=["titrant_name"]) + # Every call gets the two theta values for its group; 4 groups x 2 pts. + all_y = sorted(v for call in store for v in call["y"]) + assert all_y == pytest.approx([0.3] * 4 + [0.7] * 4) + # y_std comes from the named column, not uniform weights. + assert all(all(s == 0.1 for s in call["y_std"]) for call in store) + + def test_none_y_std_uses_uniform_weights(self): + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", y_std=None) + # Uniform weights: every y_std handed to cat_fit is 1.0. + assert all(all(s == 1.0 for s in call["y_std"]) for call in store) + + +# --- validation -------------------------------------------------------------- + +class TestValidation: + + def test_missing_y_obs_raises(self): + with pytest.raises(ValueError, match="missing required column"): + cat_response(_basic_df(), x_obs="titrant_conc", y_obs="nope") + + def test_missing_group_by_raises(self): + with pytest.raises(ValueError, match="missing required column"): + cat_response(_basic_df(), x_obs="titrant_conc", y_obs="theta", + group_by=["nope"]) + + def test_unknown_model_raises(self): + with pytest.raises(ValueError, match="Unknown model"): + cat_response(_basic_df(), x_obs="titrant_conc", y_obs="theta", + models_to_run=["not_a_model"]) + + +# --- predictions ------------------------------------------------------------- + +class TestPredictions: + + def test_predictions_tagged_with_group_keys(self): + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([])): + _, preds = cat_response(_basic_df(), x_obs="titrant_conc", + y_obs="theta", y_std="theta_std", + group_by=["titrant_name"]) + # Group keys come first, then the cat_fit prediction columns. + assert list(preds.columns[:2]) == ["genotype", "titrant_name"] + for col in ["model", "x", "y", "y_std", "is_best_model"]: + assert col in preds.columns + # Every prediction row carries a real group key. + assert not preds["genotype"].isna().any() + + +# --- chunk helper ------------------------------------------------------------ + +class TestIterChunks: + + def test_partitions_exactly(self): + assert list(_iter_chunks(list(range(7)), 3)) == [[0, 1, 2], [3, 4, 5], [6]] + + def test_empty(self): + assert list(_iter_chunks([], 3)) == [] + + +# --- serial / parallel equivalence (real cat_fit) ---------------------------- + +class TestDispatchEquivalence: + """Serial and parallel paths must produce identical, correctly-ordered + output. Uses the real (deterministic) cat_fit so the ProcessPoolExecutor + path is exercised end-to-end.""" + + def _many_genotype_df(self, n_geno): + concs = np.array([0.0, 1.0, 10.0, 100.0]) + rows = [] + for i in range(n_geno): + center = 0.1 + 0.7 * (concs / concs.max()) + 0.001 * i + for c, mid in zip(concs, center): + rows.append({"genotype": f"g{i}", "titrant_conc": float(c), + "theta": float(mid), "theta_std": 0.05}) + return pd.DataFrame(rows) + + def test_parallel_matches_serial(self): + df = self._many_genotype_df(7) + with patch.object(cat_response_mod, "_CHUNK_SIZE", 2): + serial, serial_pred = cat_response( + df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", num_workers=1) + parallel, parallel_pred = cat_response( + df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", num_workers=2) + + assert list(serial["genotype"]) == [f"g{i}" for i in range(7)] + pd.testing.assert_frame_equal(serial, parallel) + pd.testing.assert_frame_equal(serial_pred, parallel_pred) diff --git a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py new file mode 100644 index 0000000..146fc67 --- /dev/null +++ b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py @@ -0,0 +1,171 @@ +""" +Tests for cat_response_cli.py -- CSV IO, sigma fallback, column validation, +per-model / prediction output files, and argument wiring. +""" +import os +import sys + +import numpy as np +import pandas as pd +import pytest +from unittest.mock import patch + +from tfscreen.analysis.scripts import cat_response_cli +from tfscreen.analysis.scripts.cat_response_cli import ( + cat_response, + _write_per_model, + main, +) + + +def _theta_df(with_titrant=True, with_std=False, with_quantiles=True): + """A small predict-theta-style table: 2 genotypes x 4 concentrations.""" + concs = np.array([0.0, 1.0, 10.0, 100.0]) + rows = [] + for i, geno in enumerate(["wt", "m1"]): + center = 0.1 + 0.7 * (concs / concs.max()) + 0.01 * i + for c, mid in zip(concs, center): + row = {"genotype": geno, "titrant_conc": float(c), "q0.5": float(mid)} + if with_titrant: + row["titrant_name"] = "IPTG" + if with_quantiles: + row["q0.841"] = float(mid + 0.05) + row["q0.159"] = float(mid - 0.05) + if with_std: + row["my_std"] = 0.05 + rows.append(row) + return pd.DataFrame(rows) + + +def _write(tmp_path, df, name="theta.csv"): + path = str(tmp_path / name) + df.to_csv(path, index=False) + return path + + +class TestOutputFiles: + + def test_writes_main_permodel_and_predictions(self, tmp_path): + data = _write(tmp_path, _theta_df()) + out_prefix = str(tmp_path / "out") + + cat_response(data, x_obs="titrant_conc", y_obs="q0.5", + out_prefix=out_prefix, models=["flat", "linear"], + num_workers=1) + + main = pd.read_csv(f"{out_prefix}.csv") + assert len(main) == 2 # one row per genotype + # Column names are cleaned of the '|' delimiter. + assert not any("|" in c for c in main.columns) + + # One per-model file each, with genotype + est/std columns. + for model in ["flat", "linear"]: + mdf = pd.read_csv(f"{out_prefix}_{model}.csv") + assert len(mdf) == 2 + assert "genotype" in mdf.columns + assert "is_best_model" in mdf.columns + + preds = pd.read_csv(f"{out_prefix}_predictions.csv") + assert "genotype" in preds.columns + assert {"model", "x", "y", "is_best_model"}.issubset(preds.columns) + + +class TestSigmaFallback: + + def test_sigma_from_quantiles_when_no_y_std(self, tmp_path): + captured = {} + + def fake_core(df, **kwargs): + captured["y_std"] = kwargs["y_std"] + captured["sigma_vals"] = list(df[kwargs["y_std"]]) + empty = pd.DataFrame({"genotype": [], "best_model": []}) + return empty, pd.DataFrame({"genotype": []}) + + data = _write(tmp_path, _theta_df(with_quantiles=True)) + with patch.object(cat_response_cli, "_cat_response", + side_effect=fake_core), \ + patch.object(cat_response_cli, "_write_per_model"): + cat_response(data, x_obs="titrant_conc", y_obs="q0.5", + out_prefix=str(tmp_path / "out")) + + assert captured["y_std"] == "_sigma" + # (q0.841 - q0.159)/2 = 0.05 for every row. + assert captured["sigma_vals"] == pytest.approx([0.05] * 8) + + def test_explicit_y_std_takes_precedence(self, tmp_path): + captured = {} + + def fake_core(df, **kwargs): + captured["y_std"] = kwargs["y_std"] + empty = pd.DataFrame({"genotype": [], "best_model": []}) + return empty, pd.DataFrame({"genotype": []}) + + data = _write(tmp_path, _theta_df(with_std=True, with_quantiles=True)) + with patch.object(cat_response_cli, "_cat_response", + side_effect=fake_core), \ + patch.object(cat_response_cli, "_write_per_model"): + cat_response(data, x_obs="titrant_conc", y_obs="q0.5", + y_std="my_std", out_prefix=str(tmp_path / "out")) + + assert captured["y_std"] == "my_std" + + +class TestValidation: + + def test_missing_y_obs_raises(self, tmp_path): + data = _write(tmp_path, _theta_df()) + with pytest.raises(ValueError, match="missing required column"): + cat_response(data, x_obs="titrant_conc", y_obs="nope", + out_prefix=str(tmp_path / "out")) + + def test_unknown_model_raises(self, tmp_path): + data = _write(tmp_path, _theta_df()) + with pytest.raises(ValueError, match="Unknown model"): + cat_response(data, x_obs="titrant_conc", y_obs="q0.5", + models=["nope"], out_prefix=str(tmp_path / "out")) + + +class TestWritePerModel: + + def test_splits_flat_columns_into_param_table(self, tmp_path): + results_df = pd.DataFrame({ + "genotype": ["wt", "m1"], + "best_model": ["linear", "flat"], + "R2|linear": [0.99, 0.5], + "AIC_weight|linear": [0.8, 0.2], + "linear|m|est": [10.0, 5.0], + "linear|m|std": [0.1, 0.2], + "linear|b|est": [0.0, 1.0], + "linear|b|std": [0.1, 0.2], + }) + out_prefix = str(tmp_path / "out") + with patch.object(cat_response_cli, "MODEL_LIBRARY", + {"linear": {"param_names": ["m", "b"]}}): + _write_per_model(results_df, ["linear"], ["genotype"], out_prefix) + + mdf = pd.read_csv(f"{out_prefix}_linear.csv") + assert list(mdf.columns) == ["genotype", "m_est", "b_est", + "m_std", "b_std", + "is_best_model", "R2", "AIC_weight"] + assert mdf.loc[mdf["genotype"] == "wt", "m_est"].iloc[0] == 10.0 + assert bool(mdf.loc[mdf["genotype"] == "wt", "is_best_model"].iloc[0]) + assert not bool(mdf.loc[mdf["genotype"] == "m1", "is_best_model"].iloc[0]) + + +class TestArgWiring: + + def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): + data = _write(tmp_path, _theta_df()) + out_prefix = str(tmp_path / "out") + argv = ["cat_response", data, "titrant_conc", "q0.5", + "--out_prefix", out_prefix, + "--group_by", "titrant_name", + "--models", "flat", "linear", + "--num_workers", "1"] + monkeypatch.setattr(sys, "argv", argv) + + main() + + assert os.path.exists(f"{out_prefix}.csv") + assert os.path.exists(f"{out_prefix}_flat.csv") + assert os.path.exists(f"{out_prefix}_predictions.csv") diff --git a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py index ddb20e4..6375e43 100644 --- a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py +++ b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py @@ -66,7 +66,7 @@ def test_multiplicative_scale(self, tmp_path): # (5/3) / (2/1) = 0.83333... assert out["ep_obs"].iloc[0] == pytest.approx((5.0 / 3.0) / (2.0 / 1.0)) - def test_condition_selector_groups_independently(self, tmp_path): + def test_group_by_groups_independently(self, tmp_path): rows = _single_cycle_rows(condition="c1") + _single_cycle_rows(condition="c2") # Make c2's double mutant produce a different epistasis (ep = 0.0). for r in rows: @@ -76,7 +76,7 @@ def test_condition_selector_groups_independently(self, tmp_path): out_prefix = str(tmp_path / "out") extract_epistasis(data, y_obs="y", - condition_selector=["condition"], + group_by=["condition"], out_prefix=out_prefix) out = pd.read_csv(f"{out_prefix}.csv").sort_values("condition") @@ -112,10 +112,10 @@ def test_missing_y_std_raises(self, tmp_path): extract_epistasis(data, y_obs="y", y_std="nope", out_prefix=str(tmp_path / "out")) - def test_missing_condition_selector_raises(self, tmp_path): + def test_missing_group_by_raises(self, tmp_path): data = _write_csv(tmp_path, _single_cycle_rows()) with pytest.raises(ValueError, match="missing required column"): - extract_epistasis(data, y_obs="y", condition_selector=["nope"], + extract_epistasis(data, y_obs="y", group_by=["nope"], out_prefix=str(tmp_path / "out")) @@ -138,9 +138,9 @@ def test_no_cycles_writes_empty(self, tmp_path, capsys): assert "no valid mutant cycles" in captured assert "Wrote 0 rows" in captured - def test_forgot_condition_selector_hints_column(self, tmp_path, capsys): + def test_forgot_group_by_hints_column(self, tmp_path, capsys): # One row per genotype *per condition* (like titrant_conc), run without - # --condition_selector: every genotype is non-unique -> all dropped. + # --group_by: every genotype is non-unique -> all dropped. rows = [] for conc in [0.0, 0.1, 1.0]: for r in _single_cycle_rows(): @@ -152,10 +152,10 @@ def test_forgot_condition_selector_hints_column(self, tmp_path, capsys): captured = capsys.readouterr().out assert "dropped as duplicates" in captured - assert "--condition_selector titrant_conc" in captured + assert "--group_by titrant_conc" in captured # With the suggested selector it succeeds. extract_epistasis(data, y_obs="y", - condition_selector=["titrant_conc"], + group_by=["titrant_conc"], out_prefix=out_prefix) out = pd.read_csv(f"{out_prefix}.csv") assert len(out) == 3 @@ -173,7 +173,7 @@ def test_no_cycles_does_not_hint_condition(self, tmp_path, capsys): extract_epistasis(data, y_obs="y", out_prefix=out_prefix) captured = capsys.readouterr().out - assert "--condition_selector" not in captured + assert "--group_by" not in captured class TestArgWiring: @@ -187,7 +187,7 @@ def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): argv = ["extract_epistasis", data, "y", "--out_prefix", out_prefix, "--y_std", "y_err", - "--condition_selector", "condition", + "--group_by", "condition", "--scale", "add", "--keep_extra"] monkeypatch.setattr(sys, "argv", argv) diff --git a/tests/tfscreen/analysis/test_extract_epistasis.py b/tests/tfscreen/analysis/test_extract_epistasis.py index f4321bb..1184798 100644 --- a/tests/tfscreen/analysis/test_extract_epistasis.py +++ b/tests/tfscreen/analysis/test_extract_epistasis.py @@ -188,7 +188,7 @@ def test_multiplicative_epistasis(self, base_df_for_epistasis): # E_std uses relative error propagation # ACT - result = extract_epistasis(base_df_for_epistasis, "fitness", "error", condition_selector= "condition",scale="mult") + result = extract_epistasis(base_df_for_epistasis, "fitness", "error", group_by= "condition",scale="mult") # ASSERT cycle_1 = result[result["condition"] == 1].iloc[0] @@ -201,13 +201,13 @@ def test_multiplicative_epistasis(self, base_df_for_epistasis): def test_without_std_dev(self, base_df_for_epistasis): """Tests that the function runs without a y_std column.""" - result = extract_epistasis(base_df_for_epistasis, "fitness", condition_selector="condition") + result = extract_epistasis(base_df_for_epistasis, "fitness", group_by="condition") assert "ep_std" not in result.columns assert np.isclose(result[result["condition"] == 1].iloc[0]["ep_obs"], 0.0) def test_propagates_nan_from_missing_mutant(self, base_df_for_epistasis): """Tests that if a cycle is incomplete, ep_obs and ep_std are NaN.""" - result = extract_epistasis(base_df_for_epistasis, "fitness", "error",condition_selector="condition") + result = extract_epistasis(base_df_for_epistasis, "fitness", "error",group_by="condition") cycle_2 = result[result["condition"] == 2].iloc[0] # The V30A/M40T cycle assert pd.isna(cycle_2["ep_obs"]) @@ -216,16 +216,16 @@ def test_propagates_nan_from_missing_mutant(self, base_df_for_epistasis): def test_raises_on_invalid_scale(self, base_df_for_epistasis): """Tests that an invalid `scale` argument raises a ValueError.""" with pytest.raises(ValueError, match="scale should be"): - extract_epistasis(base_df_for_epistasis, "fitness", condition_selector="condition",scale="invalid") + extract_epistasis(base_df_for_epistasis, "fitness", group_by="condition",scale="invalid") def test_keep_extra_columns(self, base_df_for_epistasis): """Tests the `keep_extra` flag.""" # keep_extra = True should preserve 'extra_col' - result_true = extract_epistasis(base_df_for_epistasis, "fitness", condition_selector="condition", keep_extra=True) + result_true = extract_epistasis(base_df_for_epistasis, "fitness", group_by="condition", keep_extra=True) assert "extra_col" in result_true.columns # keep_extra = False (default) should drop 'extra_col' - result_false = extract_epistasis(base_df_for_epistasis, "fitness",condition_selector="condition") + result_false = extract_epistasis(base_df_for_epistasis, "fitness",group_by="condition") assert "extra_col" not in result_false.columns def test_returns_empty_when_no_valid_cycles(self): diff --git a/tests/tfscreen/plot/heatmap/test_heatmaps.py b/tests/tfscreen/plot/heatmap/test_heatmaps.py index eb51256..28531ec 100644 --- a/tests/tfscreen/plot/heatmap/test_heatmaps.py +++ b/tests/tfscreen/plot/heatmap/test_heatmaps.py @@ -131,6 +131,6 @@ def test_epistasis_heatmap_duplicates(mock_heatmap): 'genotype': ['WT', 'WT'], 'value': [1.0, 1.0] }) - with pytest.raises(ValueError, match="condition_selector must be unique"): + with pytest.raises(ValueError, match="group_by must be unique"): epistasis_heatmap(df, 1, 2, 'value') From 5c935fe0773e4a1205cf723fe55277fc829afcf5 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sat, 18 Jul 2026 11:07:29 -0700 Subject: [PATCH 10/26] added script to compare theta outputs from multiple runs --- CLAUDE.md | 3 +- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- pyproject.toml | 1 + reports/flake.txt | 5 +- reports/junit/junit.xml | 2 +- src/tfscreen/analysis/compare_theta.py | 526 ++++++++++++++++++ .../analysis/scripts/compare_theta_cli.py | 105 ++++ tests/tfscreen/analysis/test_compare_theta.py | 329 +++++++++++ 9 files changed, 969 insertions(+), 6 deletions(-) create mode 100644 src/tfscreen/analysis/compare_theta.py create mode 100644 src/tfscreen/analysis/scripts/compare_theta_cli.py create mode 100644 tests/tfscreen/analysis/test_compare_theta.py diff --git a/CLAUDE.md b/CLAUDE.md index 331a3d7..d5684fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,7 @@ tfs-predict-growth # Predict growth from fitted model tfs-predict-theta # Predict operator occupancy tfs-cat-response # Fit categorical response curves tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table +tfs-compare-theta # Grade per-genotype theta stability across N estimate runs (seeds / k-fold dropouts) tfs-diagnose-nan # Diagnose NaN issues in inference tfs-simulate # Simulate a full experiment tfs-report-cfu0 # Report average ln_cfu0 by genotype class from a simulate config @@ -93,7 +94,7 @@ FASTQ files | `process_raw/` | FASTQ parsing, count normalization, ln_cfu calculation | | `simulate/` | Full experiment simulation from thermodynamics to read counts | | `simulate/growth/` | Growth/growth-transition linkage models for simulation | -| `analysis/` | Downstream statistical analysis of inference outputs (cat_response, extract_epistasis) | +| `analysis/` | Downstream statistical analysis of inference outputs (cat_response, extract_epistasis, compare_theta) | | `mle/` | General-purpose MLE regression (FitManager, least squares, WLS, NLS) | | `mle/curve_models/` | Empirical curve-fitting functions and MODEL_LIBRARY used by cat_response | | `mle/fitters/` | Low-level fitter implementations (least_squares, matrix_nls, matrix_wls) | diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 7d57d56..92cb9a7 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.16%coverage96.16% \ No newline at end of file +coverage: 96.17%coverage96.17% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 286bd98..dbd12e2 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4077tests4077 \ No newline at end of file +tests: 4108tests4108 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a64b8af..bba6529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ tfs-predict-growth = "tfscreen.tfmodel.scripts.predict_growth_cli:main" tfs-predict-theta = "tfscreen.tfmodel.scripts.predict_theta_cli:main" tfs-cat-response = "tfscreen.analysis.scripts.cat_response_cli:main" tfs-extract-epistasis = "tfscreen.analysis.scripts.extract_epistasis_cli:main" +tfs-compare-theta = "tfscreen.analysis.scripts.compare_theta_cli:main" tfs-diagnose-nan = "tfscreen.tfmodel.scripts.diagnose_nan_cli:main" tfs-simulate = "tfscreen.simulate.scripts.simulate_cli:main" tfs-report-cfu0 = "tfscreen.simulate.scripts.report_cfu0_cli:main" diff --git a/reports/flake.txt b/reports/flake.txt index b6d2a42..b819604 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -50,6 +50,7 @@ ./src/tfscreen/analysis/cat_response/cat_fit.py:211:5: E303 too many blank lines (2) ./src/tfscreen/analysis/cat_response/cat_fit.py:211:32: W292 no newline at end of file ./src/tfscreen/analysis/cat_response/cat_response.py:49:1: C901 'cat_response' is too complex (12) +./src/tfscreen/analysis/compare_theta.py:200:1: C901 'compare_theta' is too complex (12) ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/extract_epistasis.py:78:25: E231 missing whitespace after ',' @@ -7578,7 +7579,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -52 C901 'cat_fit' is too complex (19) +53 C901 'cat_fit' is too complex (19) 15 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7625,4 +7626,4 @@ 84 W292 no newline at end of file 1558 W293 blank line contains whitespace 29 W391 blank line at end of file -7580 +7581 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 967d0d7..7f5874a 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/compare_theta.py b/src/tfscreen/analysis/compare_theta.py new file mode 100644 index 0000000..8f60d05 --- /dev/null +++ b/src/tfscreen/analysis/compare_theta.py @@ -0,0 +1,526 @@ +""" +Cross-run stability grading for per-genotype theta estimates. + +Given N independent theta estimates of the same library (e.g. runs that differ +only by random seed, or k-fold dropouts of the binding training data), this +module scores every genotype on two independent axes and assigns a graded +stability tier: + + Axis 1 -- reproducibility: how much the point estimates (``q0.5``) of theta + disagree across runs, measured in native theta units. This is the + axis the tier (A/B/C/D) is graded on. + + Axis 2 -- self-consistency: whether the run-to-run disagreement is explained + by each run's own reported uncertainty (derived from the stored + quantiles). Reported as an ``overdispersion`` statistic and a flag, + *not* folded into the tier -- it distinguishes "honestly uncertain" + genotypes from the dangerous "overconfident and inconsistent" ones. + +Agreement is assessed in absolute theta units (no registration/rescaling of +runs). Two comparison modes are supported: + + * mean mode (``reference_df is None``): symmetric. The target is the cross-run + mean; every run is treated equally. Intended for same-data/different-seed + runs. + + * reference mode (``reference_df`` given): asymmetric. The reference run is the + target and the N estimate runs are measured by their pooled deviation from + it. Intended for k-fold dropout runs measured against a full-data fit. + +The public API operates on already-loaded DataFrames so it is trivially +testable; the ``tfs-compare-theta`` CLI wraps it with file IO. +""" + +import numpy as np +import pandas as pd + +# Genotype-plus-condition key columns, in priority order. ``titrant_name`` is +# only included when present in the input (see ``_detect_keys``). +_GENOTYPE_KEY = "genotype" +_NAME_KEY = "titrant_name" +_CONC_KEY = "titrant_conc" + +# Below this many estimate runs, the sample standard deviation is too noisy to +# trust, so Axis 1 spread falls back to the half-range (max - min)/2. +_SMALL_N_CUTOFF = 5 + + +def _quantile_col(q): + """Column name storing quantile ``q`` (e.g. 0.159 -> 'q0.159').""" + return f"q{q}" + + +def _detect_keys(df): + """ + Return the join-key columns for an estimate table. + + ``[genotype, titrant_name, titrant_conc]`` when ``titrant_name`` is present, + otherwise ``[genotype, titrant_conc]``. + + Parameters + ---------- + df : pandas.DataFrame + An estimate table. + + Returns + ------- + list of str + The key columns, guaranteed to include ``genotype`` and + ``titrant_conc``. + + Raises + ------ + ValueError + If ``genotype`` or ``titrant_conc`` is missing. + """ + missing = [c for c in (_GENOTYPE_KEY, _CONC_KEY) if c not in df.columns] + if missing: + raise ValueError( + f"Estimate table is missing required key column(s): {missing}. " + f"Available columns: {list(df.columns)}" + ) + + keys = [_GENOTYPE_KEY] + if _NAME_KEY in df.columns: + keys.append(_NAME_KEY) + keys.append(_CONC_KEY) + return keys + + +def _condition_grid(df, keys): + """Return the set of unique condition-key tuples (all keys except genotype).""" + cond_keys = [k for k in keys if k != _GENOTYPE_KEY] + return set(map(tuple, df.loc[:, cond_keys].drop_duplicates().to_numpy())) + + +def _extract_run(df, keys, point_quantile, sigma_quantiles): + """ + Reduce one estimate table to key columns plus ``theta`` and ``sigma``. + + ``theta`` is the ``point_quantile`` column (median by default); ``sigma`` is + the symmetric 1-sigma half-width ``(q_hi - q_lo)/2`` from ``sigma_quantiles``. + + Parameters + ---------- + df : pandas.DataFrame + An estimate table with the standard quantile columns. + keys : list of str + Key columns (from ``_detect_keys``). + point_quantile : float + Quantile used as the point estimate (e.g. 0.5). + sigma_quantiles : tuple of float + ``(lo, hi)`` quantiles bracketing one sigma (e.g. (0.159, 0.841)). + + Returns + ------- + pandas.DataFrame + Columns ``keys + ['theta', 'sigma']``. + + Raises + ------ + ValueError + If any required quantile column is absent. + """ + lo, hi = sigma_quantiles + point_col = _quantile_col(point_quantile) + lo_col = _quantile_col(lo) + hi_col = _quantile_col(hi) + + needed = [point_col, lo_col, hi_col] + absent = [c for c in needed if c not in df.columns] + if absent: + raise ValueError( + f"Estimate table is missing required quantile column(s): {absent}. " + f"Available columns: {list(df.columns)}" + ) + + out = df.loc[:, keys].copy() + out["theta"] = df[point_col].to_numpy(dtype=float) + out["sigma"] = (df[hi_col].to_numpy(dtype=float) + - df[lo_col].to_numpy(dtype=float)) / 2.0 + return out + + +def _grid_label(df, keys): + """ + Return a per-row string label for the condition (non-genotype) keys. + + Used to name the per-grid-point ``sd_*`` output columns. Includes + ``titrant_name`` when it is one of the keys. + """ + conc = df[_CONC_KEY].astype(str) + if _NAME_KEY in keys: + return "sd[" + df[_NAME_KEY].astype(str) + "," + conc + "]" + return "sd[" + conc + "]" + + +def _assign_tier(rms_sd, n_present, n_runs, min_coverage, sd_tier_edges): + """ + Map a genotype's Axis-1 score to a stability tier. + + Genotypes present in fewer than ``ceil(min_coverage * n_runs)`` runs are + labelled ``low_coverage`` (not graded). Otherwise ``rms_sd`` is binned by + ``sd_tier_edges`` into ``A`` (best) through ``D`` (worst). + + Parameters + ---------- + rms_sd : float + Root-mean-square run-to-run spread over the condition grid. + n_present : int + Number of runs in which the genotype appears. + n_runs : int + Total number of estimate runs. + min_coverage : float + Minimum fraction of runs a genotype must appear in to be graded. + sd_tier_edges : sequence of float + Three ascending cutlines separating tiers A|B, B|C, C|D. + + Returns + ------- + str + One of ``'A'``, ``'B'``, ``'C'``, ``'D'``, ``'low_coverage'``. + """ + min_runs = int(np.ceil(min_coverage * n_runs)) + if n_present < min_runs: + return "low_coverage" + + if not np.isfinite(rms_sd): + return "low_coverage" + + e0, e1, e2 = sd_tier_edges + if rms_sd < e0: + return "A" + if rms_sd < e1: + return "B" + if rms_sd < e2: + return "C" + return "D" + + +def compare_theta(estimate_dfs, + reference_df=None, + *, + min_coverage=0.5, + sd_tier_edges=(0.02, 0.05, 0.10), + overdispersion_threshold=2.0, + point_quantile=0.5, + sigma_quantiles=(0.159, 0.841)): + """ + Grade per-genotype theta stability across N independent estimate runs. + + See the module docstring for the two axes and the two comparison modes. + + Parameters + ---------- + estimate_dfs : list of pandas.DataFrame + The N estimate tables. Each must share the same key columns + (``[genotype, titrant_conc]`` or with ``titrant_name``) and the standard + quantile columns (at least ``point_quantile`` and ``sigma_quantiles``). + In mean mode at least two are required. + reference_df : pandas.DataFrame or None, optional + If given, switches to reference mode: each estimate run is measured by + its deviation from this table (excluded from the mutual comparison). + Genotypes absent from the reference are dropped with a warning. + min_coverage : float, optional + Minimum fraction of estimate runs a genotype must appear in to be + graded. Default 0.5. + sd_tier_edges : tuple of float, optional + Ascending cutlines on ``rms_sd`` for tiers A|B, B|C, C|D. Default + ``(0.02, 0.05, 0.10)``. + overdispersion_threshold : float, optional + ``overdispersion`` above this sets the ``overdispersed`` flag. Default + 2.0. Does not affect the tier. + point_quantile : float, optional + Quantile used as the point estimate. Default 0.5. + sigma_quantiles : tuple of float, optional + ``(lo, hi)`` quantiles bracketing one sigma. Default (0.159, 0.841). + + Returns + ------- + pandas.DataFrame + One row per genotype, sorted by ``rms_sd``. Columns: ``genotype``, + ``n_present``, ``mode``, ``tier``, ``rms_sd``, ``max_sd``, + ``dynamic_range``, ``overdispersion``, ``overdispersed``, + ``mean_reported_sigma``, ``spread_estimator``, and one ``sd[...]`` + column per condition-grid point. + + Raises + ------ + ValueError + If fewer than the minimum number of runs are supplied, if the key sets + disagree across runs, or if required columns are missing. + """ + if not isinstance(estimate_dfs, (list, tuple)): + raise ValueError( + "estimate_dfs must be a list of DataFrames, " + f"not {type(estimate_dfs).__name__}." + ) + + mode = "mean" if reference_df is None else "reference" + min_estimates = 2 if mode == "mean" else 1 + if len(estimate_dfs) < min_estimates: + raise ValueError( + f"{mode} mode requires at least {min_estimates} estimate " + f"table(s); got {len(estimate_dfs)}." + ) + + n_runs = len(estimate_dfs) + + # Detect keys from the first run and require every other table (including + # the reference) to agree -- a silent key mismatch would corrupt the join. + keys = _detect_keys(estimate_dfs[0]) + for i, df in enumerate(estimate_dfs[1:], start=1): + if _detect_keys(df) != keys: + raise ValueError( + f"Estimate run {i} has key columns {_detect_keys(df)}, " + f"which differ from run 0's {keys}." + ) + if reference_df is not None and _detect_keys(reference_df) != keys: + raise ValueError( + f"Reference table has key columns {_detect_keys(reference_df)}, " + f"which differ from the estimate runs' {keys}." + ) + + # The condition grid (concentrations, and titrant names if present) must + # agree across runs. Missing *genotypes* are fine -- coverage handles those + # -- but a mismatched concentration grid signals unit/config errors. + grid0 = _condition_grid(estimate_dfs[0], keys) + tables = list(estimate_dfs) + ( + [reference_df] if reference_df is not None else [] + ) + for i, df in enumerate(tables[1:], start=1): + if _condition_grid(df, keys) != grid0: + raise ValueError( + f"Table {i} has a different condition grid (titrant " + f"concentrations/names) than run 0. All runs must share the " + f"same grid." + ) + + estimator = "std" if n_runs >= _SMALL_N_CUTOFF else "half_range" + + # Stack all runs long, tagged by run index. + runs = [] + for i, df in enumerate(estimate_dfs): + run = _extract_run(df, keys, point_quantile, sigma_quantiles) + run["_run"] = i + runs.append(run) + long = pd.concat(runs, ignore_index=True) + + # Coverage is measured over the estimate runs, before any inner filtering. + n_present = long.groupby(_GENOTYPE_KEY)["_run"].nunique() + + if mode == "reference": + ref = _extract_run(reference_df, keys, point_quantile, sigma_quantiles) + ref = ref.rename(columns={"theta": "theta_ref", "sigma": "sigma_ref"}) + before = long[_GENOTYPE_KEY].nunique() + long = long.merge(ref, on=keys, how="inner") + after = long[_GENOTYPE_KEY].nunique() + if after < before: + print( + f"Warning: dropped {before - after} genotype(s) absent from " + f"the reference table.", + flush=True, + ) + long["_target"] = long["theta_ref"] + long["_dev"] = long["theta"] - long["theta_ref"] + long["_var"] = long["sigma"] ** 2 + long["sigma_ref"] ** 2 + else: + # Symmetric: the per-grid mean is the target. + long["_target"] = long.groupby(keys)["theta"].transform("mean") + long["_dev"] = long["theta"] - long["_target"] + long["_var"] = long["sigma"] ** 2 + + if long.empty: + return _empty_result(keys) + + spread_label = "rms_dev" if mode == "reference" else estimator + per_grid = _per_grid_table(long, keys, estimator) + per_geno = _aggregate_genotypes(per_grid, long, keys, spread_label) + + # Attach coverage, tier, and the overdispersion flag. + per_geno["n_present"] = per_geno[_GENOTYPE_KEY].map(n_present).astype(int) + per_geno["mode"] = mode + per_geno["tier"] = [ + _assign_tier(r, n, n_runs, min_coverage, sd_tier_edges) + for r, n in zip(per_geno["rms_sd"], per_geno["n_present"]) + ] + per_geno["overdispersed"] = per_geno["overdispersion"] > overdispersion_threshold + + # Wide per-grid sd columns. + sd_wide = _sd_wide(per_grid) + per_geno = per_geno.merge(sd_wide, on=_GENOTYPE_KEY, how="left") + + front = [ + _GENOTYPE_KEY, "n_present", "mode", "tier", + "rms_sd", "max_sd", "dynamic_range", + "overdispersion", "overdispersed", "mean_reported_sigma", + "spread_estimator", + ] + sd_cols = [c for c in per_geno.columns if c.startswith("sd[")] + per_geno = per_geno.loc[:, front + sorted(sd_cols)] + per_geno = per_geno.sort_values("rms_sd", kind="mergesort").reset_index(drop=True) + return per_geno + + +def _per_grid_table(long, keys, estimator): + """ + Collapse the long run table to one row per condition-grid point. + + Returns a frame keyed by ``keys`` with: + * ``spread`` -- Axis-1 dispersion at that grid point (std or half-range in + mean mode; RMS deviation from the reference in reference mode). + * ``mu`` -- the target theta curve value (cross-run mean or reference). + * ``grid_label`` -- the ``sd[...]`` output-column name for this point. + """ + grp = long.groupby(keys, sort=False) + + if "theta_ref" in long.columns: + # Reference mode: RMS deviation of the runs from the reference. + spread = np.sqrt(grp["_dev"].apply(lambda d: np.nanmean(np.square(d)))) + elif estimator == "std": + spread = grp["theta"].std(ddof=1) + else: + spread = (grp["theta"].max() - grp["theta"].min()) / 2.0 + + mu = grp["_target"].first() + + per_grid = pd.DataFrame({"spread": spread, "mu": mu}).reset_index() + per_grid["grid_label"] = _grid_label(per_grid, keys) + return per_grid + + +def _aggregate_genotypes(per_grid, long, keys, spread_label): + """ + Collapse per-grid statistics and per-observation chi-square to per genotype. + + Computes ``rms_sd``/``max_sd`` (Axis 1) from ``per_grid``, ``dynamic_range`` + from the target curve (per ``titrant_name`` when present, then the max + range), and ``overdispersion``/``mean_reported_sigma`` (Axis 2) from the + long table. ``spread_label`` records which Axis-1 estimator was used + (``'std'``, ``'half_range'``, or ``'rms_dev'``). + """ + # Axis 1: pooled spread over the grid. + def _rms(s): + return np.sqrt(np.nanmean(np.square(s))) + + g = per_grid.groupby(_GENOTYPE_KEY, sort=False) + rms_sd = g["spread"].apply(_rms) + max_sd = g["spread"].apply(lambda s: np.nanmax(s.to_numpy())) + per_geno = pd.DataFrame({"rms_sd": rms_sd, "max_sd": max_sd}).reset_index() + + # Dynamic range: measured within each titrant, then the widest across + # titrants, so a genotype responsive in *any* titrant is not called flat. + if _NAME_KEY in keys: + by = per_grid.groupby([_GENOTYPE_KEY, _NAME_KEY])["mu"] + rng = (by.max() - by.min()).groupby(_GENOTYPE_KEY).max() + else: + by = per_grid.groupby(_GENOTYPE_KEY)["mu"] + rng = by.max() - by.min() + per_geno["dynamic_range"] = per_geno[_GENOTYPE_KEY].map(rng) + + # Axis 2: overdispersion = chi2 / dof. + # chi2 = sum of (dev)^2 / var over valid observations. + # dof = (# valid terms) - (# grid points) in mean mode (each grid point + # spends one dof estimating its own mean); (# valid terms) in + # reference mode (the target is fixed, not estimated). + var = long["_var"].to_numpy(dtype=float) + dev = long["_dev"].to_numpy(dtype=float) + with np.errstate(divide="ignore", invalid="ignore"): + term = np.where(var > 0, np.square(dev) / var, np.nan) + chi_tbl = pd.DataFrame({ + _GENOTYPE_KEY: long[_GENOTYPE_KEY].to_numpy(), + "term": term, + "sigma": long["sigma"].to_numpy(dtype=float), + }) + chi_g = chi_tbl.groupby(_GENOTYPE_KEY, sort=False) + chi2 = chi_g["term"].sum(min_count=1) + n_terms = chi_g["term"].apply(lambda s: int(np.isfinite(s).sum())) + mean_sigma = chi_g["sigma"].mean() + + is_reference = "theta_ref" in long.columns + n_grids = long.groupby(_GENOTYPE_KEY)[keys[1:]].apply( + lambda d: len(d.drop_duplicates()) + ) + dof = n_terms if is_reference else (n_terms - n_grids.reindex(n_terms.index)) + with np.errstate(divide="ignore", invalid="ignore"): + overdispersion = chi2 / dof.where(dof > 0) + + per_geno["overdispersion"] = per_geno[_GENOTYPE_KEY].map(overdispersion) + per_geno["mean_reported_sigma"] = per_geno[_GENOTYPE_KEY].map(mean_sigma) + per_geno["spread_estimator"] = spread_label + return per_geno + + +def _sd_wide(per_grid): + """Pivot the per-grid ``spread`` values to one ``sd[...]`` column each.""" + wide = per_grid.pivot_table( + index=_GENOTYPE_KEY, + columns="grid_label", + values="spread", + aggfunc="first", + ) + wide.columns.name = None + return wide.reset_index() + + +def _empty_result(keys): + """Return an empty result frame with the standard columns.""" + cols = [ + _GENOTYPE_KEY, "n_present", "mode", "tier", + "rms_sd", "max_sd", "dynamic_range", + "overdispersion", "overdispersed", "mean_reported_sigma", + "spread_estimator", + ] + return pd.DataFrame(columns=cols) + + +def stability_crosstabs(result, + overdispersion_threshold=2.0, + flat_range_threshold=0.1): + """ + Build the two 2x2 interpretation tables from a ``compare_theta`` result. + + ``low_coverage`` genotypes are excluded (they were never graded). + + Parameters + ---------- + result : pandas.DataFrame + Output of :func:`compare_theta`. + overdispersion_threshold : float, optional + Split between "consistent" (<=) and "overconfident" (>). Default 2.0. + flat_range_threshold : float, optional + Split between "flat" (dynamic_range <) and "informative" (>=). Default + 0.1. + + Returns + ------- + dict of str -> pandas.DataFrame + ``'tier_vs_overdispersion'``: reproducible/unstable x + consistent/overconfident. ``'tier_vs_dynamic_range'``: + reproducible/unstable x informative/flat. Cells are genotype counts. + """ + graded = result.loc[result["tier"].isin(["A", "B", "C", "D"])].copy() + + reproducible = np.where( + graded["tier"].isin(["A", "B"]), "reproducible", "unstable" + ) + consistent = np.where( + graded["overdispersion"] > overdispersion_threshold, + "overconfident", "consistent", + ) + informative = np.where( + graded["dynamic_range"] >= flat_range_threshold, "informative", "flat" + ) + + tier_vs_over = pd.crosstab( + pd.Series(reproducible, name="reproducibility"), + pd.Series(consistent, name="self_consistency"), + ) + tier_vs_range = pd.crosstab( + pd.Series(reproducible, name="reproducibility"), + pd.Series(informative, name="dynamic_range"), + ) + return { + "tier_vs_overdispersion": tier_vs_over, + "tier_vs_dynamic_range": tier_vs_range, + } diff --git a/src/tfscreen/analysis/scripts/compare_theta_cli.py b/src/tfscreen/analysis/scripts/compare_theta_cli.py new file mode 100644 index 0000000..e3f48b8 --- /dev/null +++ b/src/tfscreen/analysis/scripts/compare_theta_cli.py @@ -0,0 +1,105 @@ +""" +CLI for grading per-genotype theta stability across N estimate runs. +""" + +import pandas as pd + +from tfscreen.analysis.compare_theta import ( + compare_theta as _compare_theta, + stability_crosstabs, +) +from tfscreen.util.cli import generalized_main, read_lines + + +def _format_crosstabs(crosstabs): + """Render the crosstab dict as a human-readable text block.""" + blocks = [] + for name, table in crosstabs.items(): + blocks.append(f"# {name}") + blocks.append(table.to_string()) + blocks.append("") + return "\n".join(blocks) + + +def compare_theta(estimates_file, + out_prefix="tfs_compare_theta", + reference=None, + min_coverage=0.5, + overdispersion_threshold=2.0): + """ + Grade per-genotype theta stability across N independent estimate runs. + + Reads N theta-estimate CSVs (each with the standard quantile columns: + genotype, titrant_conc [, titrant_name], q0.001 ... q0.5 ... q0.999) and + scores every genotype on two axes: reproducibility (run-to-run spread of the + q0.5 point estimate, in native theta units -- the graded tier) and + self-consistency (whether that spread is explained by each run's reported + uncertainty -- a flag). Writes a per-genotype table to {out_prefix}.csv and + the interpretation crosstabs to {out_prefix}_crosstabs.txt. + + Parameters + ---------- + estimates_file : str + Path to a text file listing the estimate CSV paths, one per line + ('#' comments allowed). In the default (mean) mode at least two are + required. + out_prefix : str, optional + Prefix for the output files. Default 'tfs_compare_theta'. + reference : str or None, optional + Path to a reference-run CSV. If given, switches to reference mode: each + estimate run is scored by its deviation from this run (e.g. k-fold + dropouts vs. a full-data fit). If omitted, runs are compared to their + symmetric cross-run mean. + min_coverage : float, optional + Minimum fraction of estimate runs a genotype must appear in to be + graded; below this it is tiered 'low_coverage'. Default 0.5. + overdispersion_threshold : float, optional + Overdispersion above this sets the 'overdispersed' flag and the + 'overconfident' crosstab column. Default 2.0. + """ + paths = read_lines(estimates_file) + if not paths: + raise ValueError(f"No estimate paths found in '{estimates_file}'.") + + print(f"Reading {len(paths)} estimate file(s)...", flush=True) + estimate_dfs = [pd.read_csv(p) for p in paths] + + reference_df = None + if reference is not None: + print(f"Reading reference {reference}...", flush=True) + reference_df = pd.read_csv(reference) + + result = _compare_theta( + estimate_dfs, + reference_df=reference_df, + min_coverage=min_coverage, + overdispersion_threshold=overdispersion_threshold, + ) + + out_file = f"{out_prefix}.csv" + result.to_csv(out_file, index=False) + print(f"Wrote {len(result)} genotype rows to {out_file}", flush=True) + + # Tier breakdown to stdout. + counts = result["tier"].value_counts() + print("Tier breakdown:", flush=True) + for tier in ["A", "B", "C", "D", "low_coverage"]: + if tier in counts.index: + print(f" {tier:>12}: {counts[tier]}", flush=True) + + crosstabs = stability_crosstabs( + result, overdispersion_threshold=overdispersion_threshold + ) + crosstab_file = f"{out_prefix}_crosstabs.txt" + with open(crosstab_file, "w") as fh: + fh.write(_format_crosstabs(crosstabs)) + print(f"Wrote interpretation crosstabs to {crosstab_file}", flush=True) + + +def main(): + generalized_main(compare_theta, + manual_arg_types={"reference": str}) + + +if __name__ == "__main__": + main() diff --git a/tests/tfscreen/analysis/test_compare_theta.py b/tests/tfscreen/analysis/test_compare_theta.py new file mode 100644 index 0000000..17096c3 --- /dev/null +++ b/tests/tfscreen/analysis/test_compare_theta.py @@ -0,0 +1,329 @@ +""" +Unit tests for tfscreen.analysis.compare_theta. +""" + +import numpy as np +import pandas as pd +import pytest + +from tfscreen.analysis.compare_theta import ( + compare_theta, + stability_crosstabs, + _detect_keys, + _extract_run, + _assign_tier, +) + + +# --- Fixtures / builders ----------------------------------------------------- + +def make_estimate(theta_by_geno, concs=(0.0, 1.0), sigma=0.05, + titrant_name=None): + """ + Build one synthetic estimate table with the standard quantile schema. + + Parameters + ---------- + theta_by_geno : dict[str, sequence] + genotype -> theta (q0.5) values, one per concentration. + concs : sequence + Titrant concentrations (len must match each theta sequence). + sigma : float or dict[str, sequence] + 1-sigma half-width; scalar (shared) or per-genotype per-conc. + titrant_name : str or None + If given, a ``titrant_name`` column is added with this constant value. + + Returns + ------- + pandas.DataFrame + Columns: genotype, [titrant_name,] titrant_conc, q0.159, q0.5, q0.841. + """ + rows = [] + for geno, thetas in theta_by_geno.items(): + for j, conc in enumerate(concs): + th = thetas[j] + if isinstance(sigma, dict): + sg = sigma[geno][j] + else: + sg = sigma + row = {"genotype": geno, "titrant_conc": conc, + "q0.159": th - sg, "q0.5": th, "q0.841": th + sg} + if titrant_name is not None: + row["titrant_name"] = titrant_name + rows.append(row) + df = pd.DataFrame(rows) + cols = ["genotype"] + if titrant_name is not None: + cols.append("titrant_name") + cols += ["titrant_conc", "q0.159", "q0.5", "q0.841"] + return df.loc[:, cols] + + +def get_row(result, genotype): + """Return the single result row for a genotype as a Series.""" + sub = result.loc[result["genotype"] == genotype] + assert len(sub) == 1, f"expected 1 row for {genotype}, got {len(sub)}" + return sub.iloc[0] + + +# --- Key detection ----------------------------------------------------------- + +def test_detect_keys_without_name(): + df = make_estimate({"wt": [0.1, 0.2]}) + assert _detect_keys(df) == ["genotype", "titrant_conc"] + + +def test_detect_keys_with_name(): + df = make_estimate({"wt": [0.1, 0.2]}, titrant_name="iptg") + assert _detect_keys(df) == ["genotype", "titrant_name", "titrant_conc"] + + +def test_detect_keys_missing_raises(): + df = pd.DataFrame({"genotype": ["wt"], "q0.5": [0.1]}) + with pytest.raises(ValueError, match="titrant_conc"): + _detect_keys(df) + + +# --- sigma extraction -------------------------------------------------------- + +def test_extract_run_sigma(): + df = make_estimate({"wt": [0.3, 0.7]}, sigma=0.05) + keys = _detect_keys(df) + out = _extract_run(df, keys, 0.5, (0.159, 0.841)) + np.testing.assert_allclose(out["theta"].to_numpy(), [0.3, 0.7]) + np.testing.assert_allclose(out["sigma"].to_numpy(), [0.05, 0.05]) + + +def test_extract_run_missing_quantile_raises(): + df = make_estimate({"wt": [0.3, 0.7]}).drop(columns=["q0.841"]) + keys = _detect_keys(df) + with pytest.raises(ValueError, match="q0.841"): + _extract_run(df, keys, 0.5, (0.159, 0.841)) + + +# --- _assign_tier ------------------------------------------------------------ + +@pytest.mark.parametrize("rms,expected", [ + (0.0, "A"), + (0.019, "A"), + (0.02, "B"), + (0.049, "B"), + (0.05, "C"), + (0.099, "C"), + (0.10, "D"), + (0.5, "D"), +]) +def test_assign_tier_edges(rms, expected): + tier = _assign_tier(rms, n_present=4, n_runs=4, min_coverage=0.5, + sd_tier_edges=(0.02, 0.05, 0.10)) + assert tier == expected + + +def test_assign_tier_low_coverage(): + # present in 1 of 4 runs, min_coverage 0.5 -> needs >=2 + tier = _assign_tier(0.0, n_present=1, n_runs=4, min_coverage=0.5, + sd_tier_edges=(0.02, 0.05, 0.10)) + assert tier == "low_coverage" + + +# --- Axis 1: reproducibility, mean mode -------------------------------------- + +def test_mean_mode_identical_runs(): + df = make_estimate({"wt": [0.2, 0.8], "m1": [0.1, 0.9]}) + result = compare_theta([df, df.copy(), df.copy()]) + assert (result["rms_sd"] == 0).all() + assert (result["max_sd"] == 0).all() + assert (result["tier"] == "A").all() + assert (result["mode"] == "mean").all() + + +def test_mean_mode_known_offset_halfrange(): + # N=2 (<5) => estimator is half_range = (max-min)/2. + d = 0.2 + r1 = make_estimate({"g": [0.5, 0.5]}) + r2 = make_estimate({"g": [0.5, 0.5 + d]}) + result = compare_theta([r1, r2]) + row = get_row(result, "g") + assert row["spread_estimator"] == "half_range" + # conc0 spread 0, conc1 spread d/2 + np.testing.assert_allclose(row["max_sd"], d / 2) + np.testing.assert_allclose(row["rms_sd"], np.sqrt((0 + (d / 2) ** 2) / 2)) + + +def test_mean_mode_std_estimator_for_large_n(): + # >=5 runs => sample std (ddof=1). + thetas = [0.5, 0.5, 0.5, 0.5, 0.6] # at a single conc across 5 runs + runs = [make_estimate({"g": [t]}, concs=(0.0,)) for t in thetas] + result = compare_theta(runs) + row = get_row(result, "g") + assert row["spread_estimator"] == "std" + np.testing.assert_allclose(row["rms_sd"], np.std(thetas, ddof=1)) + + +def test_flat_curve_is_tier_a_and_zero_range(): + # Genotype pinned flat across all concs and runs: perfectly stable, no + # dynamic range. Must land in tier A (the whole point of absolute spread). + flat = make_estimate({"flat": [1e-4, 1e-4, 1e-4]}, concs=(0.0, 1.0, 2.0)) + result = compare_theta([flat, flat.copy(), flat.copy()]) + row = get_row(result, "flat") + assert row["tier"] == "A" + np.testing.assert_allclose(row["dynamic_range"], 0.0) + + +# --- Axis 2: self-consistency / overdispersion ------------------------------- + +def test_axis2_wide_sigma_not_flagged(): + d = 0.2 + r1 = make_estimate({"g": [0.5, 0.5]}, sigma=0.5) + r2 = make_estimate({"g": [0.5, 0.5 + d]}, sigma=0.5) + result = compare_theta([r1, r2], overdispersion_threshold=2.0) + row = get_row(result, "g") + # chi2 = d^2/(2 sigma^2), dof = 4 terms - 2 grids = 2 + expected = (d ** 2 / (2 * 0.5 ** 2)) / 2 + np.testing.assert_allclose(row["overdispersion"], expected) + assert not row["overdispersed"] + + +def test_axis2_tight_sigma_flagged(): + d = 0.2 + r1 = make_estimate({"g": [0.5, 0.5]}, sigma=0.01) + r2 = make_estimate({"g": [0.5, 0.5 + d]}, sigma=0.01) + result = compare_theta([r1, r2], overdispersion_threshold=2.0) + row = get_row(result, "g") + expected = (d ** 2 / (2 * 0.01 ** 2)) / 2 + np.testing.assert_allclose(row["overdispersion"], expected) + assert row["overdispersed"] + + +def test_axis2_zero_sigma_skipped(): + # A zero-width interval must not blow up axis 2 (division by zero). + r1 = make_estimate({"g": [0.5, 0.5]}, sigma=0.0) + r2 = make_estimate({"g": [0.5, 0.6]}, sigma=0.0) + result = compare_theta([r1, r2]) + row = get_row(result, "g") + # All chi2 terms invalid -> overdispersion is NaN, not inf, and not flagged. + assert np.isnan(row["overdispersion"]) + assert not row["overdispersed"] + + +# --- Reference mode ---------------------------------------------------------- + +def test_reference_mode_pooled_deviation(): + ref = make_estimate({"g": [0.5, 0.5]}, sigma=0.1) + # two estimate runs deviating from reference by +a and -b at conc1 + a, b = 0.1, 0.3 + e1 = make_estimate({"g": [0.5, 0.5 + a]}, sigma=0.1) + e2 = make_estimate({"g": [0.5, 0.5 - b]}, sigma=0.1) + result = compare_theta([e1, e2], reference_df=ref) + row = get_row(result, "g") + assert row["mode"] == "reference" + assert row["spread_estimator"] == "rms_dev" + + # per-grid spread: conc0 -> 0; conc1 -> sqrt(mean(a^2, b^2)) + conc1_spread = np.sqrt((a ** 2 + b ** 2) / 2) + np.testing.assert_allclose(row["max_sd"], conc1_spread) + # rms_sd pooled over all (grid, run) deviations = sqrt(mean(0,0,a^2,b^2)) + np.testing.assert_allclose( + row["rms_sd"], np.sqrt((a ** 2 + b ** 2) / 4) + ) + + +def test_reference_mode_overdispersion_includes_ref_sigma(): + ref = make_estimate({"g": [0.5]}, concs=(0.0,), sigma=0.1) + a = 0.2 + e1 = make_estimate({"g": [0.5 + a]}, concs=(0.0,), sigma=0.1) + result = compare_theta([e1], reference_df=ref) + row = get_row(result, "g") + # single term: dev^2 / (sigma^2 + sigma_ref^2); dof = 1 term (fixed target) + expected = a ** 2 / (0.1 ** 2 + 0.1 ** 2) + np.testing.assert_allclose(row["overdispersion"], expected) + + +def test_reference_mode_drops_genotype_absent_from_reference(capsys): + ref = make_estimate({"g": [0.5, 0.5]}) + e1 = make_estimate({"g": [0.5, 0.6], "extra": [0.1, 0.2]}) + result = compare_theta([e1, e1.copy()], reference_df=ref) + assert "extra" not in set(result["genotype"]) + assert "g" in set(result["genotype"]) + out = capsys.readouterr().out + assert "absent from the reference" in out + + +# --- Coverage ---------------------------------------------------------------- + +def test_low_coverage_tier_and_n_present(): + base = {"a": [0.1, 0.2], "b": [0.3, 0.4]} + r1 = make_estimate(base) + r2 = make_estimate(base) + r3 = make_estimate(base) + # 'c' appears in only 1 of 4 runs + r4 = make_estimate({"a": [0.1, 0.2], "b": [0.3, 0.4], "c": [0.9, 0.9]}) + result = compare_theta([r1, r2, r3, r4], min_coverage=0.5) + c = get_row(result, "c") + assert c["n_present"] == 1 + assert c["tier"] == "low_coverage" + a = get_row(result, "a") + assert a["n_present"] == 4 + + +# --- Grid / key validation --------------------------------------------------- + +def test_grid_mismatch_raises(): + r1 = make_estimate({"g": [0.1, 0.2]}, concs=(0.0, 1.0)) + r2 = make_estimate({"g": [0.1, 0.2]}, concs=(0.0, 2.0)) + with pytest.raises(ValueError, match="condition grid"): + compare_theta([r1, r2]) + + +def test_key_mismatch_raises(): + r1 = make_estimate({"g": [0.1, 0.2]}, titrant_name="iptg") + r2 = make_estimate({"g": [0.1, 0.2]}) # no titrant_name + with pytest.raises(ValueError, match="key columns"): + compare_theta([r1, r2]) + + +def test_mean_mode_requires_two_runs(): + r1 = make_estimate({"g": [0.1, 0.2]}) + with pytest.raises(ValueError, match="at least 2"): + compare_theta([r1]) + + +# --- titrant_name handling --------------------------------------------------- + +def test_titrant_name_dynamic_range_is_per_name_max(): + # Genotype is flat in 'iptg' but responsive in 'atc'; dynamic_range should + # reflect the responsive titrant, not be washed out by pooling. + iptg = make_estimate({"g": [0.5, 0.5]}, titrant_name="iptg") + atc = make_estimate({"g": [0.1, 0.9]}, titrant_name="atc") + r1 = pd.concat([iptg, atc], ignore_index=True) + result = compare_theta([r1, r1.copy()]) + row = get_row(result, "g") + np.testing.assert_allclose(row["dynamic_range"], 0.8) + # per-grid sd columns exist for both titrants + sd_cols = [c for c in result.columns if c.startswith("sd[")] + assert any("iptg" in c for c in sd_cols) + assert any("atc" in c for c in sd_cols) + + +# --- Crosstabs --------------------------------------------------------------- + +def test_stability_crosstabs_populates_cells(): + # Construct a mix: a stable+consistent, an unstable+overconfident. + stable = make_estimate({"s": [0.1, 0.9]}, sigma=0.1) + unstable_a = make_estimate({"u": [0.5, 0.5]}, sigma=0.001) + unstable_b = make_estimate({"u": [0.5, 0.9]}, sigma=0.001) + r1 = pd.concat([stable, unstable_a], ignore_index=True) + r2 = pd.concat([stable.copy(), unstable_b], ignore_index=True) + result = compare_theta([r1, r2]) + tabs = stability_crosstabs(result) + assert "tier_vs_overdispersion" in tabs + assert "tier_vs_dynamic_range" in tabs + total = tabs["tier_vs_overdispersion"].to_numpy().sum() + assert total == 2 # both genotypes graded + + +def test_result_sorted_by_rms_sd(): + r1 = make_estimate({"a": [0.5, 0.5], "b": [0.5, 0.5]}) + r2 = make_estimate({"a": [0.5, 0.55], "b": [0.5, 0.9]}) + result = compare_theta([r1, r2]) + assert list(result["rms_sd"]) == sorted(result["rms_sd"]) From 53312da975b0dcbf52901806239550c6ff1dee18 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sat, 18 Jul 2026 17:40:50 -0700 Subject: [PATCH 11/26] added interpretative layer to cat-response --- CLAUDE.md | 11 + docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- reports/flake.txt | 291 ++++-------- reports/junit/junit.xml | 2 +- .../analysis/cat_response/cat_assess.py | 227 ++++++++++ src/tfscreen/analysis/cat_response/cat_fit.py | 250 +++++++---- .../analysis/cat_response/cat_response.py | 181 ++++++-- src/tfscreen/analysis/compare_theta.py | 210 +++++++++ .../analysis/scripts/cat_response_cli.py | 56 ++- .../analysis/scripts/compare_theta_cli.py | 19 +- src/tfscreen/mle/curve_models/guesses.py | 11 +- src/tfscreen/mle/predict_with_error.py | 27 +- .../analysis/cat_response/test_cat_assess.py | 153 +++++++ .../analysis/cat_response/test_cat_fit.py | 414 ++++++++---------- .../cat_response/test_cat_response.py | 148 ++++++- .../analysis/scripts/test_cat_response_cli.py | 26 +- tests/tfscreen/analysis/test_compare_theta.py | 143 ++++++ .../tfscreen/mle/curve_models/test_guesses.py | 54 ++- tests/tfscreen/mle/test_predict_with_error.py | 42 +- 20 files changed, 1667 insertions(+), 602 deletions(-) create mode 100644 src/tfscreen/analysis/cat_response/cat_assess.py create mode 100644 tests/tfscreen/analysis/cat_response/test_cat_assess.py diff --git a/CLAUDE.md b/CLAUDE.md index d5684fa..67246be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -278,6 +278,17 @@ An alternative to prior-predictive phenotype sampling: instead of drawing theta Integration: `library_prediction` gains a `phenotype_source: empirical` branch (needs an `empirical: {phenotype_model: }` block pointing at the single self-contained `_phenotype_model.json`; `_resolve_phenotype_model_path` accepts that path with/without `.json` or the bare ``, and the fit prints the absolute path — use it so no file-copying is needed) that resamples all genotypes, injects the overrides, forces `theta_component=hill_geno` (warns + ignores any other value; the resampled phenotypes are per-genotype Hill curves and the discarded prior draw must match the `parameters_df` schema — this also avoids running/overflowing an e.g. `hill_mut` draw), drops the ignored `theta_priors`/`theta_sim_priors`, and forces `activity=fixed/1`. `phenotype_source`/`empirical` are in `selection_experiment.SIMULATE_KNOWN_KEYS`. Resampled ground truth flows into `parameters_df`/`genotype_theta_df` automatically (no new return value); `library_binding` regenerates for free (reads `parameters_df`). `tfs-build-empirical` (`simulate/scripts/build_empirical_cli.py`) is a **one-command orchestrator**: given the experimental inputs (`growth_file` and `seed` positional; `--binding_file` required; optional `--spiked_file`/`--base_growth_file`/`--thermo_data`/`--congression_lambda`/`--num_workers`) it internally calls `configure_model` (linear + hill_geno defaults — no model choices exposed, since here they'd only be wrong) then `run_prefit_calibration` (MAP-calibrates per-condition k/m), then Stages 1-2 (plus optional Stage 1.5 congression de-attenuation when `--congression_lambda` is given), saving the deliverable `_phenotype_model.json` (one self-contained, human-readable file = the generating distribution; `PopulationModel.save`/`.load`) + the diagnostic `_stage1_fits.csv`, plus the `_configure_*`/`_prefit_*` intermediates. The configure/prefit imports are lazy (the heavy JAX stack loads only on this path). The MAP prefit is the slow step, so `--calibration_file` skips configure+prefit and reuses a calibration for fast Stage-1/2 iteration — either a prefit **priors CSV** (read via `fit_phenotypes.read_calibration`, which pivots the `growth.condition_growth.k_loc`/`growth.condition_growth.m_loc` per-`condition_rep` rows — prefit writes these via `_csv_row_name` = `growth.{component}.{field}` — to wide k/m) or a wide `(condition_rep, growth_k, growth_m)` CSV. This **complements** the fully-synthetic prior path (the *accuracy* benchmark) as a *realism* benchmark. +## Categorical response assessment (`analysis/cat_response/`) + +`tfs-cat-response` fits a family of empirical shapes (`MODEL_LIBRARY`) to each group's `y_obs`-vs-`x_obs` curve and answers two **orthogonal** questions. Do not conflate them: + +- **Shape** (which model): AICc weight selection in `cat_fit.py`. Uses the small-sample-corrected AICc, not AIC — with ~8 points and up to 4 params the correction `2k(k+1)/(n-k-1)` is large and flips selections; when `n-k-1 <= 0` the model gets `aicc=inf` (can't win, but its params are still reported). AIC/R² are computed on the **weighted** residuals (`chi2 = sum(((y-yfit)/y_std)**2)`; `aic = 2k + chi2`) — the fit was always weighted, so selection is too now. +- **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading the *best* model against zero. Per-point `sig_nonzero` z-test at the observed x; a per-curve **omnibus** Wald/χ² `W = yhat @ pinv(Σ_yhat) @ yhat` (df = rank(Σ) = n_params) using the **full** prediction covariance (`predict_with_error(..., full_cov=True)` = `J·Cov·Jᵀ`), which rewards both magnitude and how-many-points and handles the strong correlation among predicted points; then **Benjamini-Hochberg** across curves → `omnibus_q`. + +`equiv_zero` (a point's whole CI ⊂ `[-delta, delta]`) is the confidence axis that separates "confidently flat" from "too noisy to tell". `delta` defaults to `delta_c * median(predicted y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits — hence the post-hoc pass); pass `--delta` for a fixed biological region. `response_class` is 3-valued with **equivalence taking precedence over significance** (a tiny-but-significant offset inside the ROPE is still practically zero): `confident_zero` (all points equiv) → else `real` (omnibus_q < alpha) → else `indeterminate` (the "flat because error is high" bucket to exclude downstream). + +Outputs: rollups (`omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; per-point flags in `{prefix}_assessment.csv`; `{prefix}_predictions.csv` holds only each group's **best** model (`best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. + ## YAML Standards All YAML files in this codebase follow these conventions. Apply them when creating or modifying any YAML file. diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 92cb9a7..f23c57b 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.17%coverage96.17% \ No newline at end of file +coverage: 96.14%coverage96.14% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index dbd12e2..3fc6039 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4108tests4108 \ No newline at end of file +tests: 4152tests4152 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index b819604..482501a 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -6,51 +6,15 @@ ./src/tfscreen/analysis/__init__.py:13:1: W391 blank line at end of file ./src/tfscreen/analysis/cat_response/__init__.py:2:77: W291 trailing whitespace ./src/tfscreen/analysis/cat_response/__init__.py:3:55: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:14:1: C901 'cat_fit' is too complex (19) -./src/tfscreen/analysis/cat_response/cat_fit.py:14:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/analysis/cat_response/cat_fit.py:25:80: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:26:33: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:42:33: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:51:51: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:87:49: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:88:40: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:89:36: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:89:51: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:90:36: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:91:40: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:92:48: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:94:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:108:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:110:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:115:48: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:117:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:124:77: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:125:47: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:127:72: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:127:74: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:128:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:130:13: E303 too many blank lines (2) -./src/tfscreen/analysis/cat_response/cat_fit.py:148:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:150:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:155:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:172:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:174:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:180:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:189:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./src/tfscreen/analysis/cat_response/cat_fit.py:189:62: E701 multiple statements on one line (colon) -./src/tfscreen/analysis/cat_response/cat_fit.py:190:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' -./src/tfscreen/analysis/cat_response/cat_fit.py:190:65: E701 multiple statements on one line (colon) -./src/tfscreen/analysis/cat_response/cat_fit.py:191:9: E701 multiple statements on one line (colon) -./src/tfscreen/analysis/cat_response/cat_fit.py:192:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:204:36: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:205:32: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:206:32: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:207:36: E231 missing whitespace after ':' -./src/tfscreen/analysis/cat_response/cat_fit.py:209:1: W293 blank line contains whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:211:5: E303 too many blank lines (2) -./src/tfscreen/analysis/cat_response/cat_fit.py:211:32: W292 no newline at end of file -./src/tfscreen/analysis/cat_response/cat_response.py:49:1: C901 'cat_response' is too complex (12) +./src/tfscreen/analysis/cat_response/cat_fit.py:29:1: C901 'cat_fit' is too complex (25) +./src/tfscreen/analysis/cat_response/cat_fit.py:154:72: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:154:74: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:226:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./src/tfscreen/analysis/cat_response/cat_fit.py:228:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./src/tfscreen/analysis/cat_response/cat_response.py:60:1: C901 'cat_response' is too complex (15) +./src/tfscreen/analysis/cat_response/cat_response.py:246:45: E127 continuation line over-indented for visual indent ./src/tfscreen/analysis/compare_theta.py:200:1: C901 'compare_theta' is too complex (12) +./src/tfscreen/analysis/compare_theta.py:632:1: C901 'aggregate_theta' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/extract_epistasis.py:78:25: E231 missing whitespace after ',' @@ -378,56 +342,55 @@ ./src/tfscreen/mle/curve_models/guesses.py:25:20: E231 missing whitespace after ',' ./src/tfscreen/mle/curve_models/guesses.py:25:25: E231 missing whitespace after ',' ./src/tfscreen/mle/curve_models/guesses.py:28:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:42:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:42:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:45:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:65:23: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:69:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:83:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:89:23: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:109:37: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:110:24: E261 at least two spaces before inline comment -./src/tfscreen/mle/curve_models/guesses.py:114:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:128:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:129:35: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:130:24: E261 at least two spaces before inline comment -./src/tfscreen/mle/curve_models/guesses.py:134:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:150:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:155:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:165:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:181:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:186:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:196:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:212:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:215:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:217:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:222:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:225:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:243:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:247:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:250:26: E225 missing whitespace around operator -./src/tfscreen/mle/curve_models/guesses.py:252:10: E261 at least two spaces before inline comment -./src/tfscreen/mle/curve_models/guesses.py:257:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:257:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:260:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:271:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:271:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:274:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:274:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:277:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:288:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:288:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:291:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:291:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:294:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:305:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:305:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:308:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:308:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:311:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:322:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:322:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:324:1: W391 blank line at end of file +./src/tfscreen/mle/curve_models/guesses.py:51:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:54:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:74:23: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:78:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:92:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:98:23: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:118:37: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:119:24: E261 at least two spaces before inline comment +./src/tfscreen/mle/curve_models/guesses.py:123:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:137:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:138:35: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:139:24: E261 at least two spaces before inline comment +./src/tfscreen/mle/curve_models/guesses.py:143:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:159:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:164:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:174:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:190:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:195:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:205:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:221:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:224:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:226:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:231:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:234:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:252:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:256:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:259:26: E225 missing whitespace around operator +./src/tfscreen/mle/curve_models/guesses.py:261:10: E261 at least two spaces before inline comment +./src/tfscreen/mle/curve_models/guesses.py:266:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:266:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:269:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:280:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:280:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:283:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:283:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:286:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:297:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:297:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:300:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:300:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:303:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:314:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:314:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:317:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:317:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:320:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:331:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:331:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:333:1: W391 blank line at end of file ./src/tfscreen/mle/curve_models/models.py:13:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/mle/curve_models/models.py:38:1: W293 blank line contains whitespace ./src/tfscreen/mle/curve_models/models.py:39:26: E231 missing whitespace after ',' @@ -540,12 +503,10 @@ ./src/tfscreen/mle/fitters/matrix_wls.py:54:1: W293 blank line contains whitespace ./src/tfscreen/mle/fitters/matrix_wls.py:55:52: W292 no newline at end of file ./src/tfscreen/mle/predict_with_error.py:3:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/predict_with_error.py:37:1: W293 blank line contains whitespace -./src/tfscreen/mle/predict_with_error.py:42:36: E231 missing whitespace after ',' -./src/tfscreen/mle/predict_with_error.py:54:1: W293 blank line contains whitespace -./src/tfscreen/mle/predict_with_error.py:57:43: E231 missing whitespace after ',' -./src/tfscreen/mle/predict_with_error.py:61:45: E231 missing whitespace after ',' -./src/tfscreen/mle/predict_with_error.py:74:32: W292 no newline at end of file +./src/tfscreen/mle/predict_with_error.py:50:36: E231 missing whitespace after ',' +./src/tfscreen/mle/predict_with_error.py:68:43: E231 missing whitespace after ',' +./src/tfscreen/mle/predict_with_error.py:72:45: E231 missing whitespace after ',' +./src/tfscreen/mle/predict_with_error.py:89:32: W292 no newline at end of file ./src/tfscreen/plot/__init__.py:36:63: W291 trailing whitespace ./src/tfscreen/plot/cat_fits.py:13:1: C901 'cat_fits' is too complex (11) ./src/tfscreen/plot/cat_fits.py:13:1: E302 expected 2 blank lines, found 1 @@ -4037,72 +3998,8 @@ ./tests/smoke-tests/test_run_growth_analysis_smoke.py:37:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:45:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:48:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:4:1: F401 'pandas as pd' imported but unused -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:10:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:17:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:31:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:34:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:38:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:42:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:44:29: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:47:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:54:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:57:56: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:67:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:72:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:75:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:82:47: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:83:55: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:84:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:88:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:95:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:98:68: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:99:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:106:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:108:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:109:29: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:111:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:116:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:121:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:129:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:134:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:136:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:138:47: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:141:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:148:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:152:49: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:153:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:155:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:171:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:175:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:177:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:181:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:184:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:187:81: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:190:32: W291 trailing whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:201:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:203:40: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:204:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:213:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:215:18: E111 indentation is not a multiple of 4 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:215:18: E117 over-indented -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:216:18: E111 indentation is not a multiple of 4 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:217:22: E111 indentation is not a multiple of 4 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:218:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:219:22: E111 indentation is not a multiple of 4 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:220:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:221:22: E111 indentation is not a multiple of 4 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:223:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:231:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:233:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:236:58: E231 missing whitespace after ',' -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:236:65: E261 at least two spaces before inline comment -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:241:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:243:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:246:1: W293 blank line contains whitespace -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:250:1: W293 blank line contains whitespace +./tests/tfscreen/analysis/cat_response/test_cat_assess.py:7:1: F401 'scipy.stats.norm' imported but unused +./tests/tfscreen/analysis/cat_response/test_cat_fit.py:7:1: F401 'pytest' imported but unused ./tests/tfscreen/analysis/test_extract_epistasis.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:22:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:34:35: W291 trailing whitespace @@ -4409,23 +4306,22 @@ ./tests/tfscreen/genetics/test_library_manager.py:815:35: E231 missing whitespace after ':' ./tests/tfscreen/genetics/test_library_manager.py:815:49: W291 trailing whitespace ./tests/tfscreen/genetics/test_library_manager.py:840:34: W292 no newline at end of file -./tests/tfscreen/mle/curve_models/test_guesses.py:7:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_guesses.py:18:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:21:44: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:23:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_guesses.py:26:32: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:33:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:41:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:46:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:51:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:57:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_guesses.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:65:45: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:66:45: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:67:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:72:45: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:73:46: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:79:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:9:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_guesses.py:22:44: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:73:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_guesses.py:76:32: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:83:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:91:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:96:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:101:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:107:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_guesses.py:109:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:115:45: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:116:45: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:117:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:122:45: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:123:46: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:129:1: W293 blank line contains whitespace ./tests/tfscreen/mle/curve_models/test_models.py:2:1: F401 'pytest' imported but unused ./tests/tfscreen/mle/curve_models/test_models.py:15:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/curve_models/test_models.py:20:1: E302 expected 2 blank lines, found 1 @@ -4607,7 +4503,6 @@ ./tests/tfscreen/mle/test_predict_with_error.py:57:1: W293 blank line contains whitespace ./tests/tfscreen/mle/test_predict_with_error.py:59:1: W293 blank line contains whitespace ./tests/tfscreen/mle/test_predict_with_error.py:64:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/test_predict_with_error.py:67:61: W291 trailing whitespace ./tests/tfscreen/plot/heatmap/test_heatmap_core.py:25:1: F401 'tfscreen.plot.default_styles.DEFAULT_HMAP_PATCH_KWARGS' imported but unused ./tests/tfscreen/plot/heatmap/test_heatmap_core.py:31:1: F401 'tfscreen.plot.helper.get_ax_limits' imported but unused ./tests/tfscreen/plot/heatmap/test_heatmap_core.py:31:47: W291 trailing whitespace @@ -7579,15 +7474,15 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -53 C901 'cat_fit' is too complex (19) -15 E111 indentation is not a multiple of 4 +54 C901 'cat_fit' is too complex (25) +10 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) -11 E117 over-indented +10 E117 over-indented 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -213 E127 continuation line over-indented for visual indent +214 E127 continuation line over-indented for visual indent 92 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 43 E201 whitespace after '[' @@ -7597,33 +7492,33 @@ 1839 E221 multiple spaces before operator 26 E222 multiple spaces after operator 8 E225 missing whitespace around operator -1100 E231 missing whitespace after ',' +1087 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals -211 E261 at least two spaces before inline comment +207 E261 at least two spaces before inline comment 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment 149 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -773 E302 expected 2 blank lines, found 1 -50 E303 too many blank lines (2) +763 E302 expected 2 blank lines, found 1 +48 E303 too many blank lines (2) 19 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line 38 E402 module level import not at top of file 87 E501 line too long (153 > 127 characters) -46 E701 multiple statements on one line (colon) +43 E701 multiple statements on one line (colon) 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' 2 E713 test for membership should be 'not in' 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 21 E741 ambiguous variable name 'l' -230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +231 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -608 W291 trailing whitespace -84 W292 no newline at end of file -1558 W293 blank line contains whitespace +592 W291 trailing whitespace +83 W292 no newline at end of file +1505 W293 blank line contains whitespace 29 W391 blank line at end of file -7581 +7476 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 7f5874a..6480747 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py new file mode 100644 index 0000000..cbbe406 --- /dev/null +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -0,0 +1,227 @@ +""" +Post-hoc assessment of fitted categorical-response curves. + +Where ``cat_fit`` answers "which *shape* best explains this curve?" (AICc model +selection), this module answers the orthogonal magnitude question: "is the curve +distinguishable from zero, and where — and when it looks flat, do we *know* it is +flat or is the error just too large to tell?" + +Two per-curve summaries drive downstream filtering: + +- **omnibus test** (``assess_best_model``): a single Wald / Mahalanobis + statistic ``W = yhat @ pinv(Sigma) @ yhat`` on the best model's predicted + values at the observed x, using the full prediction covariance. It rewards + both the magnitude of the deviation and how many points deviate, and it + accounts for the strong correlation among predicted points (they share a + handful of fit parameters). ``W ~ chi2`` with ``df = rank(Sigma)`` (the number + of free parameters). This is the "distinguishable from zero" verdict; combine + across curves with :func:`benjamini_hochberg`. + +- **equivalence test** (:func:`classify_equiv`): a point is ``equiv_zero`` when + its whole confidence interval falls inside a region of practical equivalence + ``[-delta, delta]`` centered on zero. This is what separates "confidently + flat" from "too noisy to tell" — a plain significance test cannot. ``delta`` + defaults to a multiple of the median predicted standard error + (:func:`compute_delta`), i.e. a *detectability* threshold; pass an explicit + ``delta`` for a fixed biologically-meaningful region instead. +""" + +import numpy as np +from scipy.stats import chi2, norm + +from tfscreen.mle import predict_with_error + + +def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): + """ + Evaluate the best-fit model at the observed x and test it against zero. + + Parameters + ---------- + model_func : callable + The fitted model, signature ``model_func(params, x)``. + params : np.ndarray + Best-fit parameters. + cov_matrix : np.ndarray + Covariance matrix of the fitted parameters. May contain NaN (e.g. when + the fit failed); in that case per-point errors and the omnibus test are + returned as NaN. + x : np.ndarray + The observed independent-variable values (typically the ~8 titrant + concentrations). Predictions and per-point tests are evaluated here. + alpha : float, optional + Two-sided significance level for the per-point ``sig_nonzero`` flag. + Default 0.05. + + Returns + ------- + per_point : dict + Arrays of length ``len(x)``: ``x``, ``y_est``, ``y_std``, ``z``, + ``sig_nonzero`` (bool), ``direction`` (-1/0/+1). + rollup : dict + Scalars: ``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``n_nonzero``, + ``any_nonzero``. + """ + x = np.asarray(x, dtype=float) + + y_est, y_std, y_cov = predict_with_error( + model_func, params, cov_matrix, args=[x], full_cov=True + ) + y_est = np.asarray(y_est, dtype=float) + y_std = np.asarray(y_std, dtype=float) + + # Two-sided per-point z-test against zero. z_crit turns alpha into a + # threshold on |z| (and, below, on the equivalence CI half-width). + z_crit = norm.ppf(1.0 - alpha / 2.0) + with np.errstate(divide="ignore", invalid="ignore"): + z = y_est / y_std + sig_nonzero = np.isfinite(z) & (np.abs(z) > z_crit) + direction = np.where(sig_nonzero, np.sign(y_est), 0).astype(int) + + per_point = { + "x": x, + "y_est": y_est, + "y_std": y_std, + "z": z, + "sig_nonzero": sig_nonzero, + "direction": direction, + } + + omnibus_W, omnibus_df, omnibus_p = _omnibus_chi2(y_est, y_cov) + + rollup = { + "omnibus_W": omnibus_W, + "omnibus_df": omnibus_df, + "omnibus_p": omnibus_p, + "n_nonzero": int(np.count_nonzero(sig_nonzero)), + "any_nonzero": bool(np.any(sig_nonzero)), + } + + return per_point, rollup + + +def _omnibus_chi2(y_est, y_cov): + """ + Wald/Mahalanobis test that the predicted vector differs from zero. + + ``W = y_est @ pinv(y_cov) @ y_est`` with ``df = rank(y_cov)``. The predicted + points live in a ``k``-dimensional space (``k`` = number of free params), so + ``y_cov`` is rank-deficient and a pseudo-inverse with ``df = rank`` is the + correct reduction. Returns ``(nan, 0, nan)`` when the covariance is not + finite. + """ + if not (np.all(np.isfinite(y_est)) and np.all(np.isfinite(y_cov))): + return np.nan, 0, np.nan + + # Symmetrize to kill numerical asymmetry from the finite-difference Jacobian + # before taking rank / pseudo-inverse. + y_cov = 0.5 * (y_cov + y_cov.T) + df = int(np.linalg.matrix_rank(y_cov)) + if df == 0: + return np.nan, 0, np.nan + + pinv = np.linalg.pinv(y_cov) + W = float(y_est @ pinv @ y_est) + if not np.isfinite(W) or W < 0: + return np.nan, df, np.nan + + p = float(chi2.sf(W, df)) + return W, df, p + + +def compute_delta(pred_std, delta_c=2.0): + """ + Default region-of-practical-equivalence half-width from prediction error. + + ``delta = delta_c * median(pred_std)`` over all finite predicted standard + errors. This ties "practically zero" to the typical *detectability* of the + experiment rather than to any biological effect size. Returns NaN if no + finite values are present. + + Parameters + ---------- + pred_std : array-like + Predicted per-point standard errors, pooled across all groups. + delta_c : float, optional + Multiplier on the median. Default 2.0. + """ + pred_std = np.asarray(pred_std, dtype=float) + finite = pred_std[np.isfinite(pred_std)] + if finite.size == 0: + return np.nan + return float(delta_c * np.median(finite)) + + +def classify_equiv(y_est, y_std, delta, alpha=0.05): + """ + Flag points whose whole CI lies inside the equivalence region [-delta, delta]. + + A point is ``equiv_zero`` when ``|y_est| + z_crit * y_std <= delta`` — i.e. + the two-sided ``(1 - alpha)`` confidence interval is entirely within the + region of practical equivalence around zero. NaN std or NaN/invalid delta + yield False. + + Parameters + ---------- + y_est, y_std : array-like + Per-point estimate and standard error. + delta : float + Equivalence half-width (see :func:`compute_delta`). + alpha : float, optional + Matches the confidence level used elsewhere. Default 0.05. + + Returns + ------- + np.ndarray of bool + """ + y_est = np.asarray(y_est, dtype=float) + y_std = np.asarray(y_std, dtype=float) + if not np.isfinite(delta): + return np.zeros(y_est.shape, dtype=bool) + + z_crit = norm.ppf(1.0 - alpha / 2.0) + ci_upper = np.abs(y_est) + z_crit * y_std + with np.errstate(invalid="ignore"): + equiv = np.isfinite(ci_upper) & (ci_upper <= delta) + return equiv + + +def benjamini_hochberg(pvals): + """ + Benjamini-Hochberg FDR-adjusted q-values. + + NaN p-values (failed / unassessable curves) pass through as NaN and are + excluded from the ranking. The returned q-values are aligned to the input + order and enforced monotone-nondecreasing in p, clipped to [0, 1]. + + Parameters + ---------- + pvals : array-like + Raw p-values, one per test (curve). + + Returns + ------- + np.ndarray of float + Adjusted q-values, same length/order as ``pvals``. + """ + pvals = np.asarray(pvals, dtype=float) + q = np.full(pvals.shape, np.nan) + + finite_idx = np.flatnonzero(np.isfinite(pvals)) + m = finite_idx.size + if m == 0: + return q + + p = pvals[finite_idx] + order = np.argsort(p) + ranks = np.arange(1, m + 1) + + q_sorted = p[order] * m / ranks + # Enforce monotonicity from the largest p downward (standard BH step-up). + q_sorted = np.minimum.accumulate(q_sorted[::-1])[::-1] + q_sorted = np.clip(q_sorted, 0.0, 1.0) + + q_finite = np.empty(m) + q_finite[order] = q_sorted + q[finite_idx] = q_finite + return q diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index 1f56f33..51e6eeb 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -7,26 +7,55 @@ ) from tfscreen.util.numerical import xfill +from .cat_assess import assess_best_model import numpy as np import pandas as pd -def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): +# Columns of the per-point assessment frame, in order. Kept as a module +# constant so empty-result paths emit an identically-shaped (empty) frame. +_ASSESS_COLS = ["x", "y_est", "y_std", "z", "sig_nonzero", "direction"] + +# Rollup keys added to flat_output by the post-hoc assessment. Listed here so +# the insufficient-data / all-fail paths can emit them as NaN. +_ROLLUP_KEYS = ["omnibus_W", "omnibus_df", "omnibus_p", "n_nonzero", + "any_nonzero"] + + +def _empty_assess_df(): + return pd.DataFrame({c: pd.Series(dtype=float) for c in _ASSESS_COLS}) + + +def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, + alpha=0.05, verbose=False): """ Fits multiple models to a single dataset and returns a flat dictionary of all results, suitable for aggregation. + Model selection uses AICc (the small-sample-corrected AIC) computed from + the *weighted* residuals, since with only a handful of points and up to + four parameters the correction term is large and the error bars carry real + information. After selection, the best model is evaluated at the observed x + and tested against zero (see :mod:`cat_assess`). + Parameters ---------- x, y, y_std : np.ndarray The independent variable, dependent variable, and standard error of the dependent variable. x_pred : np.ndarray, optional - array at which to predict x after fitting each model. If not specified, - fill in values within x. + array at which to predict x after fitting each model. If not specified, + fill in values within x. models_to_run : list of str, optional A list of model names to test. If None (default), all models in the global MODEL_LIBRARY will be tested. + best_only : bool, optional + If True (default), only the selected best model's predicted curve is + emitted in the returned prediction frame. If False, every successfully + fit model's curve is emitted (the larger, "all models" output). + alpha : float, optional + Two-sided significance level for the per-point ``sig_nonzero`` test. + Default 0.05. verbose : bool, optional If True, prints warnings to the console when a model fails to fit. Defaults to False. @@ -34,21 +63,23 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): Returns ------- dict - A single, flat dictionary containing the best model summary, AIC - weights for all tested models, and all parameter estimates and - standard errors for all tested models. + A single, flat dictionary containing the best model summary, AICc + weights for all tested models, all parameter estimates and standard + errors for all tested models, and the best-model omnibus rollup + (``omnibus_W/df/p``, ``n_nonzero``, ``any_nonzero``). + pd.DataFrame + Model predictions with columns ``model``, ``x``, ``y``, ``y_std``, + ``is_best_model``. Restricted to the best model unless ``best_only`` is + False. pd.DataFrame - a pandas dataframe holding model predictions. dataframe has five columns: - - 'model': name of model - - 'x': x-values used as the independent variable in the model - - 'y': y-values predicted by the model - - 'y_std': standard error on the y-values predicted by the model. (this - will be nan if the covariance matrix is not finite) - - 'is_best_model': whether or not this model was the best model tested + Per-point assessment of the best model at the observed (unique) x, with + columns ``x``, ``y_est``, ``y_std``, ``z``, ``sig_nonzero``, + ``direction``. Empty if no model could be fit. (``equiv_zero`` is added + downstream in ``cat_response`` once the global delta is known.) """ if models_to_run is None: - models_to_run = list(MODEL_LIBRARY.keys()) + models_to_run = list(MODEL_LIBRARY.keys()) flat_output = {} @@ -61,21 +92,20 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): if x_pred is None: x_pred = xfill(x) - model_pred_out = [] - x_pred_out = [] - y_pred_out = [] - y_pred_std_out = [] + # Assessment / omnibus grid: the unique observed concentrations (the shared + # titration series), one row per concentration. + x_assess = np.unique(x) - # Handle insufficient data case by returning nan dict and pred_df + # Handle insufficient data case by returning nan dict and empty pred/assess. if len(x) < 2: - # Build nan flat_output flat_output['status'] = "missing" flat_output['best_model'] = "None" flat_output['best_model_R2'] = np.nan flat_output['best_model_AIC_weight'] = np.nan + for key in _ROLLUP_KEYS: + flat_output[key] = np.nan for name in models_to_run: - param_names = MODEL_LIBRARY[name]["param_names"] flat_output[f"R2|{name}"] = np.nan flat_output[f"AIC_weight|{name}"] = np.nan @@ -83,16 +113,12 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): flat_output[f"{name}|{p_name}|est"] = np.nan flat_output[f"{name}|{p_name}|std"] = np.nan - # Build nan dataframe for pred_df - model_pred_out = np.repeat(models_to_run,len(x_pred)) - pred_df = pd.DataFrame({"model":model_pred_out, - "x":np.tile(x_pred,len(models_to_run)), - "y":np.full(len(model_pred_out), np.nan), - "y_std":np.full(len(model_pred_out), np.nan), - "is_best_model":np.zeros(len(model_pred_out), - dtype=bool)}) - - return flat_output, pred_df + pred_df = pd.DataFrame({"model": pd.Series(dtype=object), + "x": pd.Series(dtype=float), + "y": pd.Series(dtype=float), + "y_std": pd.Series(dtype=float), + "is_best_model": pd.Series(dtype=bool)}) + return flat_output, pred_df, _empty_assess_df() # Iterate through models and fit n = len(y) @@ -105,107 +131,161 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): param_names = MODEL_LIBRARY[name]["param_names"] bounds = MODEL_LIBRARY[name]["bounds"] k = len(param_names) - + + cov_matrix = None try: - + # Get guesses guesses = guess_func(x, y) # If this is a 1D array of values, we have normal guesses. Solve by - # nonlinear weighted least squares. + # nonlinear weighted least squares. if len(guesses.shape) == 1: - + params, std_err, cov_matrix, fit_obj = run_least_squares( model_func, y, y_std, guesses, bounds[0], bounds[1], args=(x,) ) if not fit_obj.success: raise RuntimeError(f"Fit failed: {fit_obj.message}") - # If this is a 2D array of values, this is a full design matrix. - # Solve by weighted least squares. + # If this is a 2D array of values, this is a full design matrix. + # Solve by weighted least squares. else: params, std_err, cov_matrix, _ = run_matrix_wls(guesses,y,1/y_std) - y_fit = model_func(params, x) - ss_res = np.sum((y - y_fit) ** 2) - ss_tot = np.sum((y - np.mean(y)) ** 2) - r2 = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0.0 - aic = 2 * k + n * np.log(ss_res / n) if ss_res > 0 else -np.inf - summary_results.append({"model": name, "R2": r2, "AIC": aic, "success": True}) - param_results[name] = {"params": params, "std_err": std_err, "names": param_names} + # Weighted residuals: fits are weighted, so selection and R2 should + # be too. chi2 is the weighted residual sum of squares. + resid = (y - y_fit) / y_std + chi2 = float(np.sum(resid ** 2)) + + w = 1.0 / (y_std ** 2) + y_wmean = np.average(y, weights=w) + ss_tot = float(np.sum(w * (y - y_wmean) ** 2)) + r2 = 1 - (chi2 / ss_tot) if ss_tot > 0 else 0.0 + + # AIC from the known-variance Gaussian likelihood (the model- + # independent -0.5*sum(log 2*pi*sigma^2) constant cancels in + # weights/deltas and is dropped). AICc adds the small-sample + # correction; when n - k - 1 <= 0 the model is unusable for + # selection (aicc = inf) but its params are still reported. + aic = 2 * k + chi2 + denom = n - k - 1 + aicc = aic + (2 * k * (k + 1) / denom) if denom > 0 else np.inf + + summary_results.append({"model": name, "R2": r2, "chi2": chi2, + "AIC": aic, "AICc": aicc, "success": True}) + param_results[name] = {"params": params, "std_err": std_err, + "cov": cov_matrix, "names": param_names, + "model_func": model_func} except (RuntimeError, ValueError) as e: if verbose: print(f"Warning: Model '{name}' failed to fit. Reason: {e}") - summary_results.append({"model": name, "R2": np.nan, "AIC": np.nan, "success": False}) + summary_results.append({"model": name, "R2": np.nan, "chi2": np.nan, + "AIC": np.nan, "AICc": np.nan, + "success": False}) param_results[name] = { - "params": np.full(k, np.nan), "std_err": np.full(k, np.nan), "names": param_names + "params": np.full(k, np.nan), "std_err": np.full(k, np.nan), + "cov": None, "names": param_names, "model_func": model_func } - params = None - - if params is not None: - - y_pred, y_pred_std = predict_with_error(model_func, - params, - cov_matrix, - args=[x_pred]) - - x_pred_out.extend(x_pred) - y_pred_out.extend(y_pred) - y_pred_std_out.extend(y_pred_std) - model_pred_out.extend([name for _ in x_pred]) - - # Post-process and flatten results + + # Post-process and flatten results. AICc drives selection and weights. summary_df = pd.DataFrame(summary_results) - valid_aics = summary_df.loc[summary_df['success'], 'AIC'] - if not valid_aics.empty: - min_aic = valid_aics.min() - summary_df['delta_AIC'] = summary_df['AIC'] - min_aic - relative_likelihood = np.exp(-0.5 * summary_df['delta_AIC']) + valid = summary_df.loc[summary_df['success'] & np.isfinite(summary_df['AICc'])] + if not valid.empty: + min_aicc = valid['AICc'].min() + summary_df['delta_AICc'] = summary_df['AICc'] - min_aicc + relative_likelihood = np.exp(-0.5 * summary_df['delta_AICc']) + # Models with infinite AICc (n - k - 1 <= 0) get zero weight. + relative_likelihood = relative_likelihood.where( + np.isfinite(summary_df['AICc']), 0.0 + ) sum_likelihoods = relative_likelihood.sum() summary_df['AIC_weight'] = relative_likelihood / sum_likelihoods else: summary_df['AIC_weight'] = np.nan - - summary_df = summary_df.sort_values(by="AIC").reset_index(drop=True) - + + summary_df = summary_df.sort_values(by="AICc").reset_index(drop=True) + # Populate the flat output dictionary for _, row in summary_df.iterrows(): model_name = row['model'] flat_output[f"AIC_weight|{model_name}"] = row['AIC_weight'] flat_output[f"R2|{model_name}"] = row['R2'] - - # Unpack params for this model + p_res = param_results[model_name] for i, p_name in enumerate(p_res['names']): flat_output[f"{model_name}|{p_name}|est"] = p_res['params'][i] flat_output[f"{model_name}|{p_name}|std"] = p_res['std_err'][i] - # Add overall status and best model info + # Overall status and best model info. success_states = summary_df['success'].unique() - if len(success_states) == 1 and success_states[0] == True: flat_output['status'] = "success" - elif len(success_states) == 1 and success_states[0] == False: flat_output['status'] = "failure" - else: flat_output['status'] = "partial" - - if not valid_aics.empty: + if len(success_states) == 1 and success_states[0] == True: + flat_output['status'] = "success" + elif len(success_states) == 1 and success_states[0] == False: + flat_output['status'] = "failure" + else: + flat_output['status'] = "partial" + + if not valid.empty: best_model_row = summary_df.iloc[0] - flat_output['best_model'] = best_model_row['model'] + best_model = best_model_row['model'] + flat_output['best_model'] = best_model flat_output['best_model_R2'] = best_model_row['R2'] flat_output['best_model_AIC_weight'] = best_model_row['AIC_weight'] else: + best_model = None flat_output['best_model'] = "None" flat_output['best_model_R2'] = np.nan flat_output['best_model_AIC_weight'] = np.nan - # Build final dataframe holding all predictions from the models - pred_df = pd.DataFrame({"model":model_pred_out, - "x":x_pred_out, - "y":y_pred_out, - "y_std":y_pred_std_out}) + # Predicted curves. Only successfully-fit models can be predicted, and only + # the best model unless best_only is False. + if best_only: + models_to_predict = [best_model] if best_model is not None else [] + else: + models_to_predict = [m for m in summary_df['model'] + if param_results[m]['cov'] is not None] + + pred_rows = [] + for name in models_to_predict: + p_res = param_results[name] + y_pred, y_pred_std = predict_with_error(p_res['model_func'], + p_res['params'], + p_res['cov'], + args=[x_pred]) + pred_rows.append(pd.DataFrame({ + "model": name, + "x": x_pred, + "y": y_pred, + "y_std": y_pred_std, + })) + + if pred_rows: + pred_df = pd.concat(pred_rows, ignore_index=True) + else: + pred_df = pd.DataFrame({"model": pd.Series(dtype=object), + "x": pd.Series(dtype=float), + "y": pd.Series(dtype=float), + "y_std": pd.Series(dtype=float)}) pred_df["is_best_model"] = pred_df["model"] == flat_output["best_model"] - - return flat_output, pred_df \ No newline at end of file + # Per-point assessment + omnibus rollup for the best model. + if best_model is not None: + p_res = param_results[best_model] + per_point, rollup = assess_best_model( + p_res['model_func'], p_res['params'], p_res['cov'], x_assess, + alpha=alpha + ) + assess_df = pd.DataFrame({c: per_point[c] for c in _ASSESS_COLS}) + for key in _ROLLUP_KEYS: + flat_output[key] = rollup[key] + else: + assess_df = _empty_assess_df() + for key in _ROLLUP_KEYS: + flat_output[key] = np.nan + + return flat_output, pred_df, assess_df diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index dd351c8..0263c79 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -2,17 +2,24 @@ Generic per-group categorical-response engine. Fits a family of candidate models to a ``y_obs`` vs ``x_obs`` curve independently -within each group and selects the best by AIC weight. Grouping mirrors +within each group and selects the best by AICc weight. Grouping mirrors ``extract_epistasis``: the ``genotype`` column is always the primary axis, with any additional ``group_by`` columns partitioning the analysis further. + +After fitting, a post-hoc pass grades each group's best curve against zero: a +per-point ``sig_nonzero`` test, an ``equiv_zero`` (region-of-practical- +equivalence) test using a global ``delta``, a per-curve omnibus chi-square test, +and a Benjamini-Hochberg FDR correction across curves (see :mod:`cat_assess`). """ from concurrent.futures import ProcessPoolExecutor import numpy as np import pandas as pd +import tqdm from .cat_fit import cat_fit +from .cat_assess import compute_delta, classify_equiv, benjamini_hochberg from tfscreen.mle.curve_models import MODEL_LIBRARY from tfscreen.util import resolve_workers from tfscreen.util.numerical import xfill @@ -23,16 +30,20 @@ _CHUNK_SIZE = 200 -def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, verbose): +def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, best_only, alpha, + verbose): """Run cat_fit for one group and tag the results with the group key.""" - flat_out, pred_df = cat_fit(x, y, y_std, - x_pred=x_pred, - models_to_run=models_to_run, - verbose=verbose) + flat_out, pred_df, assess_df = cat_fit(x, y, y_std, + x_pred=x_pred, + models_to_run=models_to_run, + best_only=best_only, + alpha=alpha, + verbose=verbose) for col, val in group_key.items(): flat_out[col] = val pred_df[col] = val - return flat_out, pred_df + assess_df[col] = val + return flat_out, pred_df, assess_df def _fit_chunk(chunk): @@ -52,15 +63,21 @@ def cat_response(df, y_std=None, group_by=None, models_to_run=None, + best_only=True, + alpha=0.05, + delta=None, + delta_c=2.0, num_workers=1, + progress=True, verbose=False): """ Classify each group's ``y_obs`` vs ``x_obs`` curve using categorical models. The DataFrame is partitioned into groups keyed by ``genotype`` plus any ``group_by`` columns. For each group, every model in ``models_to_run`` is fit - to the (``x_obs``, ``y_obs``, ``y_std``) data and the best is selected by AIC - weight. Fits are embarrassingly parallel across groups. + to the (``x_obs``, ``y_obs``, ``y_std``) data and the best is selected by AICc + weight. Fits are embarrassingly parallel across groups. A post-hoc pass then + grades each best curve against zero. Parameters ---------- @@ -80,23 +97,51 @@ def cat_response(df, 'genotype' alone. models_to_run : list of str or None, optional Model names to test. If None (default), all models in MODEL_LIBRARY. + best_only : bool, optional + If True (default), the returned prediction frame holds only each group's + best-model curve. If False, it holds every fit model's curve. + alpha : float, optional + Two-sided significance level for the per-point ``sig_nonzero`` test and + the confidence level of the ``equiv_zero`` interval. Also the threshold + applied to omnibus q-values when assigning ``response_class``. Default + 0.05. + delta : float or None, optional + Region-of-practical-equivalence half-width around zero. If None + (default), computed globally as ``delta_c * median(predicted y_std)`` + over all points (a detectability threshold). Pass a value for a fixed, + biologically-meaningful region. + delta_c : float, optional + Multiplier used when ``delta`` is derived from the median predicted + standard error. Default 2.0. num_workers : int, optional Number of worker processes. ``1`` runs serially in-process; ``-1`` uses ``os.cpu_count() - 1``; ``N`` uses ``N`` processes. Default 1. + progress : bool, optional + If True (default), show a tqdm progress bar over the per-group model + fits. verbose : bool, optional If True, print progress and per-model fit warnings. Default False. Returns ------- results_df : pandas.DataFrame - One row per group. Columns are the group-key columns followed by the flat - cat_fit output (``status``, ``best_model``, per-model ``AIC_weight|*`` / - ``R2|*`` weights, and ``||est`` / ``||std`` - parameter columns). Column names keep the ``|`` delimiter; presentation - (cleanup, per-model splitting) is left to callers. + One row per group. Group-key columns, then the flat cat_fit output + (``status``, ``best_model``, per-model ``AIC_weight|*`` / ``R2|*``, and + ``||est`` / ``std`` columns), then the assessment rollups + (``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``omnibus_q``, + ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, ``response_class``). + Column names keep the ``|`` delimiter; presentation is left to callers. predictions_df : pandas.DataFrame Predicted curves, concatenated across groups. Columns are the group-key columns followed by ``model``, ``x``, ``y``, ``y_std``, ``is_best_model``. + Restricted to the best model per group unless ``best_only`` is False. + assessment_df : pandas.DataFrame + Per-point best-model assessment at the observed (unique) x. Group-key + columns followed by ``x``, ``y_est``, ``y_std``, ``z``, ``sig_nonzero``, + ``direction``, ``equiv_zero``. + delta : float + The equivalence half-width actually used (resolved from ``delta`` / + ``delta_c`` if not supplied). """ if models_to_run is None: models_to_run = list(MODEL_LIBRARY.keys()) @@ -134,12 +179,14 @@ def cat_response(df, else: ys = np.ones(len(x), dtype=float) - work_items.append((group_key, x, y, ys, x_pred, models_to_run, verbose)) + work_items.append((group_key, x, y, ys, x_pred, models_to_run, + best_only, alpha, verbose)) n_total = len(work_items) if n_total == 0: empty = pd.DataFrame(columns=group_cols) - return empty.copy(), empty.copy() + resolved_delta = delta if delta is not None else np.nan + return empty.copy(), empty.copy(), empty.copy(), resolved_delta workers = resolve_workers(num_workers) chunks = list(_iter_chunks(work_items, _CHUNK_SIZE)) @@ -149,26 +196,104 @@ def cat_response(df, flush=True) results = [] - if workers == 1: - # Serial fast-path: run in-process, no pickling/IPC overhead. - for chunk in chunks: - results.extend(_fit_chunk(chunk)) - else: - # executor.map preserves input order, so results stay aligned with - # work_items without an explicit index map. - with ProcessPoolExecutor(max_workers=workers) as executor: - for chunk_result in executor.map(_fit_chunk, chunks): + with tqdm.tqdm(total=n_total, desc="Fitting groups", unit="group", + disable=not progress) as pbar: + if workers == 1: + # Serial fast-path: run in-process, no pickling/IPC overhead. + for chunk in chunks: + chunk_result = _fit_chunk(chunk) results.extend(chunk_result) + pbar.update(len(chunk_result)) + else: + # executor.map preserves input order, so results stay aligned with + # work_items without an explicit index map. + with ProcessPoolExecutor(max_workers=workers) as executor: + for chunk_result in executor.map(_fit_chunk, chunks): + results.extend(chunk_result) + pbar.update(len(chunk_result)) flat_list = [r[0] for r in results] pred_list = [r[1] for r in results] + assess_list = [r[2] for r in results] results_df = pd.DataFrame(flat_list) - other = [c for c in results_df.columns if c not in group_cols] - results_df = results_df[group_cols + other] predictions_df = pd.concat(pred_list, ignore_index=True) pred_other = [c for c in predictions_df.columns if c not in group_cols] predictions_df = predictions_df[group_cols + pred_other] - return results_df, predictions_df + assessment_df = pd.concat(assess_list, ignore_index=True) + + # --- Post-hoc pass: global delta, equivalence, FDR, response class ------- + resolved_delta = delta + if resolved_delta is None: + resolved_delta = compute_delta(assessment_df.get("y_std", []), delta_c) + + if len(assessment_df): + assessment_df["equiv_zero"] = classify_equiv( + assessment_df["y_est"].to_numpy(dtype=float), + assessment_df["y_std"].to_numpy(dtype=float), + resolved_delta, + alpha=alpha, + ) + else: + assessment_df["equiv_zero"] = pd.Series(dtype=bool) + + # Per-group all_equiv_zero rollup (a curve is "confidently flat at zero" + # only if *every* point's CI sits inside the equivalence region). + if len(assessment_df): + all_equiv = (assessment_df.groupby(group_cols, sort=False, + observed=True)["equiv_zero"] + .all()) + results_df = results_df.merge( + all_equiv.rename("all_equiv_zero").reset_index(), + on=group_cols, how="left" + ) + else: + results_df["all_equiv_zero"] = pd.Series(dtype=bool) + + # FDR across curves on the omnibus p-values. + results_df["omnibus_q"] = benjamini_hochberg( + results_df.get("omnibus_p", pd.Series(np.nan, index=results_df.index)) + ) + + # Three-valued response class: real / confident_zero / indeterminate. + results_df["response_class"] = _response_class(results_df, alpha) + + # Order columns: group keys first. + other = [c for c in results_df.columns if c not in group_cols] + results_df = results_df[group_cols + other] + + assess_other = [c for c in assessment_df.columns if c not in group_cols] + assessment_df = assessment_df[group_cols + assess_other] + + return results_df, predictions_df, assessment_df, resolved_delta + + +def _response_class(results_df, alpha): + """ + Assign each curve to confident_zero / real / indeterminate. + + Equivalence takes precedence over significance: a curve whose every point + sits inside the region of practical equivalence is ``confident_zero`` even + if the omnibus test finds a statistically significant but practically + negligible offset (tiny error bars make a hair-off-zero baseline + "significant"). The ROPE is what makes "real" mean *meaningfully* nonzero. + + - ``confident_zero``: every point's CI lies within [-delta, delta]. + - ``real``: not confident_zero, and omnibus q-value < alpha (distinguishable + from zero by more than the equivalence margin somewhere on the curve). + - ``indeterminate``: neither (too noisy to call, or unassessable). + """ + q = results_df.get("omnibus_q", + pd.Series(np.nan, index=results_df.index)).to_numpy() + all_equiv = results_df.get( + "all_equiv_zero", pd.Series(False, index=results_df.index) + ).fillna(False).to_numpy(dtype=bool) + + out = np.full(len(results_df), "indeterminate", dtype=object) + real = np.isfinite(q) & (q < alpha) + out[real] = "real" + # Equivalence wins over a barely-significant offset. + out[all_equiv] = "confident_zero" + return out diff --git a/src/tfscreen/analysis/compare_theta.py b/src/tfscreen/analysis/compare_theta.py index 8f60d05..6361c07 100644 --- a/src/tfscreen/analysis/compare_theta.py +++ b/src/tfscreen/analysis/compare_theta.py @@ -244,6 +244,19 @@ def compare_theta(estimate_dfs, ``mean_reported_sigma``, ``spread_estimator``, and one ``sd[...]`` column per condition-grid point. + The ``tier`` is the graded Axis-1 label. When the result is passed to + :func:`stability_crosstabs`, the tiers collapse onto the crosstab rows + as follows: + + * ``A`` and ``B`` -> ``reproducible`` + * ``C`` and ``D`` -> ``unstable`` + * ``low_coverage`` -> dropped (never graded, so not in any crosstab) + + The crosstab *columns* come from the other two per-genotype fields, not + from the tier: ``overdispersed`` (``overconfident`` vs ``consistent``) + for ``tier_vs_overdispersion``, and ``dynamic_range`` (``informative`` + vs ``flat``) for ``tier_vs_dynamic_range``. + Raises ------ ValueError @@ -524,3 +537,200 @@ def stability_crosstabs(result, "tier_vs_overdispersion": tier_vs_over, "tier_vs_dynamic_range": tier_vs_range, } + + +def _parse_quantile_columns(df): + """ + Return ``{level: column_name}`` for columns named ``q`` with a level + strictly inside (0, 1) (e.g. ``'q0.159' -> 0.159``). + """ + out = {} + for c in df.columns: + if not (isinstance(c, str) and c.startswith("q")): + continue + try: + lvl = float(c[1:]) + except ValueError: + continue + if 0.0 < lvl < 1.0: + out[lvl] = c + return out + + +def _monotone_knots(values, probs): + """ + Collapse a (non-decreasing) quantile ladder to strictly increasing knots. + + Ties in ``values`` (a flat region of the quantile function, i.e. a point + mass) are merged, keeping the highest cumulative probability at that value + -- the CDF value just above the mass. + + Returns + ------- + (numpy.ndarray, numpy.ndarray) + Strictly increasing ``values`` and their (non-decreasing) ``probs``. + """ + keep_v = [] + keep_p = [] + for x, p in zip(values, probs): + if keep_v and x <= keep_v[-1]: + keep_p[-1] = p # probs are ascending, so this keeps the max + else: + keep_v.append(x) + keep_p.append(p) + return np.asarray(keep_v, dtype=float), np.asarray(keep_p, dtype=float) + + +def _mixture_quantiles(value_matrix, probs): + """ + Quantiles of the equal-weight mixture of N per-run marginal posteriors. + + Each run contributes a marginal distribution described by its quantile + ladder (``probs`` -> ``value_matrix[i]``). The mixture "pick a run at random, + then draw from its posterior" has CDF ``F_mix = mean_i F_i``; its variance is + the within-run variance plus the between-run spread of the point estimates + (law of total variance). This reads the mixture's quantiles at the same + ``probs`` levels by averaging the per-run CDFs on the pooled support grid and + inverting. + + Parameters + ---------- + value_matrix : numpy.ndarray, shape (n_present, K) + Each row is one run's non-decreasing quantile values at ``probs``. + probs : numpy.ndarray, shape (K,) + Ascending probability levels in (0, 1), shared across runs. + + Returns + ------- + numpy.ndarray, shape (K,) + Mixture quantile values at ``probs``. + """ + n = value_matrix.shape[0] + grid = np.unique(value_matrix) + if grid.size == 1: + # Every run is a point mass at the same value -> mixture is that mass. + return np.full(probs.shape, grid[0], dtype=float) + + # Average the per-run CDFs on the pooled support grid. + f_sum = np.zeros(grid.size) + for i in range(n): + xs, ps = _monotone_knots(value_matrix[i], probs) + if xs.size == 1: + # Point-mass run: step from 0 to 1 at its single value. + f_i = np.where(grid < xs[0], 0.0, 1.0) + else: + f_i = np.interp(grid, xs, ps, left=0.0, right=1.0) + f_sum += f_i + f_mix = f_sum / n + + # Invert F_mix. Collapse ties (flat CDF = zero-density gaps) keeping the + # smallest theta for each CDF level, matching the standard quantile def. + uf, idx = np.unique(f_mix, return_index=True) + return np.interp(probs, uf, grid[idx]) + + +def aggregate_theta(estimate_dfs, *, progress_every=200_000): + """ + Combine N theta estimates into one aggregate theta-vs-condition table. + + For every ``(genotype, [titrant_name,] titrant_conc)`` the N runs present are + combined as an **equal-weight mixture** of their per-run marginal posteriors + (reconstructed from the stored quantile ladder). The aggregate error thus + folds in both each run's own posterior width *and* the run-to-run spread of + the point estimates (law of total variance), and does **not** shrink with N + -- appropriate because different-seed runs are not independent replicates. + A bias shared by all N runs is invisible to this (only sampled variation -- + seed or training-data dropout -- is captured). + + In reference mode on the CLI side, only the N estimate runs are mixed; a + reference run is never folded in (it is a comparison target, not a sample). + + Parameters + ---------- + estimate_dfs : list of pandas.DataFrame + The N estimate tables. Each must share the same key columns and carry a + quantile ladder (``q`` columns). The mixture is taken over the + probability levels present in *all* runs. + progress_every : int, optional + Print a progress line every this many genotype-condition groups. Default + 200000. + + Returns + ------- + pandas.DataFrame + Long-form, one row per ``(genotype, [titrant_name,] titrant_conc)``, with + the shared ``q`` columns holding the mixture quantiles and an + ``n_present`` column (how many runs contributed). Sorted by the keys. + + Raises + ------ + ValueError + If fewer than two runs are supplied, the key sets disagree, or the runs + share fewer than two quantile levels. + """ + if not isinstance(estimate_dfs, (list, tuple)): + raise ValueError( + "estimate_dfs must be a list of DataFrames, " + f"not {type(estimate_dfs).__name__}." + ) + if len(estimate_dfs) < 2: + raise ValueError( + f"aggregate_theta requires at least 2 estimate tables; " + f"got {len(estimate_dfs)}." + ) + + keys = _detect_keys(estimate_dfs[0]) + + # Intersect the quantile levels across runs; keep run 0's column names. + levels = None + qmap0 = _parse_quantile_columns(estimate_dfs[0]) + for i, df in enumerate(estimate_dfs): + if _detect_keys(df) != keys: + raise ValueError( + f"Estimate run {i} has key columns {_detect_keys(df)}, " + f"which differ from run 0's {keys}." + ) + lv = set(_parse_quantile_columns(df)) + levels = lv if levels is None else (levels & lv) + levels = sorted(levels) + if len(levels) < 2: + raise ValueError( + "Estimate tables share fewer than 2 quantile (q) columns; " + "cannot reconstruct per-run distributions to mix." + ) + probs = np.asarray(levels, dtype=float) + out_cols = [qmap0[lvl] for lvl in levels] + + # Stack keys and the aligned value matrix across all runs. + key_frames = [] + value_blocks = [] + for df in estimate_dfs: + qmap = _parse_quantile_columns(df) + key_frames.append(df.loc[:, keys]) + value_blocks.append( + df.loc[:, [qmap[lvl] for lvl in levels]].to_numpy(dtype=float) + ) + long_keys = pd.concat(key_frames, ignore_index=True) + values = np.vstack(value_blocks) + + groups = long_keys.groupby(keys, sort=False).indices + group_keys = list(groups.keys()) + n_groups = len(group_keys) + + out_values = np.empty((n_groups, len(levels)), dtype=float) + n_present = np.empty(n_groups, dtype=int) + for gi, gkey in enumerate(group_keys): + pos = groups[gkey] + n_present[gi] = pos.size + out_values[gi] = _mixture_quantiles(values[pos], probs) + if progress_every and (gi + 1) % progress_every == 0: + print(f" aggregated {gi + 1}/{n_groups} groups...", flush=True) + + if len(keys) == 1: + out = pd.DataFrame({keys[0]: group_keys}) + else: + out = pd.DataFrame(group_keys, columns=keys) + for j, col in enumerate(out_cols): + out[col] = out_values[:, j] + out["n_present"] = n_present + return out.sort_values(keys).reset_index(drop=True) diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index 9a60fa4..cfe7976 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -61,20 +61,30 @@ def cat_response(data_file, y_std=None, group_by=None, models=None, + alpha=0.05, + delta=None, + delta_c=2.0, + write_all_predictions=False, num_workers=-1): """ Classify each group's response curve using categorical response models. Reads a long-form CSV and fits one or more response models to every group. Groups are defined by the 'genotype' column plus any --group_by columns. For - each group the best-fitting model is selected by AIC weight. + each group the best-fitting model is selected by AICc weight, then graded + against zero (per-point sig_nonzero / equiv_zero tests + a per-curve omnibus + chi-square with a Benjamini-Hochberg FDR correction). Writes: - {out_prefix}.csv one row per group; all models' weights and - parameter estimates. + parameter estimates, plus the assessment + rollups (omnibus_p/q, n_nonzero, + all_equiv_zero, response_class). - {out_prefix}_{model}.csv one file per model; that model's parameter table and per-group fit statistics. - - {out_prefix}_predictions.csv predicted curves for every group and model. + - {out_prefix}_predictions.csv best-model predicted curves (all models + when --write_all_predictions). + - {out_prefix}_assessment.csv per (group, x) best-model assessment. Parameters ---------- @@ -96,6 +106,18 @@ def cat_response(data_file, omitted, groups are defined by 'genotype' alone. models : list of str or None, optional Response models to fit. Defaults to all models in MODEL_LIBRARY. + alpha : float, optional + Significance level for the per-point tests and the omnibus q-value + threshold used to call a curve 'real'. Default 0.05. + delta : float or None, optional + Region-of-practical-equivalence half-width around zero. If None + (default), derived globally as ``delta_c * median(predicted y_std)``. + Pass a value for a fixed, biologically-meaningful region. + delta_c : float, optional + Multiplier used when ``delta`` is auto-derived. Default 2.0. + write_all_predictions : bool, optional + If True, write every fit model's predicted curve rather than only the + best model's. Default False. num_workers : int, optional Number of parallel worker processes. ``1`` runs serially; ``-1`` (the default) uses ``os.cpu_count() - 1``; ``N`` uses ``N`` processes. @@ -136,13 +158,22 @@ def cat_response(data_file, print(f" Fitting groups defined by {group_cols} " f"with {num_workers} worker(s)...", flush=True) - results_df, predictions_df = _cat_response(df, - x_obs=x_obs, - y_obs=y_obs, - y_std=y_std, - group_by=group_by, - models_to_run=models, - num_workers=num_workers) + results_df, predictions_df, assessment_df, resolved_delta = _cat_response( + df, + x_obs=x_obs, + y_obs=y_obs, + y_std=y_std, + group_by=group_by, + models_to_run=models, + best_only=(not write_all_predictions), + alpha=alpha, + delta=delta, + delta_c=delta_c, + num_workers=num_workers, + ) + + print(f" Using equivalence half-width delta = {resolved_delta:.6g}", + flush=True) # Main table: flat, one row per group, with clean column names. main_df = results_df.copy() @@ -161,6 +192,11 @@ def cat_response(data_file, predictions_df.to_csv(pred_file, index=False) print(f"Wrote {len(predictions_df)} rows to {pred_file}", flush=True) + # Per-point best-model assessment. + assess_file = f"{out_prefix}_assessment.csv" + assessment_df.to_csv(assess_file, index=False) + print(f"Wrote {len(assessment_df)} rows to {assess_file}", flush=True) + def main(): generalized_main(cat_response, diff --git a/src/tfscreen/analysis/scripts/compare_theta_cli.py b/src/tfscreen/analysis/scripts/compare_theta_cli.py index e3f48b8..793032e 100644 --- a/src/tfscreen/analysis/scripts/compare_theta_cli.py +++ b/src/tfscreen/analysis/scripts/compare_theta_cli.py @@ -6,6 +6,7 @@ from tfscreen.analysis.compare_theta import ( compare_theta as _compare_theta, + aggregate_theta, stability_crosstabs, ) from tfscreen.util.cli import generalized_main, read_lines @@ -25,7 +26,8 @@ def compare_theta(estimates_file, out_prefix="tfs_compare_theta", reference=None, min_coverage=0.5, - overdispersion_threshold=2.0): + overdispersion_threshold=2.0, + write_aggregate=False): """ Grade per-genotype theta stability across N independent estimate runs. @@ -56,6 +58,13 @@ def compare_theta(estimates_file, overdispersion_threshold : float, optional Overdispersion above this sets the 'overdispersed' flag and the 'overconfident' crosstab column. Default 2.0. + write_aggregate : bool, optional + If set, also write {out_prefix}_aggregate.csv: a long-form aggregate + theta-vs-condition table combining the N estimate runs as an equal-weight + mixture of their per-run posteriors (quantiles reconstructed and mixed). + The aggregate error folds in both per-run posterior width and run-to-run + spread and does not shrink with N. Only the N estimate runs are mixed -- + a reference run (if any) is never included. Default False. """ paths = read_lines(estimates_file) if not paths: @@ -95,6 +104,14 @@ def compare_theta(estimates_file, fh.write(_format_crosstabs(crosstabs)) print(f"Wrote interpretation crosstabs to {crosstab_file}", flush=True) + if write_aggregate: + print("Building aggregate theta-vs-condition table...", flush=True) + aggregate = aggregate_theta(estimate_dfs) + aggregate_file = f"{out_prefix}_aggregate.csv" + aggregate.to_csv(aggregate_file, index=False) + print(f"Wrote {len(aggregate)} aggregate rows to {aggregate_file}", + flush=True) + def main(): generalized_main(compare_theta, diff --git a/src/tfscreen/mle/curve_models/guesses.py b/src/tfscreen/mle/curve_models/guesses.py index ae73a14..45e1084 100644 --- a/src/tfscreen/mle/curve_models/guesses.py +++ b/src/tfscreen/mle/curve_models/guesses.py @@ -38,8 +38,17 @@ def guess_linear(x, y): ------- np.ndarray design matrix for polynomial wls + + Notes + ----- + Columns are ``[x, 1]`` (not reversed), so the weighted-least-squares + solution comes back in the order ``[m, b]`` (slope, intercept). This + matches ``model_linear`` (``params[0]*x + params[1]``) and the ``['m', 'b']`` + ``param_names`` for the "linear" model. This is the deliberate exception to + the ``guess_poly_*`` functions, which reverse to ascending ``[c0, c1, ...]`` + order to match ``model_poly``. """ - X = np.vander(x,2)[:,::-1] + X = np.vander(x,2) return X def guess_repressor(x, y): diff --git a/src/tfscreen/mle/predict_with_error.py b/src/tfscreen/mle/predict_with_error.py index 55605ab..f2b505a 100644 --- a/src/tfscreen/mle/predict_with_error.py +++ b/src/tfscreen/mle/predict_with_error.py @@ -4,7 +4,8 @@ def predict_with_error(some_model, params, cov_matrix, args=None, - epsilon=1e-6): + epsilon=1e-6, + full_cov=False): """ Calculate model predictions and their standard errors. @@ -26,6 +27,10 @@ def predict_with_error(some_model, epsilon : float, optional The small step size used for numerical differentiation (central difference method) to calculate the Jacobian. + full_cov : bool, optional + If True, also return the full prediction covariance matrix + ``J @ Cov(p) @ J.T`` (shape ``(M, M)`` for ``M`` predicted points). + Default False. Returns ------- @@ -33,8 +38,11 @@ def predict_with_error(some_model, The predicted values from the model. calc_se : np.ndarray The standard error for each predicted value. + calc_cov : np.ndarray + Only returned when ``full_cov`` is True: the full ``(M, M)`` prediction + covariance matrix. Filled with NaN if ``cov_matrix`` is invalid. """ - + num_params = len(params) if args is None: args = [] @@ -44,6 +52,9 @@ def predict_with_error(some_model, # If the covariance matrix is invalid, we can't propagate error. if np.any(np.isnan(cov_matrix)): calc_se = np.full_like(calc_values, np.nan) + if full_cov: + m = calc_values.size + return calc_values, calc_se, np.full((m, m), np.nan) return calc_values, calc_se # Calculate the Jacobian of the Model (J_pred), which is the matrix of @@ -51,7 +62,7 @@ def predict_with_error(some_model, # This is calculated numerically via the central difference method. J_pred = np.zeros((calc_values.size, num_params)) for i in range(num_params): - + params_plus = params.copy() params_plus[i] += epsilon pred_plus = some_model(params_plus,*args) @@ -63,12 +74,16 @@ def predict_with_error(some_model, derivative = (pred_plus - pred_minus) / (2 * epsilon) J_pred[:, i] = derivative - # Propagate error: Var(y) = J @ Cov(p) @ J.T - # We only need the diagonal of this result, which can be calculated - # efficiently as follows: + # Propagate error: Cov(y) = J @ Cov(p) @ J.T. The per-point variance is the + # diagonal, which can be computed efficiently without forming the full + # matrix. calc_var = np.sum((J_pred @ cov_matrix) * J_pred, axis=1) with np.errstate(invalid='ignore'): # Ignore sqrt of potential negative variance calc_se = np.sqrt(calc_var) + if full_cov: + calc_cov = J_pred @ cov_matrix @ J_pred.T + return calc_values, calc_se, calc_cov + return calc_values, calc_se \ No newline at end of file diff --git a/tests/tfscreen/analysis/cat_response/test_cat_assess.py b/tests/tfscreen/analysis/cat_response/test_cat_assess.py new file mode 100644 index 0000000..7813cc1 --- /dev/null +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -0,0 +1,153 @@ +""" +Tests for the post-hoc assessment helpers: omnibus chi-square, per-point +significance, region-of-practical-equivalence classification, and BH FDR. +""" +import numpy as np +import pytest +from scipy.stats import chi2, norm + +from tfscreen.analysis.cat_response.cat_assess import ( + assess_best_model, + compute_delta, + classify_equiv, + benjamini_hochberg, + _omnibus_chi2, +) + + +def _linear(params, x): + m, b = params + return m * x + b + + +# --- omnibus ----------------------------------------------------------------- + +class TestOmnibus: + + def test_identity_cov_is_sum_of_squared_z(self): + """With Sigma = I, W = sum(y_est^2) and df = len(y_est).""" + y_est = np.array([1.0, -2.0, 0.5]) + W, df, p = _omnibus_chi2(y_est, np.eye(3)) + assert df == 3 + assert W == pytest.approx(np.sum(y_est ** 2)) + assert p == pytest.approx(chi2.sf(W, 3)) + + def test_rank_deficient_uses_pseudo_inverse_and_rank_df(self): + """A singular covariance yields df = rank, not the matrix dimension.""" + # Rank-1 covariance (two identical predicted points). + cov = np.array([[1.0, 1.0], [1.0, 1.0]]) + y_est = np.array([2.0, 2.0]) + W, df, p = _omnibus_chi2(y_est, cov) + assert df == 1 + assert np.isfinite(W) + assert np.isfinite(p) + + def test_nan_cov_returns_nan(self): + W, df, p = _omnibus_chi2(np.array([1.0, 2.0]), + np.full((2, 2), np.nan)) + assert np.isnan(W) and df == 0 and np.isnan(p) + + def test_assess_best_model_flags_and_rollup(self): + # y = 1*x + 0 at x=[1,2,3] -> y_est = [1,2,3], all clearly nonzero. + x = np.array([1.0, 2.0, 3.0]) + params = np.array([1.0, 0.0]) + cov = np.diag([1e-4, 1e-4]) # tiny -> small pred se -> significant + per_point, rollup = assess_best_model(_linear, params, cov, x) + + assert per_point["y_est"] == pytest.approx([1.0, 2.0, 3.0]) + assert np.all(per_point["sig_nonzero"]) + assert np.all(per_point["direction"] == 1) + assert rollup["n_nonzero"] == 3 + assert rollup["any_nonzero"] is True + assert rollup["omnibus_df"] == 2 # two free params + assert rollup["omnibus_p"] < 1e-6 + + def test_assess_best_model_nan_cov(self): + x = np.array([1.0, 2.0, 3.0]) + per_point, rollup = assess_best_model( + _linear, np.array([1.0, 0.0]), np.full((2, 2), np.nan), x + ) + assert np.all(~per_point["sig_nonzero"]) + assert np.isnan(rollup["omnibus_p"]) + assert rollup["n_nonzero"] == 0 + + +# --- equivalence ------------------------------------------------------------- + +class TestClassifyEquiv: + + def test_ci_inside_region_is_equiv(self): + # |y| + z*se = 0.1 + 1.96*0.01 ~ 0.12 < delta=0.5 -> equiv. + equiv = classify_equiv(np.array([0.1]), np.array([0.01]), delta=0.5) + assert equiv[0] + + def test_ci_straddles_boundary_not_equiv(self): + # 0.4 + 1.96*0.1 ~ 0.596 > 0.5 -> not equiv (too wide / too far). + equiv = classify_equiv(np.array([0.4]), np.array([0.1]), delta=0.5) + assert not equiv[0] + + def test_far_from_zero_not_equiv(self): + equiv = classify_equiv(np.array([5.0]), np.array([0.01]), delta=0.5) + assert not equiv[0] + + def test_nan_std_and_bad_delta(self): + assert not classify_equiv(np.array([0.0]), np.array([np.nan]), + delta=0.5)[0] + assert not classify_equiv(np.array([0.0]), np.array([0.01]), + delta=np.nan)[0] + + def test_boundary_uses_alpha(self): + # Larger alpha -> narrower CI -> easier to be equivalent. + y, s, d = np.array([0.3]), np.array([0.1]), 0.5 + # alpha=0.05 -> 0.3+1.96*0.1=0.496 < 0.5 -> equiv (just). + assert classify_equiv(y, s, d, alpha=0.05)[0] + + +# --- compute_delta ----------------------------------------------------------- + +class TestComputeDelta: + + def test_median_times_c(self): + stds = [0.1, 0.2, 0.3, np.nan] + # median of finite [0.1,0.2,0.3] = 0.2 -> *2 = 0.4 + assert compute_delta(stds, delta_c=2.0) == pytest.approx(0.4) + + def test_all_nan_returns_nan(self): + assert np.isnan(compute_delta([np.nan, np.nan])) + + def test_empty_returns_nan(self): + assert np.isnan(compute_delta([])) + + +# --- benjamini-hochberg ------------------------------------------------------ + +class TestBenjaminiHochberg: + + def test_matches_manual_step_up(self): + p = np.array([0.01, 0.02, 0.03, 0.04, 0.05]) + q = benjamini_hochberg(p) + m = 5 + expected = p * m / np.arange(1, m + 1) + expected = np.minimum.accumulate(expected[::-1])[::-1] + assert np.allclose(q, expected) + + def test_monotone_nondecreasing_in_p(self): + p = np.array([0.5, 0.001, 0.2, 0.04]) + q = benjamini_hochberg(p) + order = np.argsort(p) + assert np.all(np.diff(q[order]) >= -1e-12) + + def test_nan_passthrough_and_excluded(self): + p = np.array([0.01, np.nan, 0.5]) + q = benjamini_hochberg(p) + assert np.isnan(q[1]) + # m=2 finite tests, not 3. + assert q[0] == pytest.approx(min(0.01 * 2 / 1, q[2])) + + def test_clipped_to_one(self): + q = benjamini_hochberg(np.array([0.9, 0.95])) + assert np.all(q <= 1.0) + + def test_all_nan(self): + q = benjamini_hochberg(np.array([np.nan, np.nan])) + assert np.all(np.isnan(q)) diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index ca06cae..5dc8cb8 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -1,252 +1,180 @@ - -import pytest +""" +Tests for cat_fit: AICc/weighted-R2 selection, best_only predictions, the +per-point assessment/omnibus rollup, and the insufficient-data path. Uses the +real MODEL_LIBRARY (fits are deterministic) rather than mocks. +""" import numpy as np -import pandas as pd -from unittest.mock import MagicMock, patch +import pytest from tfscreen.analysis.cat_response.cat_fit import cat_fit +from tfscreen.mle.curve_models import MODEL_LIBRARY + + +# A titration-like grid with enough points that low-parameter models are usable. +X = np.array([0.0, 1.0, 3.0, 10.0, 30.0, 100.0]) + +# Small deterministic +/- scatter. Keeps residuals (and therefore the fitted +# covariance) nonzero -- run_matrix_wls scales covariance by the reduced +# chi-square, so a *perfect* fit would collapse it to zero. +_WOBBLE = np.array([1.0, -1.0, 1.0, -1.0, 1.0, -1.0]) + + +def _hill_data(baseline=0.1, amplitude=0.7, logK=np.log(10.0), n=1.0, + std=0.02): + from tfscreen.mle.curve_models.models import model_hill_4p + y = model_hill_4p([baseline, amplitude, logK, n], X) + y = y + 0.2 * std * _WOBBLE # tiny wobble -> finite covariance + return X, y, np.full_like(X, std) + + +def _flat_data(c=5.0, std=0.1): + # Symmetric about c -> fitted baseline is exactly c, residuals nonzero. + return X, c + std * 0.5 * _WOBBLE, np.full_like(X, std) + + +def _zero_data(std=0.1): + # Symmetric about 0 -> fitted baseline exactly 0, residuals nonzero. + return X, std * 0.5 * _WOBBLE, np.full_like(X, std) + + +# --- return shape ------------------------------------------------------------ + +def test_returns_three_tuple(): + x, y, ys = _hill_data() + out = cat_fit(x, y, ys, models_to_run=["flat", "hill_inducer"]) + assert len(out) == 3 + flat_output, pred_df, assess_df = out + assert isinstance(flat_output, dict) + + +# --- selection --------------------------------------------------------------- + +def test_structured_selected_for_sigmoid_data(): + x, y, ys = _hill_data() + flat_output, _, _ = cat_fit(x, y, ys, + models_to_run=["flat", "hill_inducer"]) + assert flat_output["best_model"] == "hill_inducer" + # Weighted R2 near 1 for a (near) perfect fit. + assert flat_output["R2|hill_inducer"] > 0.99 + assert flat_output["status"] == "success" + + +def test_flat_selected_for_flat_data(): + x, y, ys = _flat_data() + flat_output, _, _ = cat_fit(x, y, ys, + models_to_run=["flat", "hill_inducer"]) + # AICc prefers the cheaper (1-param) flat model over a 4-param hill that + # buys no fit improvement. + assert flat_output["best_model"] == "flat" -# Mock data for testing -@pytest.fixture -def mock_data(): - x = np.array([1.0, 2.0, 3.0, 4.0]) - y = np.array([2.0, 4.0, 6.0, 8.0]) - y_std = np.array([0.1, 0.1, 0.1, 0.1]) - return x, y, y_std - -@pytest.fixture -def mock_predict_side_effect(): - def side_effect(model_func, params, cov, args=None): - if args and len(args) > 0: - x_pred = args[0] - return np.zeros(len(x_pred)), np.zeros(len(x_pred)) - return np.array([]), np.array([]) - return side_effect + +def test_aicc_excludes_overparameterized_models(): + """With few points a 4-param model has n-k-1<=0 -> can't win, params kept.""" + # 4 points, hill_inducer has k=4 -> denom = 4-4-1 = -1 -> AICc = inf. + x = np.array([0.0, 1.0, 10.0, 100.0]) + y = np.array([0.1, 0.3, 0.6, 0.8]) + ys = np.full_like(x, 0.05) + flat_output, _, _ = cat_fit(x, y, ys, + models_to_run=["flat", "hill_inducer"]) + + assert flat_output["best_model"] == "flat" + assert flat_output["AIC_weight|hill_inducer"] == 0.0 + # Params are still reported (not selected != not fit). + for p in MODEL_LIBRARY["hill_inducer"]["param_names"]: + assert f"hill_inducer|{p}|est" in flat_output + + +# --- predictions ------------------------------------------------------------- + +def test_best_only_predicts_single_model(): + x, y, ys = _hill_data() + _, pred_df, _ = cat_fit(x, y, ys, models_to_run=["flat", "hill_inducer"], + best_only=True) + assert set(pred_df["model"].unique()) == {"hill_inducer"} + assert pred_df["is_best_model"].all() + + +def test_write_all_predicts_every_model(): + x, y, ys = _hill_data() + _, pred_df, _ = cat_fit(x, y, ys, models_to_run=["flat", "hill_inducer"], + best_only=False) + assert set(pred_df["model"].unique()) == {"flat", "hill_inducer"} + # is_best_model marks only the selected model. + assert set(pred_df.loc[pred_df["is_best_model"], "model"].unique()) == \ + {"hill_inducer"} + + +# --- assessment -------------------------------------------------------------- + +def test_assessment_rollup_and_frame(): + # A clearly-nonzero (constant ~5) curve: every point differs from zero. + x, y, ys = _flat_data(c=5.0) + flat_output, _, assess_df = cat_fit(x, y, ys, + models_to_run=["flat", "hill_inducer"]) + for key in ["omnibus_W", "omnibus_df", "omnibus_p", "n_nonzero", + "any_nonzero"]: + assert key in flat_output + assert list(assess_df.columns) == ["x", "y_est", "y_std", "z", + "sig_nonzero", "direction"] + # One row per unique observed x. + assert len(assess_df) == len(np.unique(x)) + # Curve sits far from zero -> every point significant, tiny omnibus p. + assert flat_output["n_nonzero"] == len(np.unique(x)) + assert flat_output["any_nonzero"] is True + assert flat_output["omnibus_p"] < 0.05 + + +def test_assessment_zero_curve_not_significant(): + x, y, ys = _zero_data() + flat_output, _, _ = cat_fit(x, y, ys, + models_to_run=["flat", "hill_inducer"]) + # A curve sitting on zero (fitted baseline exactly 0) is not distinguishable. + assert flat_output["n_nonzero"] == 0 + assert flat_output["omnibus_p"] > 0.05 + + +# --- insufficient data ------------------------------------------------------- def test_insufficient_data(): - """Test behavior when insufficient data is provided.""" x = np.array([1.0]) y = np.array([2.0]) - y_std = np.array([0.1]) - - # We pass x_pred explicit to avoid needing xfill mock just for this - flat_output, pred_df = cat_fit(x, y, y_std, x_pred=np.array([1.0, 2.0]), models_to_run=["linear"]) - - assert flat_output['status'] == "missing" - assert flat_output['best_model'] == "None" - assert np.isnan(flat_output['best_model_R2']) - - # Check that model entries exist and serve nans - assert "R2|linear" in flat_output + ys = np.array([0.1]) + flat_output, pred_df, assess_df = cat_fit( + x, y, ys, x_pred=np.array([1.0, 2.0]), models_to_run=["linear"] + ) + assert flat_output["status"] == "missing" + assert flat_output["best_model"] == "None" assert np.isnan(flat_output["R2|linear"]) - - # Check pred_df - assert len(pred_df) == 2 # 2 points in x_pred * 1 model - assert np.all(np.isnan(pred_df['y'])) - -@patch("tfscreen.analysis.cat_response.cat_fit.MODEL_LIBRARY") -@patch("tfscreen.analysis.cat_response.cat_fit.run_least_squares") -@patch("tfscreen.analysis.cat_response.cat_fit.predict_with_error") -def test_fit_linear_success(mock_predict, mock_run_ls, mock_library, mock_data, mock_predict_side_effect): - """Test successful fit for a linear model.""" - x, y, y_std = mock_data - mock_predict.side_effect = mock_predict_side_effect - - # Setup Mock Model - # Use imperfect fit so ss_res > 0 and AIC is finite - mock_model_func = MagicMock(return_value=y + 0.001) - mock_guess_func = MagicMock(return_value=np.array([1.0, 0.0])) - - mock_library.__getitem__.return_value = { - "model_func": mock_model_func, - "guess_func": mock_guess_func, - "param_names": ["m", "b"], - "bounds": ([-np.inf, -np.inf], [np.inf, np.inf]) - } - mock_library.keys.return_value = ["test_model"] - - # Setup run_least_squares return - fit_obj = MagicMock() - fit_obj.success = True - mock_run_ls.return_value = (np.array([2.0, 0.0]), np.array([0.1, 0.1]), np.eye(2), fit_obj) - - x_pred = np.array([1.0, 2.0, 3.0, 4.0]) - flat_output, pred_df = cat_fit(x, y, y_std, x_pred=x_pred, models_to_run=["test_model"]) - - assert flat_output['status'] == "success" - assert flat_output['best_model'] == "test_model" - # R2 logic: 1 - ss_res/ss_tot. ss_res > 0 now. R2 should be close to 1 but not 1.0 (maybe) - # y=[2,4,6,8], mean=5. ss_tot = 9+1+1+9 = 20. - # ss_res = sum(0.001^2) = 4*1e-6. small. - # R2 approx 1. - assert flat_output['R2|test_model'] > 0.99 - assert flat_output['AIC_weight|test_model'] == 1.0 - - # Check pred len - assert len(pred_df) == 4 - -@patch("tfscreen.analysis.cat_response.cat_fit.MODEL_LIBRARY") -@patch("tfscreen.analysis.cat_response.cat_fit.run_matrix_wls") -@patch("tfscreen.analysis.cat_response.cat_fit.predict_with_error") -def test_fit_matrix_wls_success(mock_predict, mock_run_wls, mock_library, mock_data, mock_predict_side_effect): - """Test successful fit for a model using matrix WLS path.""" - x, y, y_std = mock_data - mock_predict.side_effect = mock_predict_side_effect - - mock_model_func = MagicMock(return_value=y) - # 2D return for guess func - mock_guess_func = MagicMock(return_value=np.zeros((len(x), 2))) - - mock_library.__getitem__.return_value = { - "model_func": mock_model_func, - "guess_func": mock_guess_func, - "param_names": ["p1", "p2"], - "bounds": ([-np.inf], [np.inf]) - } - - mock_run_wls.return_value = (np.array([1.0, 1.0]), np.array([0.1, 0.1]), np.eye(2), None) - - x_pred = np.array([1.0]) # tiny pred - flat_output, pred_df = cat_fit(x, y, y_std, x_pred=x_pred, models_to_run=["test_matrix_model"]) - - assert flat_output['status'] == "success" - mock_run_wls.assert_called_once() - assert len(pred_df) == 1 - -@patch("tfscreen.analysis.cat_response.cat_fit.MODEL_LIBRARY") -@patch("tfscreen.analysis.cat_response.cat_fit.run_least_squares") -def test_fit_failure(mock_run_ls, mock_library, mock_data): - """Test handling of fit failure.""" - x, y, y_std = mock_data - - mock_guess_func = MagicMock(return_value=np.array([1.0])) - mock_library.__getitem__.return_value = { - "model_func": MagicMock(), - "guess_func": mock_guess_func, - "param_names": ["p1"], - "bounds": ([-np.inf], [np.inf]) - } - - fit_obj = MagicMock() - fit_obj.success = False - fit_obj.message = "Failed" - mock_run_ls.return_value = (None, None, None, fit_obj) - - flat_output, pred_df = cat_fit(x, y, y_std, x_pred=x, models_to_run=["test_fail_model"], verbose=True) - - assert flat_output['status'] == "failure" - assert flat_output['best_model'] == "None" - assert np.isnan(flat_output['R2|test_fail_model']) - -@patch("tfscreen.analysis.cat_response.cat_fit.MODEL_LIBRARY") -@patch("tfscreen.analysis.cat_response.cat_fit.run_least_squares") -@patch("tfscreen.analysis.cat_response.cat_fit.predict_with_error") -def test_multiple_models_selection(mock_predict, mock_run_ls, mock_library, mock_data, mock_predict_side_effect): - """Test running multiple models and selecting the best one.""" - x, y, y_std = mock_data - mock_predict.side_effect = mock_predict_side_effect - - # Model A: Good fit - mock_func_A = MagicMock(return_value=y + 0.001) - # Model B: Bad fit - mock_func_B = MagicMock(return_value=y + 10) - - mock_guess = MagicMock(return_value=np.array([1.0])) - - library_dict = { - "model_A": { - "model_func": mock_func_A, - "guess_func": mock_guess, - "param_names": ["pA"], - "bounds": ([-np.inf], [np.inf]) - }, - "model_B": { - "model_func": mock_func_B, - "guess_func": mock_guess, - "param_names": ["pB"], - "bounds": ([-np.inf], [np.inf]) - } - } - mock_library.__getitem__.side_effect = lambda k: library_dict[k] - - fit_obj = MagicMock() - fit_obj.success = True - mock_run_ls.return_value = (np.array([1.0]), np.array([0.1]), np.eye(1), fit_obj) - - flat_output, pred_df = cat_fit(x, y, y_std, x_pred=x, models_to_run=["model_A", "model_B"]) - - assert flat_output['best_model'] == "model_A" - assert flat_output['AIC_weight|model_A'] > flat_output['AIC_weight|model_B'] - -def test_x_pred_none(mock_data): - """Test x_pred default generation behavior (using xfill).""" - x, y, y_std = mock_data - - with patch("tfscreen.analysis.cat_response.cat_fit.xfill") as mock_xfill: - with patch("tfscreen.analysis.cat_response.cat_fit.MODEL_LIBRARY") as mock_lib: - # We need to run at least one model to avoid empty summary_df issues - # (though strictly user shouldn't pass empty model list, test handles typical case) - # Actually if models_to_run is [], code does not crash, just returns empty/nan stuff? - # Let's check code: - # if models_to_run is None -> keys(). If keys empty -> empty list. - # loop doesn't run. summary_results empty. - # valid_aics empty. - # code handles valid_aics.empty. - # BUT: summary_df['success'] accessed below. - # summary_df = pd.DataFrame(summary_results). - # If summary_results [], summary_df empty. summary_df['success'] raises KeyError? - # YES. - # So models_to_run cannot be empty if we want success. - # Wait, `predict_with_error` usually mocked. - - # Update: let's mock xfill to return something usable, and run a dummy model. - mock_xfill.return_value = x # simple return - - mock_guess = MagicMock(return_value=np.array([1.0])) - mock_lib.__getitem__.return_value = { - "model_func": MagicMock(return_value=y), - "guess_func": mock_guess, - "param_names": ["p"], - "bounds": ([-np.inf], [np.inf]) - } - mock_lib.keys.return_value = ["test"] - - with patch("tfscreen.analysis.cat_response.cat_fit.run_least_squares") as mock_ls: - mock_ls.return_value = (np.array([1.]), np.array([1.]), np.eye(1), MagicMock(success=True)) - with patch("tfscreen.analysis.cat_response.cat_fit.predict_with_error") as mock_pred: - mock_pred.return_value = (x, x) - - cat_fit(x, y, y_std, x_pred=None, models_to_run=["test"]) - - mock_xfill.assert_called_once() - -@patch("tfscreen.analysis.cat_response.cat_fit.MODEL_LIBRARY") -@patch("tfscreen.analysis.cat_response.cat_fit.run_least_squares") -@patch("tfscreen.analysis.cat_response.cat_fit.predict_with_error") -def test_sanitize_inputs(mock_predict, mock_ls, mock_lib, mock_predict_side_effect): - """Test that NaNs/Infs are filtered out.""" - x = np.array([1.0, 2.0, np.nan, 4.0]) - y = np.array([1.0, 2.0, 3.0, np.inf]) - y_std = np.array([0.1, 0.1, 0.1, 0.1]) - - mock_predict.side_effect = mock_predict_side_effect - - mock_guess = MagicMock(return_value=np.array([1.0])) - mock_lib.__getitem__.return_value = { - "model_func": MagicMock(return_value=np.array([0.,0.])), # match filtered size 2 - "guess_func": mock_guess, - "param_names": ["p"], - "bounds": ([-np.inf], [np.inf]) - } - - mock_ls.return_value = (np.array([1.]), np.array([0.1]), np.eye(1), MagicMock(success=True)) - - # x_pred passed explicit to avoid issues - cat_fit(x, y, y_std, x_pred=np.array([1.0]), models_to_run=["test"]) - - # Check call args to guess_func - args, _ = mock_guess.call_args - x_filtered, y_filtered = args - - assert len(x_filtered) == 2 - assert np.allclose(x_filtered, [1.0, 2.0]) + assert np.isnan(flat_output["omnibus_p"]) + # Best-only predictions: nothing to predict, empty frames. + assert len(pred_df) == 0 + assert list(pred_df.columns) == ["model", "x", "y", "y_std", + "is_best_model"] + assert len(assess_df) == 0 + + +def test_all_models_fail_returns_none(): + """If every model fails, status=failure, empty pred/assess, nan rollups.""" + # Two points but ask only for a 4-param model: too few for a fit to + # converge meaningfully, but more directly, force failure via degenerate y. + x = np.array([1.0, 2.0]) + y = np.array([np.nan, np.nan]) # all filtered -> insufficient + ys = np.array([0.1, 0.1]) + flat_output, pred_df, assess_df = cat_fit(x, y, ys, + models_to_run=["linear"]) + # All-NaN y -> filtered to zero points -> missing path. + assert flat_output["status"] == "missing" + assert len(assess_df) == 0 + + +# --- input sanitizing -------------------------------------------------------- + +def test_sanitize_filters_nonfinite(): + x = np.array([0.0, 1.0, np.nan, 10.0, 30.0, 100.0]) + y = np.array([1.0, 3.0, 5.0, 21.0, np.inf, 201.0]) + ys = np.full_like(x, 0.05) + # Two points dropped (nan x, inf y) -> 4 usable, linear still fittable. + flat_output, _, assess_df = cat_fit(x, y, ys, + models_to_run=["flat", "linear"]) + assert flat_output["status"] in ("success", "partial") + assert len(assess_df) == 4 diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 5c48db8..93d4820 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -1,6 +1,7 @@ """ Tests for the generic cat_response core: grouping, column selection, validation, -prediction tagging, and serial/parallel equivalence. +prediction tagging, the post-hoc assessment pass, and serial/parallel +equivalence. """ import numpy as np import pandas as pd @@ -24,9 +25,12 @@ # --- helpers ----------------------------------------------------------------- -def _flat(best="flat"): - """A minimal flat cat_fit result dict.""" - return {"status": "success", "best_model": best} +def _flat(best="flat", **extra): + """A minimal flat cat_fit result dict with the assessment rollups.""" + d = {"status": "success", "best_model": best, + "omnibus_p": 0.5, "n_nonzero": 0, "any_nonzero": False} + d.update(extra) + return d def _pred(n=2): @@ -40,11 +44,25 @@ def _pred(n=2): }) -def _capturing_fit(store): +def _assess(n=2, y_est=0.0, y_std=0.1): + """A minimal cat_fit per-point assessment frame.""" + return pd.DataFrame({ + "x": np.arange(n, dtype=float), + "y_est": np.full(n, y_est, dtype=float), + "y_std": np.full(n, y_std, dtype=float), + "z": np.full(n, y_est / y_std if y_std else 0.0), + "sig_nonzero": np.zeros(n, dtype=bool), + "direction": np.zeros(n, dtype=int), + }) + + +def _capturing_fit(store, **fit_kwargs): """A fake cat_fit that records the (x, y, y_std) it was handed per call.""" - def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): - store.append({"x": list(x), "y": list(y), "y_std": list(y_std)}) - return _flat(), _pred(len(x)) + def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, + best_only=True, alpha=0.05, verbose=False): + store.append({"x": list(x), "y": list(y), "y_std": list(y_std), + "best_only": best_only, "alpha": alpha}) + return _flat(**fit_kwargs), _pred(len(x)), _assess(len(np.unique(x))) return fake_fit @@ -68,8 +86,8 @@ def test_groups_by_genotype_only_by_default(self): store = [] with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit(store)): - results, _ = cat_response(_basic_df(), x_obs="titrant_conc", - y_obs="theta", y_std="theta_std") + results, _, _, _ = cat_response(_basic_df(), x_obs="titrant_conc", + y_obs="theta", y_std="theta_std") # 2 genotypes -> 2 groups (titrant_name is ignored without group_by). assert len(results) == 2 assert set(results["genotype"]) == {"wt", "m1"} @@ -79,9 +97,9 @@ def test_group_by_adds_a_column(self): store = [] with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit(store)): - results, _ = cat_response(_basic_df(), x_obs="titrant_conc", - y_obs="theta", y_std="theta_std", - group_by=["titrant_name"]) + results, _, _, _ = cat_response(_basic_df(), x_obs="titrant_conc", + y_obs="theta", y_std="theta_std", + group_by=["titrant_name"]) # 2 genotypes x 2 titrants -> 4 groups. assert len(results) == 4 assert set(results.columns[:2]) == {"genotype", "titrant_name"} @@ -95,8 +113,8 @@ def test_works_without_titrant_name_column(self): store = [] with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit(store)): - results, _ = cat_response(df, x_obs="titrant_conc", - y_obs="theta", y_std="theta_std") + results, _, _, _ = cat_response(df, x_obs="titrant_conc", + y_obs="theta", y_std="theta_std") assert len(results) == 2 assert set(results["genotype"]) == {"wt", "m1"} @@ -127,6 +145,16 @@ def test_none_y_std_uses_uniform_weights(self): # Uniform weights: every y_std handed to cat_fit is 1.0. assert all(all(s == 1.0 for s in call["y_std"]) for call in store) + def test_best_only_and_alpha_threaded_to_fitter(self): + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", best_only=False, alpha=0.01) + assert all(call["best_only"] is False for call in store) + assert all(call["alpha"] == 0.01 for call in store) + # --- validation -------------------------------------------------------------- @@ -154,9 +182,9 @@ class TestPredictions: def test_predictions_tagged_with_group_keys(self): with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit([])): - _, preds = cat_response(_basic_df(), x_obs="titrant_conc", - y_obs="theta", y_std="theta_std", - group_by=["titrant_name"]) + _, preds, _, _ = cat_response(_basic_df(), x_obs="titrant_conc", + y_obs="theta", y_std="theta_std", + group_by=["titrant_name"]) # Group keys come first, then the cat_fit prediction columns. assert list(preds.columns[:2]) == ["genotype", "titrant_name"] for col in ["model", "x", "y", "y_std", "is_best_model"]: @@ -165,6 +193,80 @@ def test_predictions_tagged_with_group_keys(self): assert not preds["genotype"].isna().any() +# --- post-hoc assessment pass ------------------------------------------------ + +class TestAssessmentPass: + + def test_assessment_tagged_and_has_equiv_zero(self): + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([])): + _, _, assess, delta = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", group_by=["titrant_name"]) + assert list(assess.columns[:2]) == ["genotype", "titrant_name"] + assert "equiv_zero" in assess.columns + assert np.isfinite(delta) + + def test_delta_defaults_to_median_times_c(self): + # Fake assessment always reports y_std=0.1 -> median=0.1 -> delta=0.2. + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([])): + _, _, _, delta = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta_c=2.0) + assert delta == pytest.approx(0.2) + + def test_explicit_delta_is_used(self): + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([])): + _, _, _, delta = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta=0.75) + assert delta == 0.75 + + def test_response_class_confident_zero(self): + # y_est=0, y_std tiny -> CI well inside delta -> all_equiv_zero -> and + # omnibus_p high (0.5) -> confident_zero. + fit = _capturing_fit([]) + + def fake(*a, **k): + flat, pred, _ = fit(*a, **k) + x = a[0] + return flat, pred, _assess(len(np.unique(x)), y_est=0.0, + y_std=1e-4) + with patch.object(cat_response_mod, "cat_fit", side_effect=fake): + results, _, _, _ = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta=0.5) + assert set(results["response_class"]) == {"confident_zero"} + + def test_equivalence_wins_over_significance(self): + # Significant omnibus (tiny p) but every point inside the ROPE + # (y_est=0, tiny std) -> practically zero should win -> confident_zero. + fit = _capturing_fit([]) + + def fake(*a, **k): + flat, pred, _ = fit(*a, **k) + flat["omnibus_p"] = 1e-9 + x = a[0] + return flat, pred, _assess(len(np.unique(x)), y_est=0.0, + y_std=1e-4) + with patch.object(cat_response_mod, "cat_fit", side_effect=fake): + results, _, _, _ = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta=0.5) + assert set(results["response_class"]) == {"confident_zero"} + + def test_response_class_real_from_low_q(self): + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([], omnibus_p=1e-8)): + results, _, _, _ = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta=1e-6) + assert set(results["response_class"]) == {"real"} + assert (results["omnibus_q"] < 0.05).all() + + # --- chunk helper ------------------------------------------------------------ class TestIterChunks: @@ -196,13 +298,15 @@ def _many_genotype_df(self, n_geno): def test_parallel_matches_serial(self): df = self._many_genotype_df(7) with patch.object(cat_response_mod, "_CHUNK_SIZE", 2): - serial, serial_pred = cat_response( + serial, serial_pred, serial_assess, serial_delta = cat_response( df, x_obs="titrant_conc", y_obs="theta", y_std="theta_std", num_workers=1) - parallel, parallel_pred = cat_response( - df, x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", num_workers=2) + parallel, parallel_pred, parallel_assess, parallel_delta = \ + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", num_workers=2) assert list(serial["genotype"]) == [f"g{i}" for i in range(7)] + assert serial_delta == pytest.approx(parallel_delta) pd.testing.assert_frame_equal(serial, parallel) pd.testing.assert_frame_equal(serial_pred, parallel_pred) + pd.testing.assert_frame_equal(serial_assess, parallel_assess) diff --git a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py index 146fc67..015496e 100644 --- a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py @@ -68,6 +68,26 @@ def test_writes_main_permodel_and_predictions(self, tmp_path): preds = pd.read_csv(f"{out_prefix}_predictions.csv") assert "genotype" in preds.columns assert {"model", "x", "y", "is_best_model"}.issubset(preds.columns) + # best_only default: predictions restricted to each group's best model. + assert preds["is_best_model"].all() + + assess = pd.read_csv(f"{out_prefix}_assessment.csv") + assert "genotype" in assess.columns + assert {"x", "y_est", "y_std", "z", "sig_nonzero", "direction", + "equiv_zero"}.issubset(assess.columns) + # Rollups landed on the main table. + assert {"omnibus_p", "omnibus_q", "n_nonzero", + "response_class"}.issubset(main.columns) + + def test_write_all_predictions_flag(self, tmp_path): + data = _write(tmp_path, _theta_df()) + out_prefix = str(tmp_path / "out") + cat_response(data, x_obs="titrant_conc", y_obs="q0.5", + out_prefix=out_prefix, models=["flat", "linear"], + write_all_predictions=True, num_workers=1) + preds = pd.read_csv(f"{out_prefix}_predictions.csv") + # Both models present when all predictions are written. + assert set(preds["model"].unique()) == {"flat", "linear"} class TestSigmaFallback: @@ -79,7 +99,8 @@ def fake_core(df, **kwargs): captured["y_std"] = kwargs["y_std"] captured["sigma_vals"] = list(df[kwargs["y_std"]]) empty = pd.DataFrame({"genotype": [], "best_model": []}) - return empty, pd.DataFrame({"genotype": []}) + return empty, pd.DataFrame({"genotype": []}), \ + pd.DataFrame({"genotype": []}), 0.1 data = _write(tmp_path, _theta_df(with_quantiles=True)) with patch.object(cat_response_cli, "_cat_response", @@ -98,7 +119,8 @@ def test_explicit_y_std_takes_precedence(self, tmp_path): def fake_core(df, **kwargs): captured["y_std"] = kwargs["y_std"] empty = pd.DataFrame({"genotype": [], "best_model": []}) - return empty, pd.DataFrame({"genotype": []}) + return empty, pd.DataFrame({"genotype": []}), \ + pd.DataFrame({"genotype": []}), 0.1 data = _write(tmp_path, _theta_df(with_std=True, with_quantiles=True)) with patch.object(cat_response_cli, "_cat_response", diff --git a/tests/tfscreen/analysis/test_compare_theta.py b/tests/tfscreen/analysis/test_compare_theta.py index 17096c3..f9e7f69 100644 --- a/tests/tfscreen/analysis/test_compare_theta.py +++ b/tests/tfscreen/analysis/test_compare_theta.py @@ -8,12 +8,41 @@ from tfscreen.analysis.compare_theta import ( compare_theta, + aggregate_theta, stability_crosstabs, _detect_keys, _extract_run, _assign_tier, + _mixture_quantiles, ) +# Standard quantile ladder used by the aggregate builders/tests. +_LEVELS = [0.025, 0.159, 0.5, 0.841, 0.975] + + +def make_estimate_ladder(theta_by_geno, concs=(0.0, 1.0), sigma=0.05, + titrant_name=None, levels=_LEVELS): + """ + Build an estimate table with a full (Gaussian) quantile ladder. + + Each (genotype, conc) gets quantiles ``q0.5 = theta`` and the other levels + placed at ``theta + z(level) * sigma`` (Normal), clamped to [0, 1]. + """ + from scipy.stats import norm + zs = {lvl: norm.ppf(lvl) for lvl in levels} + rows = [] + for geno, thetas in theta_by_geno.items(): + for j, conc in enumerate(concs): + th = thetas[j] + sg = sigma[geno][j] if isinstance(sigma, dict) else sigma + row = {"genotype": geno, "titrant_conc": conc} + if titrant_name is not None: + row["titrant_name"] = titrant_name + for lvl in levels: + row[f"q{lvl}"] = float(np.clip(th + zs[lvl] * sg, 0.0, 1.0)) + rows.append(row) + return pd.DataFrame(rows) + # --- Fixtures / builders ----------------------------------------------------- @@ -327,3 +356,117 @@ def test_result_sorted_by_rms_sd(): r2 = make_estimate({"a": [0.5, 0.55], "b": [0.5, 0.9]}) result = compare_theta([r1, r2]) assert list(result["rms_sd"]) == sorted(result["rms_sd"]) + + +# --- aggregate_theta --------------------------------------------------------- + +def agg_row(agg, genotype, conc): + sub = agg[(agg["genotype"] == genotype) & (agg["titrant_conc"] == conc)] + assert len(sub) == 1 + return sub.iloc[0] + + +def test_mixture_quantiles_identical_runs_return_input(): + # Mixing identical distributions returns that distribution. + from scipy.stats import norm + probs = np.array(_LEVELS) + vals = 0.5 + norm.ppf(probs) * 0.05 + V = np.vstack([vals, vals, vals]) + out = _mixture_quantiles(V, probs) + np.testing.assert_allclose(out, vals, atol=1e-9) + + +def test_mixture_quantiles_between_spread_widens_interval(): + # Two runs, equal width but shifted medians -> the mixture is wider than + # either input (captures between-run spread). + from scipy.stats import norm + probs = np.array(_LEVELS) + z = norm.ppf(probs) + v1 = 0.4 + z * 0.05 + v2 = 0.6 + z * 0.05 + V = np.vstack([v1, v2]) + out = _mixture_quantiles(V, probs) + hi = _LEVELS.index(0.841) + lo = _LEVELS.index(0.159) + mix_width = out[hi] - out[lo] + run_width = (v1[hi] - v1[lo]) + assert mix_width > run_width + # median sits between the two run medians + med = _LEVELS.index(0.5) + assert 0.4 < out[med] < 0.6 + + +def test_mixture_quantiles_point_mass_all_equal(): + probs = np.array(_LEVELS) + V = np.full((3, probs.size), 0.0) # all runs pinned at theta=0 + out = _mixture_quantiles(V, probs) + np.testing.assert_allclose(out, 0.0) + + +def test_mixture_quantiles_point_mass_mixed_is_monotone_between(): + # Two point masses at a and b: mixture quantiles monotone and in [a, b]. + probs = np.array([0.1, 0.5, 0.9]) + a, b = 0.2, 0.8 + V = np.vstack([np.full(3, a), np.full(3, b)]) + out = _mixture_quantiles(V, probs) + assert np.all(np.diff(out) >= 0) + assert a <= out[1] <= b + + +def test_aggregate_identical_runs_matches_input(): + df = make_estimate_ladder({"wt": [0.2, 0.8], "m1": [0.5, 0.5]}) + agg = aggregate_theta([df, df.copy(), df.copy()]) + # schema: keys + ladder + n_present + for lvl in _LEVELS: + assert f"q{lvl}" in agg.columns + assert "n_present" in agg.columns + row = agg_row(agg, "wt", 0.0) + src = df[(df["genotype"] == "wt") & (df["titrant_conc"] == 0.0)].iloc[0] + for lvl in _LEVELS: + np.testing.assert_allclose(row[f"q{lvl}"], src[f"q{lvl}"], atol=1e-9) + assert row["n_present"] == 3 + + +def test_aggregate_between_variance_widens(): + r1 = make_estimate_ladder({"g": [0.4, 0.4]}, sigma=0.05) + r2 = make_estimate_ladder({"g": [0.6, 0.6]}, sigma=0.05) + agg = aggregate_theta([r1, r2]) + row = agg_row(agg, "g", 0.0) + agg_width = row["q0.841"] - row["q0.159"] + single_width = ( + make_estimate_ladder({"g": [0.4, 0.4]}, sigma=0.05).iloc[0]["q0.841"] + - make_estimate_ladder({"g": [0.4, 0.4]}, sigma=0.05).iloc[0]["q0.159"] + ) + assert agg_width > single_width + # symmetric shift -> median near the mean of the two medians + np.testing.assert_allclose(row["q0.5"], 0.5, atol=0.05) + + +def test_aggregate_coverage_reported(): + base = {"a": [0.2, 0.8]} + r1 = make_estimate_ladder(base) + r2 = make_estimate_ladder(base) + r3 = make_estimate_ladder({"a": [0.2, 0.8], "b": [0.5, 0.5]}) + agg = aggregate_theta([r1, r2, r3]) + assert agg_row(agg, "a", 0.0)["n_present"] == 3 + assert agg_row(agg, "b", 0.0)["n_present"] == 1 + + +def test_aggregate_with_titrant_name(): + r1 = make_estimate_ladder({"g": [0.2, 0.8]}, titrant_name="iptg") + agg = aggregate_theta([r1, r1.copy()]) + assert "titrant_name" in agg.columns + assert (agg["titrant_name"] == "iptg").all() + + +def test_aggregate_requires_two_runs(): + r1 = make_estimate_ladder({"g": [0.2, 0.8]}) + with pytest.raises(ValueError, match="at least 2"): + aggregate_theta([r1]) + + +def test_aggregate_key_mismatch_raises(): + r1 = make_estimate_ladder({"g": [0.2, 0.8]}, titrant_name="iptg") + r2 = make_estimate_ladder({"g": [0.2, 0.8]}) + with pytest.raises(ValueError, match="key columns"): + aggregate_theta([r1, r2]) diff --git a/tests/tfscreen/mle/curve_models/test_guesses.py b/tests/tfscreen/mle/curve_models/test_guesses.py index 70b3d43..b73aead 100644 --- a/tests/tfscreen/mle/curve_models/test_guesses.py +++ b/tests/tfscreen/mle/curve_models/test_guesses.py @@ -3,10 +3,11 @@ import numpy as np from tfscreen.mle.curve_models import guesses +from tfscreen.mle.curve_models.models import model_linear +from tfscreen.mle import run_matrix_wls @pytest.mark.parametrize("guess_func, expected_cols", [ (guesses.guess_flat, 1), - (guesses.guess_linear, 2), (guesses.guess_poly_2nd, 3), (guesses.guess_poly_3rd, 4), (guesses.guess_poly_4th, 5), @@ -15,11 +16,60 @@ def test_polynomial_guesses(guess_func, expected_cols): x = np.linspace(0, 10, 10) y = np.ones_like(x) - + design_matrix = guess_func(x, y) assert design_matrix.shape == (10, expected_cols) assert np.all(design_matrix[:, 0] == 1) # Intercept column is first (reversed vander) + +def test_guess_linear_column_order(): + """ + guess_linear must return columns [x, 1] (NOT reversed), so the WLS + solution comes back as [m, b] to match model_linear and the "linear" + param_names of ['m', 'b']. This is the deliberate exception to the + reversed-vander poly guesses. + """ + x = np.linspace(0, 10, 10) + y = np.ones_like(x) + + design_matrix = guesses.guess_linear(x, y) + assert design_matrix.shape == (10, 2) + # First column is x, second column is the constant 1 + assert np.allclose(design_matrix[:, 0], x) + assert np.all(design_matrix[:, 1] == 1) + + +def test_linear_fit_recovers_slope_intercept(): + """ + End-to-end: fitting perfect data y = m*x + b via the linear model's design + matrix must recover [m, b] in the order model_linear expects, giving R2=1. + Guards against the param-order mismatch between guess_linear and + model_linear. + """ + m_true, b_true = 2.0, 1.0 + x = np.linspace(0, 10, 25) + y = m_true * x + b_true + y_std = np.ones_like(y) + + design_matrix = guesses.guess_linear(x, y) + params, std_err, cov, _ = run_matrix_wls(design_matrix, y, 1.0 / y_std) + + # Params come back as [m, b], matching model_linear and param_names. + assert np.isclose(params[0], m_true) + assert np.isclose(params[1], b_true) + + # model_linear(params, x) reproduces the data. + y_fit = model_linear(params, x) + assert np.allclose(y_fit, y) + + # Weighted R2 is essentially 1 on perfect linear data. + w = 1.0 / (y_std ** 2) + chi2 = float(np.sum(((y - y_fit) / y_std) ** 2)) + y_wmean = np.average(y, weights=w) + ss_tot = float(np.sum(w * (y - y_wmean) ** 2)) + r2 = 1 - chi2 / ss_tot + assert np.isclose(r2, 1.0) + def test_guess_biphasic_dip_edge_case(): # Dip at the end x = np.array([0, 1, 2]) diff --git a/tests/tfscreen/mle/test_predict_with_error.py b/tests/tfscreen/mle/test_predict_with_error.py index bb23941..2cff8af 100644 --- a/tests/tfscreen/mle/test_predict_with_error.py +++ b/tests/tfscreen/mle/test_predict_with_error.py @@ -64,6 +64,46 @@ def constant_model(params): def test_numerical_derivative_epsilon(): # Test that epsilon is used # Use a non-linear model where epsilon matters slightly compared to analytical? - # actually, with linear model, numerical deriv is exact. + # actually, with linear model, numerical deriv is exact. # y = x^2. dy/dx = 2x. pass + + +def test_full_cov_diagonal_matches_se(): + """full_cov returns the (M, M) prediction covariance; its diagonal is se^2.""" + x = np.array([1.0, 2.0, 3.0]) + params = np.array([2.0, 1.0]) + cov = np.array([[0.1, 0.0], + [0.0, 0.2]]) + + val, se, pred_cov = predict_with_error(linear_model, params, cov, + args=(x,), full_cov=True) + + assert pred_cov.shape == (3, 3) + assert np.allclose(np.diag(pred_cov), se ** 2) + + # Off-diagonal: Cov(y_i, y_j) = x_i*x_j*Var(m) + Var(b). + expected = np.outer(x, x) * 0.1 + 0.2 + assert np.allclose(pred_cov, expected) + + +def test_full_cov_nan_cov(): + """Invalid param covariance -> NaN-filled (M, M) prediction covariance.""" + x = np.array([1.0, 2.0]) + params = np.array([2.0, 1.0]) + cov = np.array([[np.nan, 0.0], [0.0, 0.2]]) + + val, se, pred_cov = predict_with_error(linear_model, params, cov, + args=(x,), full_cov=True) + + assert pred_cov.shape == (2, 2) + assert np.all(np.isnan(pred_cov)) + assert np.all(np.isnan(se)) + + +def test_full_cov_default_off(): + """Without full_cov, the return is still a 2-tuple (back-compat).""" + x = np.array([1.0, 2.0]) + out = predict_with_error(linear_model, np.array([2.0, 1.0]), + np.eye(2), args=(x,)) + assert len(out) == 2 From b75b6183ce953163804085ae3a1bef01219762c1 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 19 Jul 2026 07:33:08 -0400 Subject: [PATCH 12/26] more sophisticated cat response code --- CLAUDE.md | 2 +- docs/badges/coverage-badge.svg | 1 - docs/badges/tests-badge.svg | 125 +++++++++++++++++- reports/flake.txt | 27 ++-- reports/junit/junit.xml | 2 +- .../analysis/cat_response/cat_assess.py | 9 +- src/tfscreen/analysis/cat_response/cat_fit.py | 56 +++++--- .../analysis/cat_response/cat_response.py | 20 +-- src/tfscreen/analysis/extract_epistasis.py | 8 ++ .../analysis/scripts/cat_response_cli.py | 24 ++-- .../analysis/scripts/extract_epistasis_cli.py | 19 ++- src/tfscreen/plot/cat_fits.py | 12 +- src/tfscreen/util/__init__.py | 4 + src/tfscreen/util/dataframe/__init__.py | 4 + .../util/dataframe/resolve_obs_columns.py | 87 ++++++++++++ .../analysis/cat_response/test_cat_assess.py | 4 +- .../analysis/cat_response/test_cat_fit.py | 8 +- .../cat_response/test_cat_response.py | 31 +++-- .../analysis/scripts/test_cat_response_cli.py | 32 ++++- .../scripts/test_extract_epistasis_cli.py | 41 +++++- tests/tfscreen/plot/test_cat_fits.py | 16 +-- .../dataframe/test_resolve_obs_columns.py | 107 +++++++++++++++ 22 files changed, 541 insertions(+), 98 deletions(-) create mode 100644 src/tfscreen/util/dataframe/resolve_obs_columns.py create mode 100644 tests/tfscreen/util/dataframe/test_resolve_obs_columns.py diff --git a/CLAUDE.md b/CLAUDE.md index 67246be..f481a19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -287,7 +287,7 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( `equiv_zero` (a point's whole CI ⊂ `[-delta, delta]`) is the confidence axis that separates "confidently flat" from "too noisy to tell". `delta` defaults to `delta_c * median(predicted y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits — hence the post-hoc pass); pass `--delta` for a fixed biological region. `response_class` is 3-valued with **equivalence taking precedence over significance** (a tiny-but-significant offset inside the ROPE is still practically zero): `confident_zero` (all points equiv) → else `real` (omnibus_q < alpha) → else `indeterminate` (the "flat because error is high" bucket to exclude downstream). -Outputs: rollups (`omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; per-point flags in `{prefix}_assessment.csv`; `{prefix}_predictions.csv` holds only each group's **best** model (`best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. +Outputs: rollups (`omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. ## YAML Standards diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index f23c57b..e69de29 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +0,0 @@ -coverage: 96.14%coverage96.14% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 3fc6039..61ce646 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1,124 @@ -tests: 4152tests4152 \ No newline at end of file + + + + + + + +shields.io | 520: Web server is returning an unknown error + + + + + + + + +
+
+
+

+ Web server is returning an unknown error + Error code 520 +

+
+ Visit cloudflare.com for more information. +
+
2026-07-19 11:31:36 UTC
+
+
+
+
+
+
+ + + + +
+ You +

+ + Browser + +

+ + Working + +
+
+ + Seattle +

+ + Cloudflare + +

+ + Working + +
+
+
+ + + + +
+ img.shields.io +

+ + Host + +

+ + Error + +
+
+
+
+ +
+
+
+

What happened?

+

There is an unknown connection issue between Cloudflare and the origin web server. As a result, the web page can not be displayed.

+
+
+

What can I do?

+ +

If you are a visitor of this website:

+

Please try again in a few minutes.

+ +

If you are the owner of this website:

+

There is an issue between Cloudflare's cache and your origin web server. Cloudflare monitors for these errors and automatically investigates the cause. To help support the investigation, you can pull the corresponding error log from your web server and submit it our support team. Please include the Ray ID (which is at the bottom of this error page). Additional troubleshooting resources.

+
+
+
+ + + +
+
+ + \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 482501a..0942c27 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -6,13 +6,13 @@ ./src/tfscreen/analysis/__init__.py:13:1: W391 blank line at end of file ./src/tfscreen/analysis/cat_response/__init__.py:2:77: W291 trailing whitespace ./src/tfscreen/analysis/cat_response/__init__.py:3:55: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:29:1: C901 'cat_fit' is too complex (25) -./src/tfscreen/analysis/cat_response/cat_fit.py:154:72: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:154:74: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:226:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./src/tfscreen/analysis/cat_response/cat_fit.py:228:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./src/tfscreen/analysis/cat_response/cat_fit.py:42:1: C901 'cat_fit' is too complex (25) +./src/tfscreen/analysis/cat_response/cat_fit.py:169:72: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:169:74: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:241:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./src/tfscreen/analysis/cat_response/cat_fit.py:243:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' ./src/tfscreen/analysis/cat_response/cat_response.py:60:1: C901 'cat_response' is too complex (15) -./src/tfscreen/analysis/cat_response/cat_response.py:246:45: E127 continuation line over-indented for visual indent +./src/tfscreen/analysis/cat_response/cat_response.py:250:45: E127 continuation line over-indented for visual indent ./src/tfscreen/analysis/compare_theta.py:200:1: C901 'compare_theta' is too complex (12) ./src/tfscreen/analysis/compare_theta.py:632:1: C901 'aggregate_theta' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) @@ -23,7 +23,7 @@ ./src/tfscreen/analysis/extract_epistasis.py:100:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:113:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:125:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:129:1: C901 'extract_epistasis' is too complex (13) +./src/tfscreen/analysis/extract_epistasis.py:129:1: C901 'extract_epistasis' is too complex (15) ./src/tfscreen/analysis/extract_epistasis.py:133:37: E252 missing whitespace around parameter equals ./src/tfscreen/analysis/extract_epistasis.py:133:38: E252 missing whitespace around parameter equals ./src/tfscreen/analysis/extract_epistasis.py:183:36: W291 trailing whitespace @@ -530,9 +530,9 @@ ./src/tfscreen/plot/cat_fits.py:131:31: E231 missing whitespace after ',' ./src/tfscreen/plot/cat_fits.py:137:53: E231 missing whitespace after ',' ./src/tfscreen/plot/cat_fits.py:147:34: E231 missing whitespace after ',' -./src/tfscreen/plot/cat_fits.py:147:48: E231 missing whitespace after ',' +./src/tfscreen/plot/cat_fits.py:147:54: E231 missing whitespace after ',' ./src/tfscreen/plot/cat_fits.py:163:34: E231 missing whitespace after ',' -./src/tfscreen/plot/cat_fits.py:163:48: E231 missing whitespace after ',' +./src/tfscreen/plot/cat_fits.py:163:54: E231 missing whitespace after ',' ./src/tfscreen/plot/cat_fits.py:171:26: E701 multiple statements on one line (colon) ./src/tfscreen/plot/cat_fits.py:172:26: E701 multiple statements on one line (colon) ./src/tfscreen/plot/cat_fits.py:173:12: E701 multiple statements on one line (colon) @@ -3661,7 +3661,7 @@ ./src/tfscreen/tfmodel/tensors/tensor_manager.py:512:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/tensors/tensor_manager.py:517:1: W293 blank line contains whitespace ./src/tfscreen/tfmodel/tensors/tensor_manager.py:528:1: W391 blank line at end of file -./src/tfscreen/util/__init__.py:75:2: W292 no newline at end of file +./src/tfscreen/util/__init__.py:79:2: W292 no newline at end of file ./src/tfscreen/util/cli/__init__.py:16:1: W391 blank line at end of file ./src/tfscreen/util/cli/generalized_main.py:1:11: W291 trailing whitespace ./src/tfscreen/util/cli/generalized_main.py:5:1: C901 'generalized_main' is too complex (15) @@ -3687,7 +3687,7 @@ ./src/tfscreen/util/cli/generalized_main.py:104:59: E231 missing whitespace after ',' ./src/tfscreen/util/cli/generalized_main.py:104:75: E231 missing whitespace after ',' ./src/tfscreen/util/cli/generalized_main.py:105:1: W293 blank line contains whitespace -./src/tfscreen/util/dataframe/__init__.py:28:2: W292 no newline at end of file +./src/tfscreen/util/dataframe/__init__.py:32:2: W292 no newline at end of file ./src/tfscreen/util/dataframe/add_group_columns.py:3:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/util/dataframe/add_group_columns.py:8:79: W291 trailing whitespace ./src/tfscreen/util/dataframe/add_group_columns.py:9:1: W293 blank line contains whitespace @@ -3999,7 +3999,6 @@ ./tests/smoke-tests/test_run_growth_analysis_smoke.py:45:1: W293 blank line contains whitespace ./tests/smoke-tests/test_run_growth_analysis_smoke.py:48:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/cat_response/test_cat_assess.py:7:1: F401 'scipy.stats.norm' imported but unused -./tests/tfscreen/analysis/cat_response/test_cat_fit.py:7:1: F401 'pytest' imported but unused ./tests/tfscreen/analysis/test_extract_epistasis.py:16:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:22:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_extract_epistasis.py:34:35: W291 trailing whitespace @@ -7513,7 +7512,7 @@ 1 E722 do not use bare 'except' 1 E731 do not assign a lambda expression, use a def 21 E741 ambiguous variable name 'l' -231 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used @@ -7521,4 +7520,4 @@ 83 W292 no newline at end of file 1505 W293 blank line contains whitespace 29 W391 blank line at end of file -7476 +7475 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 6480747..2da91c8 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py index cbbe406..1cb91b7 100644 --- a/src/tfscreen/analysis/cat_response/cat_assess.py +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -56,8 +56,9 @@ def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): Returns ------- per_point : dict - Arrays of length ``len(x)``: ``x``, ``y_est``, ``y_std``, ``z``, - ``sig_nonzero`` (bool), ``direction`` (-1/0/+1). + Arrays of length ``len(x)``: ``x``, ``y_model`` (best-fit curve value), + ``y_model_std`` (propagated fit uncertainty), ``z``, ``sig_nonzero`` + (bool), ``direction`` (-1/0/+1). rollup : dict Scalars: ``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``n_nonzero``, ``any_nonzero``. @@ -80,8 +81,8 @@ def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): per_point = { "x": x, - "y_est": y_est, - "y_std": y_std, + "y_model": y_est, + "y_model_std": y_std, "z": z, "sig_nonzero": sig_nonzero, "direction": direction, diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index 51e6eeb..f547c3e 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -12,9 +12,20 @@ import numpy as np import pandas as pd +# Keys returned by assess_best_model's per-point dict (the model curve + tests). +_PER_POINT_COLS = ["x", "y_model", "y_model_std", "z", "sig_nonzero", + "direction"] + # Columns of the per-point assessment frame, in order. Kept as a module -# constant so empty-result paths emit an identically-shaped (empty) frame. -_ASSESS_COLS = ["x", "y_est", "y_std", "z", "sig_nonzero", "direction"] +# constant so empty-result paths emit an identically-shaped (empty) frame. This +# is the self-contained record: the best model's name, the observed data +# (``y_obs`` and its input error ``y_std``), the fitted curve at the observed x +# (``y_model`` and its propagated error ``y_model_std``), and the zero tests. +_ASSESS_COLS = ["model", "x", "y_obs", "y_std", "y_model", "y_model_std", + "z", "sig_nonzero", "direction"] + +# Non-float dtypes for the empty-frame path. +_ASSESS_DTYPES = {"model": object, "sig_nonzero": bool, "direction": int} # Rollup keys added to flat_output by the post-hoc assessment. Listed here so # the insufficient-data / all-fail paths can emit them as NaN. @@ -23,7 +34,9 @@ def _empty_assess_df(): - return pd.DataFrame({c: pd.Series(dtype=float) for c in _ASSESS_COLS}) + return pd.DataFrame({ + c: pd.Series(dtype=_ASSESS_DTYPES.get(c, float)) for c in _ASSESS_COLS + }) def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, @@ -68,12 +81,14 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, errors for all tested models, and the best-model omnibus rollup (``omnibus_W/df/p``, ``n_nonzero``, ``any_nonzero``). pd.DataFrame - Model predictions with columns ``model``, ``x``, ``y``, ``y_std``, - ``is_best_model``. Restricted to the best model unless ``best_only`` is - False. + Model predictions with columns ``model``, ``x``, ``y_model``, + ``y_model_std``, ``is_best_model``. Restricted to the best model unless + ``best_only`` is False. pd.DataFrame - Per-point assessment of the best model at the observed (unique) x, with - columns ``x``, ``y_est``, ``y_std``, ``z``, ``sig_nonzero``, + Self-contained per-point assessment of the best model at the observed + (unique) x, with columns ``model``, ``x``, ``y_obs`` (observed value), + ``y_std`` (observed input error), ``y_model`` (fitted curve), + ``y_model_std`` (propagated fit error), ``z``, ``sig_nonzero``, ``direction``. Empty if no model could be fit. (``equiv_zero`` is added downstream in ``cat_response`` once the global delta is known.) """ @@ -115,8 +130,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, pred_df = pd.DataFrame({"model": pd.Series(dtype=object), "x": pd.Series(dtype=float), - "y": pd.Series(dtype=float), - "y_std": pd.Series(dtype=float), + "y_model": pd.Series(dtype=float), + "y_model_std": pd.Series(dtype=float), "is_best_model": pd.Series(dtype=bool)}) return flat_output, pred_df, _empty_assess_df() @@ -260,8 +275,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, pred_rows.append(pd.DataFrame({ "model": name, "x": x_pred, - "y": y_pred, - "y_std": y_pred_std, + "y_model": y_pred, + "y_model_std": y_pred_std, })) if pred_rows: @@ -269,8 +284,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, else: pred_df = pd.DataFrame({"model": pd.Series(dtype=object), "x": pd.Series(dtype=float), - "y": pd.Series(dtype=float), - "y_std": pd.Series(dtype=float)}) + "y_model": pd.Series(dtype=float), + "y_model_std": pd.Series(dtype=float)}) pred_df["is_best_model"] = pred_df["model"] == flat_output["best_model"] # Per-point assessment + omnibus rollup for the best model. @@ -280,7 +295,18 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, p_res['model_func'], p_res['params'], p_res['cov'], x_assess, alpha=alpha ) - assess_df = pd.DataFrame({c: per_point[c] for c in _ASSESS_COLS}) + assess_df = pd.DataFrame({c: per_point[c] for c in _PER_POINT_COLS}) + + # Attach the observed data aggregated to the assessment grid. With one + # observation per concentration (the shared-grid assumption) this is a + # straight alignment; replicate concentrations are mean-collapsed. + obs = (pd.DataFrame({"x": x, "y_obs": y, "y_std": y_std}) + .groupby("x", sort=True).mean().reindex(x_assess)) + assess_df["y_obs"] = obs["y_obs"].to_numpy() + assess_df["y_std"] = obs["y_std"].to_numpy() + assess_df["model"] = best_model + assess_df = assess_df[_ASSESS_COLS] + for key in _ROLLUP_KEYS: flat_output[key] = rollup[key] else: diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 0263c79..74d1332 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -133,12 +133,14 @@ def cat_response(df, Column names keep the ``|`` delimiter; presentation is left to callers. predictions_df : pandas.DataFrame Predicted curves, concatenated across groups. Columns are the group-key - columns followed by ``model``, ``x``, ``y``, ``y_std``, ``is_best_model``. - Restricted to the best model per group unless ``best_only`` is False. + columns followed by ``model``, ``x``, ``y_model``, ``y_model_std``, + ``is_best_model``. Restricted to the best model per group unless + ``best_only`` is False. assessment_df : pandas.DataFrame - Per-point best-model assessment at the observed (unique) x. Group-key - columns followed by ``x``, ``y_est``, ``y_std``, ``z``, ``sig_nonzero``, - ``direction``, ``equiv_zero``. + Self-contained per-point best-model assessment at the observed (unique) + x. Group-key columns followed by ``model``, ``x``, ``y_obs``, ``y_std``, + ``y_model``, ``y_model_std``, ``z``, ``sig_nonzero``, ``direction``, + ``equiv_zero``. delta : float The equivalence half-width actually used (resolved from ``delta`` / ``delta_c`` if not supplied). @@ -227,12 +229,14 @@ def cat_response(df, # --- Post-hoc pass: global delta, equivalence, FDR, response class ------- resolved_delta = delta if resolved_delta is None: - resolved_delta = compute_delta(assessment_df.get("y_std", []), delta_c) + resolved_delta = compute_delta( + assessment_df.get("y_model_std", []), delta_c + ) if len(assessment_df): assessment_df["equiv_zero"] = classify_equiv( - assessment_df["y_est"].to_numpy(dtype=float), - assessment_df["y_std"].to_numpy(dtype=float), + assessment_df["y_model"].to_numpy(dtype=float), + assessment_df["y_model_std"].to_numpy(dtype=float), resolved_delta, alpha=alpha, ) diff --git a/src/tfscreen/analysis/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index b0042de..648a69a 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -247,4 +247,12 @@ def extract_epistasis( if y_std is not None: cycles["ep_std"] = ep_std + # Sort by (genotype, group_by) for stable, readable output + sort_cols = ["genotype"] + if group_by is not None: + if isinstance(group_by, str): + group_by = [group_by] + sort_cols.extend(group_by) + cycles = cycles.sort_values(sort_cols).reset_index(drop=True) + return cycles diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index cfe7976..30f8602 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -8,6 +8,7 @@ cat_response as _cat_response, ) from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.util import resolve_obs_columns from tfscreen.util.cli import generalized_main @@ -56,7 +57,7 @@ def _write_per_model(results_df, models, group_cols, out_prefix): def cat_response(data_file, x_obs, - y_obs, + y_obs=None, out_prefix="tfs_cat_response", y_std=None, group_by=None, @@ -93,8 +94,10 @@ def cat_response(data_file, ``y_obs``, and (if given) ``y_std`` and every column in ``group_by``. x_obs : str Name of the column holding the independent variable (e.g. 'titrant_conc'). - y_obs : str - Name of the column holding the observable (e.g. 'q0.5', 'point_est'). + y_obs : str or None, optional + Name of the column holding the observable (e.g. 'q0.5', 'point_est'). If + None (default) and the input has a 'q0.5' column (as written by + tfs-predict-theta), 'q0.5' is used. out_prefix : str, optional Prefix for the output CSV files. Default 'tfs_cat_response'. y_std : str or None, optional @@ -134,12 +137,9 @@ def cat_response(data_file, group_cols = ["genotype"] + (list(group_by) if group_by else []) - # Sigma fallback: symmetric quantile half-width when no explicit column and - # tfs-predict-theta's quantile columns are present. - if y_std is None and "q0.841" in df.columns and "q0.159" in df.columns: - df = df.copy() - df["_sigma"] = (df["q0.841"] - df["q0.159"]) / 2 - y_std = "_sigma" + # Fill in y_obs/y_std defaults from quantile columns (q0.5 for the point + # estimate, (q0.841 - q0.159)/2 for the std) when not given explicitly. + df, y_obs, y_std = resolve_obs_columns(df, y_obs=y_obs, y_std=y_std) # Fail fast on missing columns rather than deep inside the group loop. required = list(dict.fromkeys(group_cols + [x_obs, y_obs] @@ -200,9 +200,11 @@ def cat_response(data_file, def main(): generalized_main(cat_response, - manual_arg_types={"y_std": str, + manual_arg_types={"y_obs": str, + "y_std": str, "group_by": str, - "models": str}, + "models": str, + "delta": float}, manual_arg_nargs={"group_by": "+", "models": "+"}) diff --git a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py index 8388462..6dd0c37 100644 --- a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -7,6 +7,7 @@ from tfscreen.analysis.extract_epistasis import ( extract_epistasis as _extract_epistasis, ) +from tfscreen.util import resolve_obs_columns from tfscreen.util.cli import generalized_main @@ -90,7 +91,7 @@ def _diagnose_empty_output(df, group_by, exclude=()): def extract_epistasis(data_file, - y_obs, + y_obs=None, out_prefix="tfs_epistasis", y_std=None, group_by=None, @@ -111,15 +112,18 @@ def extract_epistasis(data_file, Path to the input CSV. Must contain a 'genotype' column, the column named by y_obs, and (if given) the columns named by y_std and group_by. - y_obs : str + y_obs : str or None, optional Name of the column holding the observable for which epistasis is - calculated (e.g. 'fitness', 'dG'). + calculated (e.g. 'fitness', 'dG'). If None (default) and the input has a + 'q0.5' column (as written by tfs-predict-theta), 'q0.5' is used. out_prefix : str, optional Prefix for the output CSV file, written to {out_prefix}.csv. Default 'tfs_epistasis'. y_std : str or None, optional Name of the column holding the standard error of y_obs. If given, the - epistasis error (ep_std) is propagated and written. + epistasis error (ep_std) is propagated and written. If None (default) + and both 'q0.841' and 'q0.159' are present, the standard error is taken + as (q0.841 - q0.159) / 2. group_by : list of str or None, optional One or more column names that define a unique experimental condition. Epistasis is calculated independently within each condition. If omitted, @@ -135,6 +139,10 @@ def extract_epistasis(data_file, print(f"Reading {data_file}...", flush=True) df = pd.read_csv(data_file) + # Fill in y_obs/y_std defaults from quantile columns (q0.5 for the point + # estimate, (q0.841 - q0.159)/2 for the std) when not given explicitly. + df, y_obs, y_std = resolve_obs_columns(df, y_obs=y_obs, y_std=y_std) + # Fail fast on missing columns rather than deep inside the pivot. required = ["genotype", y_obs] if y_std is not None: @@ -171,7 +179,8 @@ def extract_epistasis(data_file, def main(): generalized_main(extract_epistasis, - manual_arg_types={"y_std": str, + manual_arg_types={"y_obs": str, + "y_std": str, "group_by": str, "scale": str}, manual_arg_nargs={"group_by": "+"}) diff --git a/src/tfscreen/plot/cat_fits.py b/src/tfscreen/plot/cat_fits.py index 83bc329..bae0521 100644 --- a/src/tfscreen/plot/cat_fits.py +++ b/src/tfscreen/plot/cat_fits.py @@ -45,8 +45,8 @@ def cat_fits(x,y,y_std, y_std : numpy.ndarray Array of standard deviations for the y-coordinates. pred_df : pandas.DataFrame - DataFrame containing model predictions. Must include columns 'x', 'y', 'y_std', - 'model', and 'is_best_model'. + DataFrame containing model predictions. Must include columns 'x', + 'y_model', 'y_model_std', 'model', and 'is_best_model'. title : str, optional Put this title on the plot data_color : str, optional @@ -144,12 +144,12 @@ def cat_fits(x,y,y_std, "label": m, } - ax.plot(model_df['x'],model_df['y'],'-', + ax.plot(model_df['x'],model_df['y_model'],'-', **this_fit_line_kwargs) ax.fill_between(model_df['x'], - model_df['y'] - model_df['y_std'], - model_df['y'] + model_df['y_std'], + model_df['y_model'] - model_df['y_model_std'], + model_df['y_model'] + model_df['y_model_std'], color=err_area_color, zorder=0) else: @@ -160,7 +160,7 @@ def cat_fits(x,y,y_std, "label": m, } - ax.plot(model_df['x'],model_df['y'],'-', + ax.plot(model_df['x'],model_df['y_model'],'-', **this_fit_line_kwargs) # Add legend diff --git a/src/tfscreen/util/__init__.py b/src/tfscreen/util/__init__.py index 5034d5a..575b23e 100644 --- a/src/tfscreen/util/__init__.py +++ b/src/tfscreen/util/__init__.py @@ -72,4 +72,8 @@ from .dataframe import ( # noqa: F401 add_group_columns +) + +from .dataframe import ( # noqa: F401 + resolve_obs_columns ) \ No newline at end of file diff --git a/src/tfscreen/util/dataframe/__init__.py b/src/tfscreen/util/dataframe/__init__.py index edec5aa..79fb50d 100644 --- a/src/tfscreen/util/dataframe/__init__.py +++ b/src/tfscreen/util/dataframe/__init__.py @@ -25,4 +25,8 @@ from .add_group_columns import ( # noqa: F401 add_group_columns +) + +from .resolve_obs_columns import ( # noqa: F401 + resolve_obs_columns ) \ No newline at end of file diff --git a/src/tfscreen/util/dataframe/resolve_obs_columns.py b/src/tfscreen/util/dataframe/resolve_obs_columns.py new file mode 100644 index 0000000..f7267ba --- /dev/null +++ b/src/tfscreen/util/dataframe/resolve_obs_columns.py @@ -0,0 +1,87 @@ +def _quantile_col(q): + """Column name storing quantile ``q`` (e.g. 0.159 -> 'q0.159').""" + return f"q{q}" + + +def resolve_obs_columns(df, + y_obs=None, + y_std=None, + point_quantile=0.5, + sigma_quantiles=(0.159, 0.841), + sigma_col="_sigma"): + """ + Resolve default observable / uncertainty columns for quantile tables. + + Several ``tfs-*`` tools consume a long-form CSV and need a single point + estimate (``y_obs``) and, optionally, a per-row standard deviation + (``y_std``). When the table stores a posterior as quantile columns (e.g. + ``q0.5``, ``q0.159``, ``q0.841`` as written by ``tfs-predict-theta``), the + caller can omit both and let this helper fill in sane defaults: + + - ``y_obs`` defaults to the ``point_quantile`` column (``q0.5``). + - ``y_std`` defaults to the symmetric one-sigma half-width from + ``sigma_quantiles``, ``(q0.841 - q0.159) / 2``, added as a new column. + + An explicitly supplied name always takes precedence and is returned + unchanged (no quantile inspection is done for that axis). + + Parameters + ---------- + df : pandas.DataFrame + The input table. Not modified in place; a copy is returned only when a + sigma column has to be added. + y_obs : str or None, optional + Name of the observable column. If None, the ``point_quantile`` column is + used when present. + y_std : str or None, optional + Name of the standard-deviation column. If None, the symmetric quantile + half-width is computed from ``sigma_quantiles`` when both bounding + columns are present; otherwise it stays None (unweighted). + point_quantile : float, optional + Quantile used as the ``y_obs`` fallback. Default 0.5 (median). + sigma_quantiles : tuple of float, optional + ``(lo, hi)`` quantiles bracketing one sigma. Default (0.159, 0.841). + sigma_col : str, optional + Name of the column created to hold the derived sigma. Default + ``"_sigma"``. + + Returns + ------- + df : pandas.DataFrame + Either the original DataFrame (unchanged) or a copy with the derived + ``sigma_col`` added. + y_obs : str + The resolved observable column name. + y_std : str or None + The resolved standard-deviation column name, or None if no explicit + column was given and the quantile columns were unavailable. + + Raises + ------ + ValueError + If ``y_obs`` is None and the ``point_quantile`` column is not present -- + an observable cannot be guessed. + """ + # Resolve the observable. Fall back to the point-estimate quantile. + if y_obs is None: + point_col = _quantile_col(point_quantile) + if point_col not in df.columns: + raise ValueError( + f"No 'y_obs' column specified and the default '{point_col}' " + f"column is not present. Specify an observable column " + f"explicitly. Available columns: {list(df.columns)}" + ) + y_obs = point_col + + # Resolve the standard deviation. Fall back to the symmetric quantile + # half-width, mirroring the convention used across the codebase. + if y_std is None: + lo, hi = sigma_quantiles + lo_col = _quantile_col(lo) + hi_col = _quantile_col(hi) + if lo_col in df.columns and hi_col in df.columns: + df = df.copy() + df[sigma_col] = (df[hi_col] - df[lo_col]) / 2 + y_std = sigma_col + + return df, y_obs, y_std diff --git a/tests/tfscreen/analysis/cat_response/test_cat_assess.py b/tests/tfscreen/analysis/cat_response/test_cat_assess.py index 7813cc1..099d239 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_assess.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -48,13 +48,13 @@ def test_nan_cov_returns_nan(self): assert np.isnan(W) and df == 0 and np.isnan(p) def test_assess_best_model_flags_and_rollup(self): - # y = 1*x + 0 at x=[1,2,3] -> y_est = [1,2,3], all clearly nonzero. + # y = 1*x + 0 at x=[1,2,3] -> y_model = [1,2,3], all clearly nonzero. x = np.array([1.0, 2.0, 3.0]) params = np.array([1.0, 0.0]) cov = np.diag([1e-4, 1e-4]) # tiny -> small pred se -> significant per_point, rollup = assess_best_model(_linear, params, cov, x) - assert per_point["y_est"] == pytest.approx([1.0, 2.0, 3.0]) + assert per_point["y_model"] == pytest.approx([1.0, 2.0, 3.0]) assert np.all(per_point["sig_nonzero"]) assert np.all(per_point["direction"] == 1) assert rollup["n_nonzero"] == 3 diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index 5dc8cb8..351f79e 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -114,10 +114,14 @@ def test_assessment_rollup_and_frame(): for key in ["omnibus_W", "omnibus_df", "omnibus_p", "n_nonzero", "any_nonzero"]: assert key in flat_output - assert list(assess_df.columns) == ["x", "y_est", "y_std", "z", + assert list(assess_df.columns) == ["model", "x", "y_obs", "y_std", + "y_model", "y_model_std", "z", "sig_nonzero", "direction"] # One row per unique observed x. assert len(assess_df) == len(np.unique(x)) + # Model name recorded; observed data carried through alongside the fit. + assert (assess_df["model"] == "flat").all() + assert assess_df["y_obs"].to_numpy() == pytest.approx(y) # Curve sits far from zero -> every point significant, tiny omnibus p. assert flat_output["n_nonzero"] == len(np.unique(x)) assert flat_output["any_nonzero"] is True @@ -148,7 +152,7 @@ def test_insufficient_data(): assert np.isnan(flat_output["omnibus_p"]) # Best-only predictions: nothing to predict, empty frames. assert len(pred_df) == 0 - assert list(pred_df.columns) == ["model", "x", "y", "y_std", + assert list(pred_df.columns) == ["model", "x", "y_model", "y_model_std", "is_best_model"] assert len(assess_df) == 0 diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 93d4820..7f7f4b6 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -38,19 +38,22 @@ def _pred(n=2): return pd.DataFrame({ "model": ["flat"] * n, "x": np.arange(n, dtype=float), - "y": np.zeros(n), - "y_std": np.zeros(n), + "y_model": np.zeros(n), + "y_model_std": np.zeros(n), "is_best_model": [True] * n, }) -def _assess(n=2, y_est=0.0, y_std=0.1): +def _assess(n=2, y_model=0.0, y_model_std=0.1): """A minimal cat_fit per-point assessment frame.""" return pd.DataFrame({ + "model": ["flat"] * n, "x": np.arange(n, dtype=float), - "y_est": np.full(n, y_est, dtype=float), - "y_std": np.full(n, y_std, dtype=float), - "z": np.full(n, y_est / y_std if y_std else 0.0), + "y_obs": np.full(n, y_model, dtype=float), + "y_std": np.full(n, y_model_std, dtype=float), + "y_model": np.full(n, y_model, dtype=float), + "y_model_std": np.full(n, y_model_std, dtype=float), + "z": np.full(n, y_model / y_model_std if y_model_std else 0.0), "sig_nonzero": np.zeros(n, dtype=bool), "direction": np.zeros(n, dtype=int), }) @@ -187,7 +190,7 @@ def test_predictions_tagged_with_group_keys(self): group_by=["titrant_name"]) # Group keys come first, then the cat_fit prediction columns. assert list(preds.columns[:2]) == ["genotype", "titrant_name"] - for col in ["model", "x", "y", "y_std", "is_best_model"]: + for col in ["model", "x", "y_model", "y_model_std", "is_best_model"]: assert col in preds.columns # Every prediction row carries a real group key. assert not preds["genotype"].isna().any() @@ -225,15 +228,15 @@ def test_explicit_delta_is_used(self): assert delta == 0.75 def test_response_class_confident_zero(self): - # y_est=0, y_std tiny -> CI well inside delta -> all_equiv_zero -> and - # omnibus_p high (0.5) -> confident_zero. + # y_model=0, y_model_std tiny -> CI well inside delta -> all_equiv_zero + # -> and omnibus_p high (0.5) -> confident_zero. fit = _capturing_fit([]) def fake(*a, **k): flat, pred, _ = fit(*a, **k) x = a[0] - return flat, pred, _assess(len(np.unique(x)), y_est=0.0, - y_std=1e-4) + return flat, pred, _assess(len(np.unique(x)), y_model=0.0, + y_model_std=1e-4) with patch.object(cat_response_mod, "cat_fit", side_effect=fake): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", @@ -242,15 +245,15 @@ def fake(*a, **k): def test_equivalence_wins_over_significance(self): # Significant omnibus (tiny p) but every point inside the ROPE - # (y_est=0, tiny std) -> practically zero should win -> confident_zero. + # (y_model=0, tiny std) -> practically zero should win -> confident_zero. fit = _capturing_fit([]) def fake(*a, **k): flat, pred, _ = fit(*a, **k) flat["omnibus_p"] = 1e-9 x = a[0] - return flat, pred, _assess(len(np.unique(x)), y_est=0.0, - y_std=1e-4) + return flat, pred, _assess(len(np.unique(x)), y_model=0.0, + y_model_std=1e-4) with patch.object(cat_response_mod, "cat_fit", side_effect=fake): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", diff --git a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py index 015496e..7e8d317 100644 --- a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py @@ -67,14 +67,16 @@ def test_writes_main_permodel_and_predictions(self, tmp_path): preds = pd.read_csv(f"{out_prefix}_predictions.csv") assert "genotype" in preds.columns - assert {"model", "x", "y", "is_best_model"}.issubset(preds.columns) + assert {"model", "x", "y_model", "y_model_std", + "is_best_model"}.issubset(preds.columns) # best_only default: predictions restricted to each group's best model. assert preds["is_best_model"].all() assess = pd.read_csv(f"{out_prefix}_assessment.csv") assert "genotype" in assess.columns - assert {"x", "y_est", "y_std", "z", "sig_nonzero", "direction", - "equiv_zero"}.issubset(assess.columns) + assert {"model", "x", "y_obs", "y_std", "y_model", "y_model_std", "z", + "sig_nonzero", "direction", "equiv_zero"}.issubset( + assess.columns) # Rollups landed on the main table. assert {"omnibus_p", "omnibus_q", "n_nonzero", "response_class"}.issubset(main.columns) @@ -113,6 +115,27 @@ def fake_core(df, **kwargs): # (q0.841 - q0.159)/2 = 0.05 for every row. assert captured["sigma_vals"] == pytest.approx([0.05] * 8) + def test_y_obs_defaults_to_q05(self, tmp_path): + captured = {} + + def fake_core(df, **kwargs): + captured["y_obs"] = kwargs["y_obs"] + captured["y_std"] = kwargs["y_std"] + empty = pd.DataFrame({"genotype": [], "best_model": []}) + return empty, pd.DataFrame({"genotype": []}), \ + pd.DataFrame({"genotype": []}), 0.1 + + data = _write(tmp_path, _theta_df(with_quantiles=True)) + with patch.object(cat_response_cli, "_cat_response", + side_effect=fake_core), \ + patch.object(cat_response_cli, "_write_per_model"): + # Neither y_obs nor y_std given -> both resolved from quantiles. + cat_response(data, x_obs="titrant_conc", + out_prefix=str(tmp_path / "out")) + + assert captured["y_obs"] == "q0.5" + assert captured["y_std"] == "_sigma" + def test_explicit_y_std_takes_precedence(self, tmp_path): captured = {} @@ -179,7 +202,8 @@ class TestArgWiring: def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): data = _write(tmp_path, _theta_df()) out_prefix = str(tmp_path / "out") - argv = ["cat_response", data, "titrant_conc", "q0.5", + argv = ["cat_response", data, "titrant_conc", + "--y_obs", "q0.5", "--out_prefix", out_prefix, "--group_by", "titrant_name", "--models", "flat", "linear", diff --git a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py index 6375e43..507cbd6 100644 --- a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py +++ b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py @@ -119,6 +119,44 @@ def test_missing_group_by_raises(self, tmp_path): out_prefix=str(tmp_path / "out")) +class TestQuantileDefaults: + + def _quantile_cycle_rows(self): + """Same additive cycle (ep = 1.0) stored as quantile columns.""" + rows = _single_cycle_rows() + out = [] + for r in rows: + y = r["y"] + out.append({ + "genotype": r["genotype"], + "q0.159": y - 0.1, + "q0.5": y, + "q0.841": y + 0.1, + }) + return out + + def test_y_obs_defaults_to_q05(self, tmp_path): + data = _write_csv(tmp_path, self._quantile_cycle_rows()) + out_prefix = str(tmp_path / "out") + + # No y_obs / y_std given -> q0.5 for the point estimate, quantile + # half-width for the std. + extract_epistasis(data, out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv") + assert len(out) == 1 + assert out["ep_obs"].iloc[0] == pytest.approx(1.0) + # Symmetric half-width is 0.1 for every state -> sqrt(4*0.1**2) = 0.2. + assert out["ep_std"].iloc[0] == pytest.approx(0.2) + + def test_missing_q05_raises(self, tmp_path): + rows = [{"genotype": r["genotype"], "value": r["y"]} + for r in _single_cycle_rows()] + data = _write_csv(tmp_path, rows) + with pytest.raises(ValueError, match="No 'y_obs' column"): + extract_epistasis(data, out_prefix=str(tmp_path / "out")) + + class TestEmptyResult: def test_no_cycles_writes_empty(self, tmp_path, capsys): @@ -184,7 +222,8 @@ def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): + _single_cycle_rows(condition="c2")) out_prefix = str(tmp_path / "out") - argv = ["extract_epistasis", data, "y", + argv = ["extract_epistasis", data, + "--y_obs", "y", "--out_prefix", out_prefix, "--y_std", "y_err", "--group_by", "condition", diff --git a/tests/tfscreen/plot/test_cat_fits.py b/tests/tfscreen/plot/test_cat_fits.py index 50eef03..5139d0a 100644 --- a/tests/tfscreen/plot/test_cat_fits.py +++ b/tests/tfscreen/plot/test_cat_fits.py @@ -12,8 +12,8 @@ def test_cat_fits_basic(): pred_df = pd.DataFrame({ 'x': np.tile(x, 2), - 'y': np.concatenate([y, y*0.9]), - 'y_std': np.tile(y_std, 2), + 'y_model': np.concatenate([y, y*0.9]), + 'y_model_std': np.tile(y_std, 2), 'model': ['Model A']*4 + ['Model B']*4, 'is_best_model': [True]*4 + [False]*4 }) @@ -46,8 +46,8 @@ def test_cat_fits_custom_ax(): pred_df = pd.DataFrame({ 'x': [1], - 'y': [1], - 'y_std': [0.1], + 'y_model': [1], + 'y_model_std': [0.1], 'model': ['Model A'], 'is_best_model': [True] }) @@ -65,8 +65,8 @@ def test_cat_fits_nan_handling(): # Pred df pred_df = pd.DataFrame({ 'x': [1], - 'y': [1], - 'y_std': [0.1], + 'y_model': [1], + 'y_model_std': [0.1], 'model': ['Model A'], 'is_best_model': [True] }) @@ -82,8 +82,8 @@ def test_cat_fits_log_scale(): pred_df = pd.DataFrame({ 'x': x, - 'y': y, - 'y_std': y_std, + 'y_model': y, + 'y_model_std': y_std, 'model': ['Model A']*2, 'is_best_model': [True]*2 }) diff --git a/tests/tfscreen/util/dataframe/test_resolve_obs_columns.py b/tests/tfscreen/util/dataframe/test_resolve_obs_columns.py new file mode 100644 index 0000000..36d9bea --- /dev/null +++ b/tests/tfscreen/util/dataframe/test_resolve_obs_columns.py @@ -0,0 +1,107 @@ +""" +Tests for resolve_obs_columns -- quantile-column defaults for y_obs / y_std. +""" +import pytest +import pandas as pd + +from tfscreen.util import resolve_obs_columns + + +def _quantile_df(): + return pd.DataFrame({ + "genotype": ["wt", "A15G"], + "q0.159": [0.4, 1.4], + "q0.5": [0.5, 1.5], + "q0.841": [0.6, 1.8], + }) + + +def test_explicit_names_pass_through_unchanged(): + df = pd.DataFrame({"genotype": ["wt"], "y": [1.0], "err": [0.1]}) + out_df, y_obs, y_std = resolve_obs_columns(df, y_obs="y", y_std="err") + + assert y_obs == "y" + assert y_std == "err" + # No sigma column added; original frame returned untouched. + assert out_df is df + assert "_sigma" not in out_df.columns + + +def test_y_obs_defaults_to_median_quantile(): + df = _quantile_df() + out_df, y_obs, y_std = resolve_obs_columns(df) + + assert y_obs == "q0.5" + assert y_std == "_sigma" + + +def test_sigma_is_symmetric_half_width(): + df = _quantile_df() + out_df, y_obs, y_std = resolve_obs_columns(df) + + # (q0.841 - q0.159) / 2 + assert out_df["_sigma"].tolist() == pytest.approx([0.1, 0.2]) + + +def test_original_frame_not_mutated_when_sigma_added(): + df = _quantile_df() + out_df, _, _ = resolve_obs_columns(df) + + assert "_sigma" in out_df.columns + assert "_sigma" not in df.columns # copy-on-write, original untouched + assert out_df is not df + + +def test_explicit_y_obs_but_default_y_std(): + df = _quantile_df() + out_df, y_obs, y_std = resolve_obs_columns(df, y_obs="q0.5") + + assert y_obs == "q0.5" + assert y_std == "_sigma" + + +def test_no_sigma_columns_leaves_y_std_none(): + df = pd.DataFrame({"genotype": ["wt"], "q0.5": [0.5]}) + out_df, y_obs, y_std = resolve_obs_columns(df) + + assert y_obs == "q0.5" + assert y_std is None + assert out_df is df + + +def test_only_one_sigma_column_leaves_y_std_none(): + # Only the upper bound present -> cannot form a half-width. + df = pd.DataFrame({"genotype": ["wt"], "q0.5": [0.5], "q0.841": [0.6]}) + _, y_obs, y_std = resolve_obs_columns(df) + + assert y_obs == "q0.5" + assert y_std is None + + +def test_missing_point_quantile_raises(): + df = pd.DataFrame({"genotype": ["wt"], "value": [0.5]}) + with pytest.raises(ValueError, match="No 'y_obs' column"): + resolve_obs_columns(df) + + +def test_custom_quantiles(): + df = pd.DataFrame({ + "genotype": ["wt"], + "q0.25": [0.3], + "q0.75": [0.7], + }) + out_df, y_obs, y_std = resolve_obs_columns( + df, point_quantile=0.25, sigma_quantiles=(0.25, 0.75) + ) + + assert y_obs == "q0.25" + assert y_std == "_sigma" + assert out_df["_sigma"].iloc[0] == pytest.approx((0.7 - 0.3) / 2) + + +def test_custom_sigma_col_name(): + df = _quantile_df() + out_df, _, y_std = resolve_obs_columns(df, sigma_col="theta_std") + + assert y_std == "theta_std" + assert "theta_std" in out_df.columns From 790cccee1850eb49d99207f2f6ad2e8a3336a397 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 19 Jul 2026 22:40:28 -0400 Subject: [PATCH 13/26] improved cat_response stability and fitting --- CLAUDE.md | 2 + docs/badges/coverage-badge.svg | 1 + docs/badges/tests-badge.svg | 125 +---- reports/flake.txt | 465 +++++++++--------- reports/junit/junit.xml | 2 +- src/tfscreen/analysis/cat_response/cat_fit.py | 35 +- .../analysis/cat_response/cat_response.py | 13 +- .../analysis/scripts/cat_response_cli.py | 9 +- src/tfscreen/mle/curve_models/__init__.py | 51 +- src/tfscreen/mle/curve_models/guesses.py | 82 +++ src/tfscreen/mle/curve_models/models.py | 124 +++++ src/tfscreen/mle/fitters/_util.py | 94 +++- src/tfscreen/util/numerical/xfill.py | 15 +- .../analysis/cat_response/test_cat_fit.py | 71 ++- .../tfscreen/mle/curve_models/test_guesses.py | 74 ++- .../tfscreen/mle/curve_models/test_models.py | 63 ++- tests/tfscreen/mle/fitters/test_util.py | 99 +++- tests/tfscreen/util/numerical/test_xfill.py | 24 +- 18 files changed, 942 insertions(+), 407 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f481a19..72e2c91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -282,6 +282,8 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( `tfs-cat-response` fits a family of empirical shapes (`MODEL_LIBRARY`) to each group's `y_obs`-vs-`x_obs` curve and answers two **orthogonal** questions. Do not conflate them: +**Model x-scale (concentration-parameterized vs log-conc).** `MODEL_LIBRARY` models are **not** interchangeable on x-scale. The Hill family (`repressor`/`inducer`/`hill_*`) and `biphasic_*` are parameterized in **raw concentration** and take `log(x)` internally (`_hill` does `np.log(x)`), so they are already sigmoids/peaks *in log-concentration* and **must be handed raw x** — feeding them `log10(x)` (negative) both double-logs and NaNs them. The geometric models (`bell_peak`/`bell_dip` Gaussian-in-x, `linear`) are shapes in raw x; their `*_log` counterparts (`bell_peak_log`/`bell_dip_log` = Gaussian in `log10(x)` with a free real `center`, and `linear_log` = line in `log10(x)`) are the log-concentration versions. The `*_log` models own their transform via `models._to_log10_x` (x stays **raw concentration** in the data — there is no `--log_x` flag and no separate log column): `x <= 0` (the no-titrant point) is floored to `min(x[x>0])/100` before the log, computed per-call (identical across groups for a shared titration grid). `flat` is scale-invariant. When adding a new model, decide which camp it's in — never blanket-transform x. `DEFAULT_MODELS` (in `curve_models/__init__.py`) is the curated set fit when `--models`/`models_to_run` is omitted (was: all of `MODEL_LIBRARY`): `flat, linear_log, repressor, inducer, bell_peak_log, bell_dip_log` — one parameterization per qualitative response. All other models (raw-x `bell_*`/`linear`, 4-param `hill_*`, `biphasic_*`) stay registered and reachable via `--models`. + - **Shape** (which model): AICc weight selection in `cat_fit.py`. Uses the small-sample-corrected AICc, not AIC — with ~8 points and up to 4 params the correction `2k(k+1)/(n-k-1)` is large and flips selections; when `n-k-1 <= 0` the model gets `aicc=inf` (can't win, but its params are still reported). AIC/R² are computed on the **weighted** residuals (`chi2 = sum(((y-yfit)/y_std)**2)`; `aic = 2k + chi2`) — the fit was always weighted, so selection is too now. - **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading the *best* model against zero. Per-point `sig_nonzero` z-test at the observed x; a per-curve **omnibus** Wald/χ² `W = yhat @ pinv(Σ_yhat) @ yhat` (df = rank(Σ) = n_params) using the **full** prediction covariance (`predict_with_error(..., full_cov=True)` = `J·Cov·Jᵀ`), which rewards both magnitude and how-many-points and handles the strong correlation among predicted points; then **Benjamini-Hochberg** across curves → `omnibus_q`. diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index e69de29..3d2b8fa 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -0,0 +1 @@ +coverage: 96.19%coverage96.19% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 61ce646..c644be3 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1,124 +1 @@ - - - - - - - -shields.io | 520: Web server is returning an unknown error - - - - - - - - -
-
-
-

- Web server is returning an unknown error - Error code 520 -

-
- Visit cloudflare.com for more information. -
-
2026-07-19 11:31:36 UTC
-
-
-
-
-
-
- - - - -
- You -

- - Browser - -

- - Working - -
-
- - Seattle -

- - Cloudflare - -

- - Working - -
-
-
- - - - -
- img.shields.io -

- - Host - -

- - Error - -
-
-
-
- -
-
-
-

What happened?

-

There is an unknown connection issue between Cloudflare and the origin web server. As a result, the web page can not be displayed.

-
-
-

What can I do?

- -

If you are a visitor of this website:

-

Please try again in a few minutes.

- -

If you are the owner of this website:

-

There is an issue between Cloudflare's cache and your origin web server. Cloudflare monitors for these errors and automatically investigates the cause. To help support the investigation, you can pull the corresponding error log from your web server and submit it our support team. Please include the Ray ID (which is at the bottom of this error page). Additional troubleshooting resources.

-
-
-
- - - -
-
- - \ No newline at end of file +tests: 4186tests4186 \ No newline at end of file diff --git a/reports/flake.txt b/reports/flake.txt index 0942c27..9eca229 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -6,13 +6,13 @@ ./src/tfscreen/analysis/__init__.py:13:1: W391 blank line at end of file ./src/tfscreen/analysis/cat_response/__init__.py:2:77: W291 trailing whitespace ./src/tfscreen/analysis/cat_response/__init__.py:3:55: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:42:1: C901 'cat_fit' is too complex (25) -./src/tfscreen/analysis/cat_response/cat_fit.py:169:72: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:169:74: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:241:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./src/tfscreen/analysis/cat_response/cat_fit.py:243:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./src/tfscreen/analysis/cat_response/cat_fit.py:42:1: C901 'cat_fit' is too complex (26) +./src/tfscreen/analysis/cat_response/cat_fit.py:175:72: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:175:74: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:262:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./src/tfscreen/analysis/cat_response/cat_fit.py:264:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' ./src/tfscreen/analysis/cat_response/cat_response.py:60:1: C901 'cat_response' is too complex (15) -./src/tfscreen/analysis/cat_response/cat_response.py:250:45: E127 continuation line over-indented for visual indent +./src/tfscreen/analysis/cat_response/cat_response.py:253:45: E127 continuation line over-indented for visual indent ./src/tfscreen/analysis/compare_theta.py:200:1: C901 'compare_theta' is too complex (12) ./src/tfscreen/analysis/compare_theta.py:632:1: C901 'aggregate_theta' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) @@ -286,149 +286,167 @@ ./src/tfscreen/mle/curve_models/__init__.py:11:77: W291 trailing whitespace ./src/tfscreen/mle/curve_models/__init__.py:16:76: W291 trailing whitespace ./src/tfscreen/mle/curve_models/__init__.py:18:29: W291 trailing whitespace -./src/tfscreen/mle/curve_models/__init__.py:52:26: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:53:26: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:54:27: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:55:22: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:58:28: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:59:28: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:60:29: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:61:24: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:62:27: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:65:31: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:66:31: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:67:32: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:68:27: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:69:30: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:72:29: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:73:29: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:74:30: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:75:25: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:76:28: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:79:36: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:80:36: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:81:37: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:82:32: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:83:35: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:84:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/__init__.py:86:34: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:87:34: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:88:35: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:89:30: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:90:33: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:93:31: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:94:31: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:95:32: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:96:27: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:97:30: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:100:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:57:26: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:58:26: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:59:27: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:60:22: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:63:28: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:64:28: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:65:29: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:66:24: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:67:27: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:70:32: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:71:32: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:72:33: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:73:28: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:74:31: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:77:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:78:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:79:32: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:80:27: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:81:30: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:84:29: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:85:29: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:86:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:87:25: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:88:28: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:91:36: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:92:36: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:93:37: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:94:32: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:95:35: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:96:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/__init__.py:98:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:99:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:100:35: E231 missing whitespace after ':' ./src/tfscreen/mle/curve_models/__init__.py:101:30: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:102:31: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:103:26: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:104:29: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:104:55: W291 trailing whitespace -./src/tfscreen/mle/curve_models/__init__.py:107:35: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:108:35: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:109:36: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:110:31: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:111:34: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:114:34: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:115:34: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:116:35: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:117:30: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:117:33: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:102:33: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:105:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:106:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:107:32: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:108:27: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:109:30: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:112:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:113:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:114:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:115:26: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:116:29: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:119:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:120:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:121:36: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:123:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:124:34: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:127:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:128:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:129:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:131:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:132:33: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:135:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:136:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:137:36: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:138:31: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:139:34: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:142:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:143:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:144:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:145:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:145:33: E201 whitespace after '[' ./src/tfscreen/mle/curve_models/guesses.py:3:69: W291 trailing whitespace -./src/tfscreen/mle/curve_models/guesses.py:11:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:25:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:25:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:28:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:51:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:54:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:74:23: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:78:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:92:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:98:23: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:118:37: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:119:24: E261 at least two spaces before inline comment -./src/tfscreen/mle/curve_models/guesses.py:123:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:137:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:138:35: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:139:24: E261 at least two spaces before inline comment -./src/tfscreen/mle/curve_models/guesses.py:143:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:13:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:27:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:27:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:30:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:53:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:56:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:76:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:96:23: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:100:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:114:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:120:23: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:140:37: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:141:24: E261 at least two spaces before inline comment +./src/tfscreen/mle/curve_models/guesses.py:145:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/mle/curve_models/guesses.py:159:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:164:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:174:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:190:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:195:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:205:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:221:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:224:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:226:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:231:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:234:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:252:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:256:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:259:26: E225 missing whitespace around operator -./src/tfscreen/mle/curve_models/guesses.py:261:10: E261 at least two spaces before inline comment -./src/tfscreen/mle/curve_models/guesses.py:266:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:266:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:269:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:280:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:280:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:283:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:283:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:286:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:297:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:297:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:300:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:300:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:160:35: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:161:24: E261 at least two spaces before inline comment +./src/tfscreen/mle/curve_models/guesses.py:165:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:181:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:186:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:196:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:212:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:217:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:227:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:257:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:287:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/mle/curve_models/guesses.py:303:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:314:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:314:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:317:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/guesses.py:317:21: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:320:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/guesses.py:331:20: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:331:25: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/guesses.py:333:1: W391 blank line at end of file -./src/tfscreen/mle/curve_models/models.py:13:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:38:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:39:26: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:40:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:41:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:68:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:70:51: W291 trailing whitespace -./src/tfscreen/mle/curve_models/models.py:75:25: W291 trailing whitespace -./src/tfscreen/mle/curve_models/models.py:88:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:91:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:130:27: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:130:37: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:130:42: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:130:47: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:132:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:203:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:206:31: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:206:41: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:207:37: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:207:47: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:210:38: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:211:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:213:37: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:213:47: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:214:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:251:30: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:251:40: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:252:31: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:254:30: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:254:40: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:255:31: E231 missing whitespace after ',' -./src/tfscreen/mle/curve_models/models.py:259:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:262:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:301:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:304:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/curve_models/models.py:316:28: W291 trailing whitespace -./src/tfscreen/mle/curve_models/models.py:326:1: W293 blank line contains whitespace -./src/tfscreen/mle/curve_models/models.py:330:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:306:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:308:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:313:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:316:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:334:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:338:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:341:26: E225 missing whitespace around operator +./src/tfscreen/mle/curve_models/guesses.py:343:10: E261 at least two spaces before inline comment +./src/tfscreen/mle/curve_models/guesses.py:348:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:348:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:351:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:362:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:362:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:365:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:365:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:368:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:379:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:379:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:382:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:382:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:385:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:396:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:396:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:399:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/guesses.py:399:21: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:402:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/guesses.py:413:20: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:413:25: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/guesses.py:415:1: W391 blank line at end of file +./src/tfscreen/mle/curve_models/models.py:54:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:79:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:80:26: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:81:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:82:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:109:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:141:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:143:51: W291 trailing whitespace +./src/tfscreen/mle/curve_models/models.py:148:25: W291 trailing whitespace +./src/tfscreen/mle/curve_models/models.py:161:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:164:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:203:27: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:203:37: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:203:42: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:203:47: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:205:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:276:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:279:31: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:279:41: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:280:37: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:280:47: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:283:38: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:284:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:286:37: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:286:47: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:287:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:375:30: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:375:40: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:376:31: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:378:30: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:378:40: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:379:31: E231 missing whitespace after ',' +./src/tfscreen/mle/curve_models/models.py:383:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:386:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:425:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:428:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/mle/curve_models/models.py:440:28: W291 trailing whitespace +./src/tfscreen/mle/curve_models/models.py:450:1: W293 blank line contains whitespace +./src/tfscreen/mle/curve_models/models.py:454:1: W293 blank line contains whitespace ./src/tfscreen/mle/fit_manager.py:26:72: W291 trailing whitespace ./src/tfscreen/mle/fit_manager.py:29:56: W291 trailing whitespace ./src/tfscreen/mle/fit_manager.py:34:72: W291 trailing whitespace @@ -462,14 +480,6 @@ ./src/tfscreen/mle/fit_manager.py:456:1: W293 blank line contains whitespace ./src/tfscreen/mle/fit_manager.py:473:1: W293 blank line contains whitespace ./src/tfscreen/mle/fitters/__init__.py:7:1: W391 blank line at end of file -./src/tfscreen/mle/fitters/_util.py:5:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/mle/fitters/_util.py:5:14: E231 missing whitespace after ',' -./src/tfscreen/mle/fitters/_util.py:5:24: E231 missing whitespace after ',' -./src/tfscreen/mle/fitters/_util.py:5:31: E231 missing whitespace after ',' -./src/tfscreen/mle/fitters/_util.py:6:1: W293 blank line contains whitespace -./src/tfscreen/mle/fitters/_util.py:19:18: E225 missing whitespace around operator -./src/tfscreen/mle/fitters/_util.py:21:44: W291 trailing whitespace -./src/tfscreen/mle/fitters/_util.py:27:34: W292 no newline at end of file ./src/tfscreen/mle/fitters/least_squares.py:7:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/mle/fitters/least_squares.py:42:1: W293 blank line contains whitespace ./src/tfscreen/mle/fitters/least_squares.py:43:1: W293 blank line contains whitespace @@ -3773,13 +3783,11 @@ ./src/tfscreen/util/numerical/transform.py:10:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/util/numerical/transform.py:189:36: W292 no newline at end of file ./src/tfscreen/util/numerical/xfill.py:3:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/util/numerical/xfill.py:14:1: W293 blank line contains whitespace -./src/tfscreen/util/numerical/xfill.py:26:1: W293 blank line contains whitespace -./src/tfscreen/util/numerical/xfill.py:32:1: W293 blank line contains whitespace -./src/tfscreen/util/numerical/xfill.py:58:14: E111 indentation is not a multiple of 4 -./src/tfscreen/util/numerical/xfill.py:58:14: E117 over-indented -./src/tfscreen/util/numerical/xfill.py:58:29: E261 at least two spaces before inline comment -./src/tfscreen/util/numerical/xfill.py:79:20: W292 no newline at end of file +./src/tfscreen/util/numerical/xfill.py:39:1: W293 blank line contains whitespace +./src/tfscreen/util/numerical/xfill.py:65:14: E111 indentation is not a multiple of 4 +./src/tfscreen/util/numerical/xfill.py:65:14: E117 over-indented +./src/tfscreen/util/numerical/xfill.py:65:29: E261 at least two spaces before inline comment +./src/tfscreen/util/numerical/xfill.py:88:20: W292 no newline at end of file ./src/tfscreen/util/numerical/zero_truncated_poisson.py:5:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/util/numerical/zero_truncated_poisson.py:45:1: W293 blank line contains whitespace ./src/tfscreen/util/numerical/zero_truncated_poisson.py:50:1: W293 blank line contains whitespace @@ -4305,46 +4313,46 @@ ./tests/tfscreen/genetics/test_library_manager.py:815:35: E231 missing whitespace after ':' ./tests/tfscreen/genetics/test_library_manager.py:815:49: W291 trailing whitespace ./tests/tfscreen/genetics/test_library_manager.py:840:34: W292 no newline at end of file -./tests/tfscreen/mle/curve_models/test_guesses.py:9:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_guesses.py:22:44: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:73:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_guesses.py:76:32: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:83:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:91:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:14:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_guesses.py:27:44: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:78:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_guesses.py:81:32: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:88:1: W293 blank line contains whitespace ./tests/tfscreen/mle/curve_models/test_guesses.py:96:1: W293 blank line contains whitespace ./tests/tfscreen/mle/curve_models/test_guesses.py:101:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:107:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_guesses.py:109:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:115:45: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:116:45: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:117:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_guesses.py:122:45: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:123:46: E261 at least two spaces before inline comment -./tests/tfscreen/mle/curve_models/test_guesses.py:129:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:106:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:112:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_guesses.py:114:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:120:45: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:121:45: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:122:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_guesses.py:127:45: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:128:46: E261 at least two spaces before inline comment +./tests/tfscreen/mle/curve_models/test_guesses.py:134:1: W293 blank line contains whitespace ./tests/tfscreen/mle/curve_models/test_models.py:2:1: F401 'pytest' imported but unused -./tests/tfscreen/mle/curve_models/test_models.py:15:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:20:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:26:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:30:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:18:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_models.py:23:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_models.py:29:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/curve_models/test_models.py:33:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:37:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:41:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:49:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:54:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:55:52: W291 trailing whitespace -./tests/tfscreen/mle/curve_models/test_models.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:64:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:69:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:71:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:74:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:79:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:36:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:40:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:44:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_models.py:52:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_models.py:57:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:58:52: W291 trailing whitespace +./tests/tfscreen/mle/curve_models/test_models.py:62:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:67:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/curve_models/test_models.py:72:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:74:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:77:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/curve_models/test_models.py:82:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:84:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:88:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/curve_models/test_models.py:92:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:85:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:87:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:91:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/curve_models/test_models.py:95:1: W293 blank line contains whitespace ./tests/tfscreen/mle/curve_models/test_models.py:98:1: W293 blank line contains whitespace -./tests/tfscreen/mle/curve_models/test_models.py:100:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:101:1: W293 blank line contains whitespace +./tests/tfscreen/mle/curve_models/test_models.py:103:1: W293 blank line contains whitespace ./tests/tfscreen/mle/fitters/test_least_squares.py:3:1: F401 'pytest' imported but unused ./tests/tfscreen/mle/fitters/test_least_squares.py:6:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/fitters/test_least_squares.py:10:1: E302 expected 2 blank lines, found 1 @@ -4416,32 +4424,28 @@ ./tests/tfscreen/mle/fitters/test_svd_error.py:55:1: W293 blank line contains whitespace ./tests/tfscreen/mle/fitters/test_svd_error.py:59:1: W293 blank line contains whitespace ./tests/tfscreen/mle/fitters/test_util.py:3:1: F401 'pytest' imported but unused -./tests/tfscreen/mle/fitters/test_util.py:7:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/fitters/test_util.py:12:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:15:34: E261 at least two spaces before inline comment -./tests/tfscreen/mle/fitters/test_util.py:16:31: E261 at least two spaces before inline comment -./tests/tfscreen/mle/fitters/test_util.py:17:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:23:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/fitters/test_util.py:25:29: E261 at least two spaces before inline comment -./tests/tfscreen/mle/fitters/test_util.py:27:31: E261 at least two spaces before inline comment -./tests/tfscreen/mle/fitters/test_util.py:28:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:34:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:36:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:40:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/fitters/test_util.py:45:18: E261 at least two spaces before inline comment -./tests/tfscreen/mle/fitters/test_util.py:46:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:49:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:53:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/fitters/test_util.py:59:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:61:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:64:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/fitters/test_util.py:70:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:75:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:79:1: E302 expected 2 blank lines, found 1 -./tests/tfscreen/mle/fitters/test_util.py:85:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:98:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:102:1: W293 blank line contains whitespace -./tests/tfscreen/mle/fitters/test_util.py:105:1: W391 blank line at end of file +./tests/tfscreen/mle/fitters/test_util.py:6:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/fitters/test_util.py:11:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:14:34: E261 at least two spaces before inline comment +./tests/tfscreen/mle/fitters/test_util.py:15:31: E261 at least two spaces before inline comment +./tests/tfscreen/mle/fitters/test_util.py:16:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:22:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/fitters/test_util.py:24:29: E261 at least two spaces before inline comment +./tests/tfscreen/mle/fitters/test_util.py:26:31: E261 at least two spaces before inline comment +./tests/tfscreen/mle/fitters/test_util.py:27:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:33:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:35:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:39:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/fitters/test_util.py:44:18: E261 at least two spaces before inline comment +./tests/tfscreen/mle/fitters/test_util.py:45:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:48:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:52:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/fitters/test_util.py:58:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:63:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:67:1: E302 expected 2 blank lines, found 1 +./tests/tfscreen/mle/fitters/test_util.py:73:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:86:1: W293 blank line contains whitespace +./tests/tfscreen/mle/fitters/test_util.py:90:1: W293 blank line contains whitespace ./tests/tfscreen/mle/test_fit_manager.py:6:1: F401 'scipy.special.expit' imported but unused ./tests/tfscreen/mle/test_fit_manager.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/mle/test_fit_manager.py:20:1: E302 expected 2 blank lines, found 1 @@ -7454,7 +7458,8 @@ ./tests/tfscreen/util/numerical/test_xfill.py:92:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_xfill.py:99:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_xfill.py:101:29: E261 at least two spaces before inline comment -./tests/tfscreen/util/numerical/test_xfill.py:109:41: W292 no newline at end of file +./tests/tfscreen/util/numerical/test_xfill.py:110:1: E302 expected 2 blank lines, found 0 +./tests/tfscreen/util/numerical/test_xfill.py:125:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_zero_truncated_poisson.py:5:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_zero_truncated_poisson.py:10:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/numerical/test_zero_truncated_poisson.py:14:1: W293 blank line contains whitespace @@ -7473,7 +7478,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -54 C901 'cat_fit' is too complex (25) +54 C901 'cat_fit' is too complex (26) 10 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7484,14 +7489,14 @@ 214 E127 continuation line over-indented for visual indent 92 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent -43 E201 whitespace after '[' +46 E201 whitespace after '[' 28 E202 whitespace before ']' 6 E203 whitespace before ',' 4 E211 whitespace before '(' 1839 E221 multiple spaces before operator 26 E222 multiple spaces after operator -8 E225 missing whitespace around operator -1087 E231 missing whitespace after ',' +7 E225 missing whitespace around operator +1096 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 207 E261 at least two spaces before inline comment @@ -7499,7 +7504,7 @@ 2 E266 too many leading '#' for block comment 149 E272 multiple spaces before keyword 1 E301 expected 1 blank line, found 0 -763 E302 expected 2 blank lines, found 1 +767 E302 expected 2 blank lines, found 1 48 E303 too many blank lines (2) 19 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 @@ -7516,8 +7521,8 @@ 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -592 W291 trailing whitespace -83 W292 no newline at end of file -1505 W293 blank line contains whitespace -29 W391 blank line at end of file -7475 +590 W291 trailing whitespace +81 W292 no newline at end of file +1500 W293 blank line contains whitespace +28 W391 blank line at end of file +7480 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 2da91c8..d9dd82b 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index f547c3e..3c46b2e 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -1,4 +1,4 @@ -from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS from tfscreen.mle import ( run_least_squares, @@ -60,8 +60,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, array at which to predict x after fitting each model. If not specified, fill in values within x. models_to_run : list of str, optional - A list of model names to test. If None (default), all models in the - global MODEL_LIBRARY will be tested. + A list of model names to test. If None (default), the curated + ``DEFAULT_MODELS`` set is used. best_only : bool, optional If True (default), only the selected best model's predicted curve is emitted in the returned prediction frame. If False, every successfully @@ -94,7 +94,7 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, """ if models_to_run is None: - models_to_run = list(MODEL_LIBRARY.keys()) + models_to_run = list(DEFAULT_MODELS) flat_output = {} @@ -105,7 +105,9 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, y_std = y_std[finite_mask] if x_pred is None: - x_pred = xfill(x) + # min_value=0 avoids negative concentrations in the pad (the + # concentration-parameterized models take log(x) and NaN on negative x). + x_pred = xfill(x, min_value=0.0) # Assessment / omnibus grid: the unique observed concentrations (the shared # titration series), one row per concentration. @@ -160,8 +162,12 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, params, std_err, cov_matrix, fit_obj = run_least_squares( model_func, y, y_std, guesses, bounds[0], bounds[1], args=(x,) ) - if not fit_obj.success: - raise RuntimeError(f"Fit failed: {fit_obj.message}") + # On "SVD did not converge" run_least_squares returns the + # exception (not an OptimizeResult) as fit_obj, so guard the + # attribute access -- treat a missing .success as a failed fit. + if not getattr(fit_obj, "success", False): + msg = getattr(fit_obj, "message", fit_obj) + raise RuntimeError(f"Fit failed: {msg}") # If this is a 2D array of values, this is a full design matrix. # Solve by weighted least squares. @@ -189,6 +195,21 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, denom = n - k - 1 aicc = aic + (2 * k * (k + 1) / denom) if denom > 0 else np.inf + # A converged fit can still have a singular Jacobian, in which case + # get_cov returns an all-NaN covariance (and NaN std errors) even + # though fit.success is True. Such a model has finite params/chi2 and + # would otherwise be selectable, but its prediction/assessment errors + # would be NaN -- and if it won it would poison the global delta. + # Treat it as unusable for selection: force aicc = inf (zero weight, + # can't win) and drop the covariance, while still reporting the point + # estimates. This reuses the same path as the n - k - 1 <= 0 case. + cov_usable = (cov_matrix is not None + and np.all(np.isfinite(cov_matrix)) + and np.all(np.isfinite(std_err))) + if not cov_usable: + aicc = np.inf + cov_matrix = None + summary_results.append({"model": name, "R2": r2, "chi2": chi2, "AIC": aic, "AICc": aicc, "success": True}) param_results[name] = {"params": params, "std_err": std_err, diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 74d1332..620de8b 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -20,7 +20,7 @@ from .cat_fit import cat_fit from .cat_assess import compute_delta, classify_equiv, benjamini_hochberg -from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS from tfscreen.util import resolve_workers from tfscreen.util.numerical import xfill @@ -96,7 +96,8 @@ def cat_response(df, curve is fit per group. If None (default), groups are defined by 'genotype' alone. models_to_run : list of str or None, optional - Model names to test. If None (default), all models in MODEL_LIBRARY. + Model names to test. If None (default), the curated ``DEFAULT_MODELS`` + set (see ``tfscreen.mle.curve_models``). best_only : bool, optional If True (default), the returned prediction frame holds only each group's best-model curve. If False, it holds every fit model's curve. @@ -146,7 +147,7 @@ def cat_response(df, ``delta_c`` if not supplied). """ if models_to_run is None: - models_to_run = list(MODEL_LIBRARY.keys()) + models_to_run = list(DEFAULT_MODELS) bad = [m for m in models_to_run if m not in MODEL_LIBRARY] if bad: @@ -164,8 +165,10 @@ def cat_response(df, ) # A shared prediction grid spanning all observed x-values, so every group's - # predicted curve is evaluated on the same axis. - x_pred = xfill(pd.unique(df[x_obs]), num_points=100) + # predicted curve is evaluated on the same axis. min_value=0 keeps the pad + # from producing negative concentrations (the concentration-parameterized + # models take log(x) and would NaN on negative x). + x_pred = xfill(pd.unique(df[x_obs]), num_points=100, min_value=0.0) # observed=True so unused categorical combinations do not create empty groups. work_items = [] diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index 30f8602..b612583 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -7,7 +7,7 @@ from tfscreen.analysis.cat_response.cat_response import ( cat_response as _cat_response, ) -from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS from tfscreen.util import resolve_obs_columns from tfscreen.util.cli import generalized_main @@ -108,7 +108,10 @@ def cat_response(data_file, Additional column(s) that, together with 'genotype', define a group. If omitted, groups are defined by 'genotype' alone. models : list of str or None, optional - Response models to fit. Defaults to all models in MODEL_LIBRARY. + Response models to fit. If None (default), the curated ``DEFAULT_MODELS`` + set is used (flat, linear_log, repressor, inducer, bell_peak_log, + bell_dip_log). Pass explicit names to reach any model in MODEL_LIBRARY, + including the raw-x and biphasic variants. alpha : float, optional Significance level for the per-point tests and the omnibus q-value threshold used to call a curve 'real'. Default 0.05. @@ -126,7 +129,7 @@ def cat_response(data_file, default) uses ``os.cpu_count() - 1``; ``N`` uses ``N`` processes. """ if models is None: - models = list(MODEL_LIBRARY.keys()) + models = list(DEFAULT_MODELS) bad = [m for m in models if m not in MODEL_LIBRARY] if bad: diff --git a/src/tfscreen/mle/curve_models/__init__.py b/src/tfscreen/mle/curve_models/__init__.py index 9e22685..32dbe79 100644 --- a/src/tfscreen/mle/curve_models/__init__.py +++ b/src/tfscreen/mle/curve_models/__init__.py @@ -23,9 +23,11 @@ from .models import ( # noqa: F401 model_flat, model_linear, + model_linear_logx, model_hill_3p, model_hill_4p, model_bell, + model_bell_logx, model_biphasic_peak, model_biphasic_dip, model_poly @@ -34,12 +36,15 @@ from .guesses import ( guess_flat, guess_linear, + guess_linear_logx, guess_repressor, guess_inducer, guess_hill_repressor, guess_hill_inducer, guess_bell_peak, guess_bell_dip, + guess_bell_peak_logx, + guess_bell_dip_logx, guess_biphasic_peak, guess_biphasic_dip, ) @@ -61,6 +66,13 @@ "bounds":([-inf, -inf], [ inf, inf])}, + # linear model in log10-concentration + "linear_log": {"model_func":model_linear_logx, + "guess_func":guess_linear_logx, + "param_names":['m', 'b'], + "bounds":([-inf, -inf], + [ inf, inf])}, + # 3 point hill model with negative amplitude "repressor": {"model_func":model_hill_3p, "guess_func":guess_repressor, @@ -101,7 +113,23 @@ "guess_func":guess_bell_dip, "param_names":['baseline', 'amplitude', 'ln_x0', 'ln_width'], "bounds":([-inf, -inf, -inf, -inf], - [ inf, 0, inf, inf])}, + [ inf, 0, inf, inf])}, + + # gaussian in log10-concentration with positive amplitude (peak) + "bell_peak_log": {"model_func":model_bell_logx, + "guess_func":guess_bell_peak_logx, + "param_names":['baseline', 'amplitude', 'center', + 'ln_width'], + "bounds":([-inf, 0, -inf, -inf], + [ inf, inf, inf, inf])}, + + # gaussian in log10-concentration with negative amplitude (dip) + "bell_dip_log": {"model_func":model_bell_logx, + "guess_func":guess_bell_dip_logx, + "param_names":['baseline', 'amplitude', 'center', + 'ln_width'], + "bounds":([-inf, -inf, -inf, -inf], + [ inf, 0, inf, inf])}, # sequential processes "biphasic_peak": {"model_func":model_biphasic_peak, @@ -118,3 +146,24 @@ [inf, inf, inf, inf])}, } + +# Default set fit by tfs-cat-response when no models are specified. One +# parameterization per qualitative response shape, all appropriate for +# concentration data on a log axis: +# - flat : non-responsive / null baseline +# - linear_log : monotonic, non-saturating trend in log-concentration +# - repressor : saturating sigmoid down (Hill, n=1) +# - inducer : saturating sigmoid up (Hill, n=1) +# - bell_peak_log : band-pass peak, symmetric in log-concentration +# - bell_dip_log : band-stop dip, symmetric in log-concentration +# The remaining models (raw-x bell_peak/bell_dip/linear, the 4-parameter Hill +# variants, and the biphasic peak/dip shapes) stay registered and are reachable +# via the ``--models`` flag, but are not fit by default. +DEFAULT_MODELS = [ + "flat", + "linear_log", + "repressor", + "inducer", + "bell_peak_log", + "bell_dip_log", +] diff --git a/src/tfscreen/mle/curve_models/guesses.py b/src/tfscreen/mle/curve_models/guesses.py index 45e1084..a84b56e 100644 --- a/src/tfscreen/mle/curve_models/guesses.py +++ b/src/tfscreen/mle/curve_models/guesses.py @@ -8,6 +8,8 @@ import numpy as np +from .models import _to_log10_x + def guess_flat(x, y): """ Generates a guess for the flat model. @@ -51,6 +53,26 @@ def guess_linear(x, y): X = np.vander(x,2) return X +def guess_linear_logx(x, y): + """ + Generates guesses for the log-concentration linear model. + + Parameters + ---------- + x, y : np.ndarray + Input data arrays (``x`` is raw concentration). + + Returns + ------- + np.ndarray + Design matrix for polynomial WLS with columns ``[log10(x), 1]`` (not + reversed), so the solution comes back as ``[m, b]`` to match + ``model_linear_logx``. Mirrors ``guess_linear`` but on the log10-x axis. + """ + z = _to_log10_x(x) + X = np.vander(z, 2) + return X + def guess_repressor(x, y): """ Generates guesses for the repressor model. @@ -202,6 +224,66 @@ def guess_bell_dip(x, y): return np.array([baseline, amplitude, ln_x0, ln_width]) +def guess_bell_peak_logx(x, y): + """ + Generates guesses for the log-concentration bell-shaped peak model. + + Parameters + ---------- + x, y : np.ndarray + Input data arrays (``x`` is raw concentration). + + Returns + ------- + np.ndarray + Guesses for parameters [baseline, amplitude, center, ln_width], where + ``center`` is the peak location on the ``log10(x)`` axis. + """ + z = _to_log10_x(x) + + # Baseline is the minimum observed value. + baseline = np.min(y) + + # Find the location and amplitude of the peak (center in log10-x space). + peak_idx = np.argmax(y) + amplitude = y[peak_idx] - baseline + center = z[peak_idx] + + # Guess the width is a fraction of the total log10-x range. + width = max((np.max(z) - np.min(z)) / 4.0, 1e-9) + + return np.array([baseline, amplitude, center, np.log(width)]) + +def guess_bell_dip_logx(x, y): + """ + Generates guesses for the log-concentration bell-shaped dip model. + + Parameters + ---------- + x, y : np.ndarray + Input data arrays (``x`` is raw concentration). + + Returns + ------- + np.ndarray + Guesses for parameters [baseline, amplitude, center, ln_width], where + ``center`` is the dip location on the ``log10(x)`` axis. + """ + z = _to_log10_x(x) + + # Baseline is the maximum observed value. + baseline = np.max(y) + + # Find the location and amplitude of the dip (center in log10-x space). + dip_idx = np.argmin(y) + amplitude = y[dip_idx] - baseline + center = z[dip_idx] + + # Guess the width is a fraction of the total log10-x range. + width = max((np.max(z) - np.min(z)) / 4.0, 1e-9) + + return np.array([baseline, amplitude, center, np.log(width)]) + def guess_biphasic_peak(x, y): """ Generates guesses for the biphasic peak model. diff --git a/src/tfscreen/mle/curve_models/models.py b/src/tfscreen/mle/curve_models/models.py index 4546d6e..f0bb5d3 100644 --- a/src/tfscreen/mle/curve_models/models.py +++ b/src/tfscreen/mle/curve_models/models.py @@ -10,6 +10,47 @@ EXP_CLIP = 700 POWER_CLIP = 25 + +def _to_log10_x(x): + """ + Map raw concentration ``x`` onto a ``log10`` axis for the log-conc models. + + Unlike the concentration-parameterized Hill / biphasic models (which take + the log of ``x`` internally and so must be handed raw concentration), the + geometric log-conc models (``*_log``) are shapes *in* ``log10(x)`` and use + this helper to transform their input. Keeping the transform inside the model + means the data column stays raw concentration -- no separate log column and + no CLI flag. + + Any ``x <= 0`` (typically the ``x == 0`` no-titrant point) is replaced, + *before* the log, by ``min(x[x > 0]) / 100`` -- two decades below the + smallest positive value in the array. The floor is taken from the array + handed to this call; for the usual shared titration grid every group sees + the same concentrations, so the floor is consistent across groups. Non-finite + entries are preserved as NaN (the fitter drops them). If ``x`` has no positive + entries the result is all-NaN (an unfittable degenerate group). + + Parameters + ---------- + x : np.ndarray + Raw independent-variable values (concentration units). + + Returns + ------- + np.ndarray + ``log10(x)`` with the ``x <= 0`` floor applied. + """ + x = np.asarray(x, dtype=float) + pos = x[np.isfinite(x) & (x > 0)] + if pos.size == 0: + return np.full(x.shape, np.nan) + + floor = pos.min() / 100.0 + x_sub = np.where(x > 0, x, floor) + z = np.log10(x_sub) + # Preserve NaNs (np.where turned any NaN into the floor above). + return np.where(np.isnan(x), np.nan, z) + def model_flat(params, x): """ A constant, flat line (null model). @@ -65,6 +106,38 @@ def model_linear(params, x): return params[0]*x + params[1] +def model_linear_logx(params, x): + """ + A linear model in log10-concentration: ``y = m*log10(x) + b``. + + The log-concentration counterpart of ``model_linear``. ``x`` is raw + concentration; the ``log10`` transform (with the ``x <= 0`` floor) happens + inside via :func:`_to_log10_x`. + + Parameters + ---------- + params : array-like + A two-element array: [m, b]. + - m: The slope with respect to ``log10(x)``. + - b: The y-value at ``log10(x) == 0`` (i.e. at ``x == 1``). + x : np.ndarray + The independent variable values (raw concentration). + + Returns + ------- + np.ndarray + The calculated y-values. + + Notes + ----- + - Mathematical Form: y = m*log10(x) + b + - Biological Interpretation: A dose-dependent response that is linear in the + log of concentration and does not saturate within the tested range. + """ + + z = _to_log10_x(x) + return params[0]*z + params[1] + def _hill(params, x): """ Core private hill model used by public models. @@ -215,6 +288,57 @@ def model_bell(params, x): return baseline + amplitude * np.exp(exponent_safe) +def model_bell_logx(params, x): + """ + A symmetric, bell-shaped (Gaussian) peak/dip in log10-concentration. + + The log-concentration counterpart of ``model_bell``. Where ``model_bell`` is + a Gaussian in raw ``x`` with its center parameterized as ``exp(ln_x0)`` (so + the center is a positive concentration), this model is a Gaussian in + ``log10(x)`` with the center a *free real* on the log axis. ``x`` is raw + concentration; the ``log10`` transform (with the ``x <= 0`` floor) happens + inside via :func:`_to_log10_x`. A positive ``amplitude`` gives a peak, a + negative one a dip. + + Parameters + ---------- + params : array-like + A four-element array: [baseline, amplitude, center, ln_width]. + - baseline: Baseline occupancy (asymptote on both sides). + - amplitude: Height of the peak (>0) or depth of the dip (<0). + - center: Center of the peak on the ``log10(x)`` axis (a real number, + e.g. ``-3`` for a peak at ``x == 1e-3``). + - ln_width: The natural log of the Gaussian width (in log10-x units). + x : np.ndarray + The independent variable values (raw concentration). + + Returns + ------- + np.ndarray + The calculated y-values. + + Notes + ----- + - Mathematical Form: + y = baseline + amplitude * exp(-0.5 * ((log10(x) - center) / width)^2) + - Biological Interpretation: a band-pass (peak) or band-stop (dip) response + centered symmetrically in log-concentration -- activity that switches on + then off (or off then on) over a range of concentrations. + """ + + baseline, amplitude, center, ln_width = params + + z = _to_log10_x(x) + + ln_width_safe = np.clip(ln_width, -EXP_CLIP, EXP_CLIP) + width = max(np.exp(ln_width_safe), EPSILON) + + exponent = -0.5 * ((z - center) / width) ** 2 + exponent_safe = np.clip(exponent, -EXP_CLIP, EXP_CLIP) + + return baseline + amplitude * np.exp(exponent_safe) + + def model_biphasic_peak(params, x): """ A biphasic model describing an asymmetric peak-like response. diff --git a/src/tfscreen/mle/fitters/_util.py b/src/tfscreen/mle/fitters/_util.py index c817087..e089712 100644 --- a/src/tfscreen/mle/fitters/_util.py +++ b/src/tfscreen/mle/fitters/_util.py @@ -1,27 +1,89 @@ import numpy as np -from scipy.sparse import issparse -def get_cov(y,residuals,params,J): - - # Build covariance matrix and estimate standard errors + +def get_cov(y, residuals, params, J): + """ + Estimate the parameter covariance matrix and standard errors from a fit. + + The covariance is built from the singular value decomposition of the + Jacobian ``J`` rather than by inverting ``JᵀJ`` directly. Forming ``JᵀJ`` + squares the condition number, so a merely ill-conditioned-but-identifiable + Jacobian can tip into numerical singularity and yield a spurious all-NaN + covariance. Working from the SVD of ``J`` avoids that -- this mirrors how + ``scipy.optimize.curve_fit`` builds ``pcov``. + + For ``J = U S Vᵀ`` we have ``(JᵀJ)⁻¹ = V S⁻² Vᵀ`` and + ``cov = chi2_red · V S⁻² Vᵀ``, where ``chi2_red`` is the reduced chi-square + of the (already weighted) residuals. + + A genuinely rank-deficient Jacobian (a truly unidentified parameter or + parameter combination -- e.g. the width of a bell whose amplitude has gone + to zero) is detected by a singular value at or below + ``eps · max(J.shape) · s_max``. The covariance is then not defined, and an + all-NaN matrix / NaN standard errors are returned. This is the same contract + as before, so callers that test the covariance for NaN (``cat_fit``'s + selection guard, ``predict_with_error``) keep working unchanged; a converged + fit with an unusable curvature stays out of any weighted comparison. + + Parameters + ---------- + y : np.ndarray + Observed data. Used only to count valid (non-NaN) observations for the + degrees of freedom. + residuals : np.ndarray + The (weighted) residuals at the solution. + params : np.ndarray + Best-fit parameter values. + J : np.ndarray + The Jacobian of the residuals with respect to the parameters, shape + ``(num_obs, num_params)``. + + Returns + ------- + cov_matrix : np.ndarray + The ``(num_params, num_params)`` covariance matrix, or an all-NaN matrix + if ``J`` is rank-deficient / non-finite. + std_errors : np.ndarray + The per-parameter standard errors (sqrt of the covariance diagonal), or + all-NaN if ``J`` is rank-deficient / non-finite. + """ num_params = len(params) num_obs = np.sum(~np.isnan(y)) # Count only valid observations dof = num_obs - num_params if dof < 1: dof = 1 # Avoid division by zero for poorly constrained fits - chi2_red = np.sum(residuals**2) / dof + chi2_red = np.sum(residuals ** 2) / dof + + nan_cov = np.full((num_params, num_params), np.nan) + nan_std = np.full(num_params, np.nan) + + J = np.asarray(J, dtype=float) + if not np.all(np.isfinite(J)): + return nan_cov, nan_std try: - JTJ = J.T @ J - if issparse(J): - JTJ =JTJ.toarray() - cov_matrix = chi2_red * np.linalg.inv(JTJ) - with np.errstate(invalid='ignore'): - std_errors = np.sqrt(np.diagonal(cov_matrix)) - except (np.linalg.LinAlgError, ValueError): - cov_matrix = np.full((num_params, num_params), np.nan) - std_errors = np.full(num_params, np.nan) - - return cov_matrix, std_errors \ No newline at end of file + # Economy SVD of the Jacobian: J = U @ diag(s) @ Vt. + _, s, Vt = np.linalg.svd(J, full_matrices=False) + except np.linalg.LinAlgError: + return nan_cov, nan_std + + # Rank-deficiency test (the same threshold scipy.optimize.curve_fit uses). + # Fewer singular values than parameters (an underdetermined fit) or a tiny + # singular value both mean a parameter direction is unidentified, so the + # covariance is not defined. + if s.size < num_params or s[0] == 0.0: + return nan_cov, nan_std + threshold = np.finfo(float).eps * max(J.shape) * s[0] + if np.any(s <= threshold): + return nan_cov, nan_std + + # cov = chi2_red * V S^-2 Vt, formed from the SVD without ever building JtJ. + # Scaling the columns of V (= rows of Vt) by 1/s and taking the Gram product + # gives V S^-2 Vt, which is symmetric positive semidefinite by construction. + Vs = Vt.T / s + cov_matrix = chi2_red * (Vs @ Vs.T) + std_errors = np.sqrt(np.diagonal(cov_matrix)) + + return cov_matrix, std_errors diff --git a/src/tfscreen/util/numerical/xfill.py b/src/tfscreen/util/numerical/xfill.py index 0e7427c..c7ca74c 100644 --- a/src/tfscreen/util/numerical/xfill.py +++ b/src/tfscreen/util/numerical/xfill.py @@ -3,7 +3,8 @@ def xfill(x, num_points=100, use_log=None, - pad_by=0.1): + pad_by=0.1, + min_value=None): """ Smoothly fill points between minimum and maximum values in x. @@ -11,7 +12,7 @@ def xfill(x, (even if this means the spacing is not perfectly smooth). This allows one-to-one comparisons between measured values and a model calculated using the filled-in x values. - + Parameters ---------- x : np.ndarray @@ -23,7 +24,13 @@ def xfill(x, on the data's dynamic range. pad_by : float, optional Expand the range beyond the min/max of x by this factor, by default 0.1. - + min_value : float, optional + Floor for the (linear-scale) lower bound. When set, the padded lower + bound is clamped to be no smaller than ``min_value``. Use ``0.0`` for + domains where negative values are meaningless (e.g. concentration), so + that the padding below the minimum does not produce negative x. Ignored + on the log scale (already strictly positive). Default None (no clamp). + Returns ------- np.ndarray @@ -69,6 +76,8 @@ def xfill(x, pad = span * pad_by if span > 0 else 0 x_min = np.min(x_finite) - pad x_max = np.max(x_finite) + pad + if min_value is not None: + x_min = max(x_min, min_value) x_filled = np.linspace(x_min, x_max, num_points) # Re-insert original points into the filled array at the closest positions. diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index 351f79e..32c4d79 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -7,7 +7,7 @@ import pytest from tfscreen.analysis.cat_response.cat_fit import cat_fit -from tfscreen.mle.curve_models import MODEL_LIBRARY +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS # A titration-like grid with enough points that low-parameter models are usable. @@ -182,3 +182,72 @@ def test_sanitize_filters_nonfinite(): models_to_run=["flat", "linear"]) assert flat_output["status"] in ("success", "partial") assert len(assess_df) == 4 + + +def test_default_models_are_the_curated_set(): + """With models_to_run=None, cat_fit fits exactly DEFAULT_MODELS (not all).""" + x, y, y_std = _hill_data() + flat, _, _ = cat_fit(x, y, y_std) # models_to_run defaults to None + fit_models = {k.split("|", 1)[1] for k in flat if k.startswith("AIC_weight|")} + assert fit_models == set(DEFAULT_MODELS) + # The log-conc variants are in; the raw-x duplicates and biphasic are out. + assert "bell_peak_log" in fit_models + assert "biphasic_peak" not in fit_models + assert "bell_peak" not in fit_models + + +# --- degenerate-covariance guard --------------------------------------------- + +def _singular_bell_data(): + """Constant y -> bell amplitude ~ 0 -> center/width unidentified. + + scipy converges (fit.success is True) but the Jacobian is rank-deficient, so + get_cov returns an all-NaN covariance. This is the reachable case where a + model would otherwise be selected with a NaN covariance. + """ + x = np.array([0.0, 1.0, 3.0, 10.0, 30.0, 100.0]) + y = np.full_like(x, 0.5) + ys = np.full_like(x, 0.05) + return x, y, ys + + +def test_singular_covariance_model_excluded_from_selection(): + # bell_peak is the only candidate and it fits with a NaN covariance. + x, y, ys = _singular_bell_data() + flat, _, assess = cat_fit(x, y, ys, models_to_run=["bell_peak"]) + + # Not selectable -> no best model, and (crucially) no NaN-poisoned + # assessment rows to leak into the global delta. + assert flat["best_model"] == "None" + assert len(assess) == 0 + # Point estimates are still reported (not selected != not fit). + assert "bell_peak|amplitude|est" in flat + assert np.isfinite(flat["bell_peak|amplitude|est"]) + + +def test_singular_covariance_loses_to_usable_model(): + """A NaN-covariance model gets zero weight; a usable competitor wins.""" + x, y, ys = _singular_bell_data() + # flat also fits this constant data (with a finite covariance) and wins. + flat, _, assess = cat_fit(x, y, ys, models_to_run=["flat", "bell_peak"]) + + assert flat["best_model"] == "flat" + assert flat["AIC_weight|bell_peak"] == 0.0 + # The selected model's assessment errors are all finite (delta stays clean). + assert np.all(np.isfinite(assess["y_model_std"].to_numpy())) + + +# --- prediction grid domain -------------------------------------------------- + +def test_prediction_grid_is_non_negative(): + """The predicted curve grid must not include negative concentrations. + + The concentration-parameterized models take log(x); a negative x_pred would + make them emit NaN (and a RuntimeWarning). The xfill min_value=0 floor + prevents that. + """ + x, y, ys = _hill_data() # X spans [0, 100]; linear pad would go negative + _, pred_df, _ = cat_fit(x, y, ys, models_to_run=["flat", "hill_inducer"]) + assert (pred_df["x"].to_numpy() >= 0).all() + # And the predicted curve has no NaN (Hill no longer evaluated at x < 0). + assert not pred_df["y_model"].isna().any() diff --git a/tests/tfscreen/mle/curve_models/test_guesses.py b/tests/tfscreen/mle/curve_models/test_guesses.py index b73aead..e9e3105 100644 --- a/tests/tfscreen/mle/curve_models/test_guesses.py +++ b/tests/tfscreen/mle/curve_models/test_guesses.py @@ -3,8 +3,13 @@ import numpy as np from tfscreen.mle.curve_models import guesses -from tfscreen.mle.curve_models.models import model_linear -from tfscreen.mle import run_matrix_wls +from tfscreen.mle.curve_models.models import ( + model_linear, + model_linear_logx, + model_bell_logx, + _to_log10_x, +) +from tfscreen.mle import run_matrix_wls, run_least_squares @pytest.mark.parametrize("guess_func, expected_cols", [ (guesses.guess_flat, 1), @@ -130,3 +135,68 @@ def test_peak_and_dip_guesses(): # Biphasic dip guess = guesses.guess_biphasic_dip(x, y_dip) assert len(guess) == 4 + + +# --- log-concentration guesses ----------------------------------------------- + +def test_guess_linear_logx_column_order(): + """ + guess_linear_logx returns columns [log10(x), 1] (not reversed), so the WLS + solution comes back as [m, b] to match model_linear_logx and its ['m', 'b'] + param_names. + """ + x = np.array([1e-3, 1e-2, 1e-1, 1.0]) + y = np.ones_like(x) + X = guesses.guess_linear_logx(x, y) + assert X.shape == (4, 2) + assert np.allclose(X[:, 0], _to_log10_x(x)) + assert np.all(X[:, 1] == 1) + + +def test_linear_logx_fit_recovers_slope_intercept(): + """End-to-end: fitting y = m*log10(x) + b recovers [m, b] with R2 = 1.""" + m_true, b_true = -1.5, 0.4 + x = np.geomspace(1e-4, 1.0, 25) + y = m_true * np.log10(x) + b_true + y_std = np.ones_like(y) + + X = guesses.guess_linear_logx(x, y) + params, _, _, _ = run_matrix_wls(X, y, 1.0 / y_std) + assert np.isclose(params[0], m_true) + assert np.isclose(params[1], b_true) + assert np.allclose(model_linear_logx(params, x), y) + + +def test_guess_bell_logx_centers_in_log_space(): + # Peak concentration 1e-3 -> center guess near log10(1e-3) = -3. + x = np.geomspace(1e-6, 1.0, 25) + z = _to_log10_x(x) + y_peak = np.exp(-0.5 * ((z + 3.0) / 0.5) ** 2) + g = guesses.guess_bell_peak_logx(x, y_peak) + assert len(g) == 4 + assert np.isclose(g[2], -3.0, atol=1.0) # center near -3 in log space + assert g[1] > 0 # positive amplitude (peak) + + # Dip version. + g_dip = guesses.guess_bell_dip_logx(x, 1.0 - y_peak) + assert np.isclose(g_dip[2], -3.0, atol=1.0) + assert g_dip[1] < 0 # negative amplitude (dip) + + +def test_bell_logx_fit_recovers_curve(): + """End-to-end NLS fit of a log-conc bell peak recovers the true center.""" + x = np.geomspace(1e-6, 1.0, 30) + z = _to_log10_x(x) + true = [0.1, 0.8, -3.0, np.log(0.7)] + y = true[0] + true[1] * np.exp(-0.5 * ((z - true[2]) / 0.7) ** 2) + y_std = np.full_like(y, 0.01) + + g = guesses.guess_bell_peak_logx(x, y) + lower = [-np.inf, 0.0, -np.inf, -np.inf] + upper = [np.inf, np.inf, np.inf, np.inf] + params, _, _, fit_obj = run_least_squares( + model_bell_logx, y, y_std, g, lower, upper, args=(x,) + ) + assert fit_obj.success + assert np.isclose(params[2], -3.0, atol=0.05) # recovered center + assert np.allclose(model_bell_logx(params, x), y, atol=1e-2) diff --git a/tests/tfscreen/mle/curve_models/test_models.py b/tests/tfscreen/mle/curve_models/test_models.py index 5c6a9ae..a9a5950 100644 --- a/tests/tfscreen/mle/curve_models/test_models.py +++ b/tests/tfscreen/mle/curve_models/test_models.py @@ -4,12 +4,15 @@ from tfscreen.mle.curve_models.models import ( model_flat, model_linear, + model_linear_logx, model_hill_3p, model_hill_4p, model_bell, + model_bell_logx, model_biphasic_peak, model_biphasic_dip, - model_poly + model_poly, + _to_log10_x, ) def test_model_flat(): @@ -100,3 +103,61 @@ def test_model_biphasic_dip(): assert np.allclose(y[0], 0.5 + 1.0/101.0) assert np.allclose(y[1], 1.0/101.0 + 0.5) + + +# --- log-concentration models ------------------------------------------------ + +def test_to_log10_x_floor_and_passthrough(): + # x == 0 -> log10(min positive / 100). min positive = 1e-3 -> floor 1e-5. + x = np.array([0.0, 1e-3, 1e-2, 1e-1]) + z = _to_log10_x(x) + assert np.isclose(z[0], np.log10(1e-5)) + assert np.allclose(z[1:], np.log10([1e-3, 1e-2, 1e-1])) + + +def test_to_log10_x_nan_preserved(): + z = _to_log10_x(np.array([np.nan, 1e-2, 0.0])) + assert np.isnan(z[0]) + assert np.isclose(z[1], -2.0) + # 0 -> floor = 1e-2/100 = 1e-4 -> log10 = -4 + assert np.isclose(z[2], -4.0) + + +def test_to_log10_x_all_nonpositive_is_nan(): + z = _to_log10_x(np.array([0.0, 0.0, -1.0])) + assert np.all(np.isnan(z)) + + +def test_model_linear_logx(): + # y = m*log10(x) + b with m=2, b=1. + x = np.array([1e-2, 1e-1, 1.0]) # log10 -> [-2, -1, 0] + y = model_linear_logx([2.0, 1.0], x) + assert np.allclose(y, [2 * -2 + 1, 2 * -1 + 1, 2 * 0 + 1]) + + +def test_model_bell_logx_peak_centered_in_log_space(): + # Peak at center = -3 (i.e. x = 1e-3), baseline 0, amplitude 1. + params = [0.0, 1.0, -3.0, np.log(1.0)] + x = np.array([1e-4, 1e-3, 1e-2]) # log10 -> [-4, -3, -2] + y = model_bell_logx(params, x) + # Max at the center concentration. + assert np.argmax(y) == 1 + assert np.isclose(y[1], 1.0) + # Symmetric one log-unit either side of center. + assert np.isclose(y[0], y[2]) + + +def test_model_bell_logx_negative_amplitude_is_dip(): + params = [1.0, -1.0, -3.0, np.log(1.0)] + x = np.array([1e-4, 1e-3, 1e-2]) + y = model_bell_logx(params, x) + # Minimum at the center concentration. + assert np.argmin(y) == 1 + assert np.isclose(y[1], 0.0) + + +def test_model_bell_logx_handles_zero_x(): + # x == 0 must not blow up (it is floored inside _to_log10_x). + params = [0.0, 1.0, -3.0, np.log(1.0)] + y = model_bell_logx(params, np.array([0.0, 1e-3, 1e-2])) + assert np.all(np.isfinite(y)) diff --git a/tests/tfscreen/mle/fitters/test_util.py b/tests/tfscreen/mle/fitters/test_util.py index a5d35a0..658c7a5 100644 --- a/tests/tfscreen/mle/fitters/test_util.py +++ b/tests/tfscreen/mle/fitters/test_util.py @@ -1,7 +1,6 @@ import numpy as np import pytest -from scipy.sparse import csr_matrix from tfscreen.mle.fitters._util import get_cov def test_get_cov_basic(): @@ -50,17 +49,6 @@ def test_get_cov_singular(): assert np.all(np.isnan(cov)) assert np.all(np.isnan(std)) -def test_get_cov_sparse(): - # Test with sparse Jacobian - X = csr_matrix([[1.0], [1.0]]) - y = np.array([1.0, 1.2]) - params = np.array([1.1]) - residuals = y - 1.1 - - cov, std = get_cov(y, residuals, params, X) - - assert np.allclose(cov, [[0.01]]) - def test_get_cov_dof_limit(): # num_obs <= num_params -> dof < 1 -> dof clamped to 1 X = np.array([[1.0]]) @@ -103,3 +91,90 @@ def test_get_cov_nans_in_y(): cov, std = get_cov(y, residuals, params, X) assert np.allclose(cov, [[1.0/3.0]]) + +def _jacobian_from_svd(singular_values, m, seed=0): + """Build a dense J = U diag(s) Vt with prescribed singular values.""" + rng = np.random.default_rng(seed) + n = len(singular_values) + # Random orthonormal columns for U (m x n) and a random orthogonal Vt (n x n). + U, _ = np.linalg.qr(rng.standard_normal((m, n))) + Vt, _ = np.linalg.qr(rng.standard_normal((n, n))) + return U @ np.diag(singular_values) @ Vt + + +def test_get_cov_matches_jtj_on_well_conditioned(): + """SVD covariance equals the classic chi2_red * inv(JtJ) when well posed.""" + J = _jacobian_from_svd([2.0, 0.5, 1.0], m=8, seed=1) + y = np.zeros(8) + params = np.zeros(3) + residuals = np.array([0.1, -0.1, 0.2, -0.2, 0.1, -0.1, 0.05, -0.05]) + + cov, std = get_cov(y, residuals, params, J) + + dof = 8 - 3 + chi2_red = np.sum(residuals ** 2) / dof + cov_ref = chi2_red * np.linalg.inv(J.T @ J) + assert np.allclose(cov, cov_ref) + assert np.allclose(std, np.sqrt(np.diagonal(cov_ref))) + + +def test_get_cov_ill_conditioned_but_identifiable_is_finite(): + """A full-rank but ill-conditioned J (kappa ~ 1e8) stays finite. + + Forming JtJ would square the condition number (~1e16, ~float64 epsilon) and + make the direct inverse unreliable/NaN; the SVD path recovers a correct, + finite covariance. + """ + s = [1.0, 1e-8] + J = _jacobian_from_svd(s, m=6, seed=2) + y = np.zeros(6) + params = np.zeros(2) + residuals = np.full(6, 0.1) + + cov, std = get_cov(y, residuals, params, J) + assert np.all(np.isfinite(cov)) + assert np.all(np.isfinite(std)) + + # Matches the analytic V S^-2 Vt scaling (huge but finite variance along the + # weakly-constrained direction). + dof = 6 - 2 + chi2_red = np.sum(residuals ** 2) / dof + _, sv, Vt = np.linalg.svd(J, full_matrices=False) + cov_ref = chi2_red * (Vt.T / sv) @ (Vt.T / sv).T + assert np.allclose(cov, cov_ref) + + +def test_get_cov_genuinely_rank_deficient_is_nan(): + """A singular value below the rank threshold -> unidentified -> all-NaN.""" + J = _jacobian_from_svd([1.0, 1e-18], m=6, seed=3) + y = np.zeros(6) + params = np.zeros(2) + residuals = np.full(6, 0.1) + + cov, std = get_cov(y, residuals, params, J) + assert np.all(np.isnan(cov)) + assert np.all(np.isnan(std)) + + +def test_get_cov_underdetermined_is_nan(): + """Fewer observations than parameters -> not identifiable -> all-NaN.""" + J = np.array([[1.0, 2.0]]) # 1 obs, 2 params + y = np.array([1.0]) + params = np.array([0.0, 0.0]) + residuals = np.array([0.1]) + + cov, std = get_cov(y, residuals, params, J) + assert np.all(np.isnan(cov)) + assert np.all(np.isnan(std)) + + +def test_get_cov_nonfinite_jacobian_is_nan(): + """A non-finite Jacobian -> all-NaN (no spurious numbers).""" + J = np.array([[1.0], [np.nan]]) + y = np.array([1.0, 2.0]) + params = np.array([1.0]) + residuals = np.array([0.0, 0.0]) + + cov, std = get_cov(y, residuals, params, J) + assert np.all(np.isnan(cov)) + assert np.all(np.isnan(std)) diff --git a/tests/tfscreen/util/numerical/test_xfill.py b/tests/tfscreen/util/numerical/test_xfill.py index f2ed4e6..82747e3 100644 --- a/tests/tfscreen/util/numerical/test_xfill.py +++ b/tests/tfscreen/util/numerical/test_xfill.py @@ -106,4 +106,26 @@ def test_xfill_log_fallback_not_enough_points(): # Check it is linear: (0, 100) -> linear sequence # min 0, max 100. span 100. pad 10. range -10 to 110. assert np.isclose(result[0], -10.0) - assert np.isclose(result[-1], 110.0) \ No newline at end of file + assert np.isclose(result[-1], 110.0) +def test_xfill_min_value_clamps_lower_bound(): + """min_value floors the padded lower bound (no negative pad).""" + x = np.array([0., 1., 3., 10., 30., 100.]) + # Without a floor, the 10% pad drops the lower bound to -10. + unclamped = xfill(x, num_points=100, use_log=False) + assert unclamped.min() < 0 + + # With min_value=0 the lower bound is clamped to 0; observed points kept. + clamped = xfill(x, num_points=100, use_log=False, min_value=0.0) + assert clamped.min() == 0.0 + assert (clamped >= 0).all() + assert np.isin(x, clamped).all() + # Upper bound is unaffected by the lower clamp. + assert np.isclose(clamped.max(), 110.0) + +def test_xfill_min_value_noop_when_above_floor(): + """min_value has no effect when the padded lower bound is already above it.""" + x = np.array([10., 20., 50.]) + a = xfill(x, num_points=10, use_log=False, pad_by=0.1) + b = xfill(x, num_points=10, use_log=False, pad_by=0.1, min_value=0.0) + assert np.allclose(a, b) + assert np.isclose(b.min(), 6.0) From 30f3a81b047d68609b8f683456f12fc13dc65b96 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 19 Jul 2026 22:47:54 -0400 Subject: [PATCH 14/26] added toy thermo for intuition building --- examples/toy_thermo/toy_thermo.ipynb | 677 ++++++++++++++++++ src/tfscreen/simulate/toy_thermo/__init__.py | 22 + src/tfscreen/simulate/toy_thermo/core.py | 81 +++ src/tfscreen/simulate/toy_thermo/genotypes.py | 200 ++++++ src/tfscreen/simulate/toy_thermo/sampling.py | 57 ++ .../tfscreen/simulate/toy_thermo/test_core.py | 71 ++ .../simulate/toy_thermo/test_genotypes.py | 177 +++++ .../simulate/toy_thermo/test_integration.py | 42 ++ .../simulate/toy_thermo/test_sampling.py | 73 ++ 9 files changed, 1400 insertions(+) create mode 100644 examples/toy_thermo/toy_thermo.ipynb create mode 100644 src/tfscreen/simulate/toy_thermo/__init__.py create mode 100644 src/tfscreen/simulate/toy_thermo/core.py create mode 100644 src/tfscreen/simulate/toy_thermo/genotypes.py create mode 100644 src/tfscreen/simulate/toy_thermo/sampling.py create mode 100644 tests/tfscreen/simulate/toy_thermo/test_core.py create mode 100644 tests/tfscreen/simulate/toy_thermo/test_genotypes.py create mode 100644 tests/tfscreen/simulate/toy_thermo/test_integration.py create mode 100644 tests/tfscreen/simulate/toy_thermo/test_sampling.py diff --git a/examples/toy_thermo/toy_thermo.ipynb b/examples/toy_thermo/toy_thermo.ipynb new file mode 100644 index 0000000..1bdda25 --- /dev/null +++ b/examples/toy_thermo/toy_thermo.ipynb @@ -0,0 +1,677 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ce58e8d", + "metadata": {}, + "source": [ + "# Toy four-state thermodynamic model\n", + "\n", + "A small, self-contained simulator for building intuition about the curves that\n", + "`tfs-cat-response` and `tfs-extract-epistasis` extract. No JAX, no config files —\n", + "just NumPy/SciPy/pandas, meant to be poked at interactively.\n", + "\n", + "**The system** (a monomer TF, activity fixed at 1, pure repressor):\n", + "\n", + "$$\\mathrm{HD} \\rightleftharpoons \\mathrm{H} + \\mathrm{D} \\rightleftharpoons \\mathrm{L} + \\mathrm{D} \\rightleftharpoons \\mathrm{LE} + \\mathrm{D}$$\n", + "\n", + "- **H, L** — two protein conformations in intrinsic equilibrium.\n", + "- **D** — DNA (operator); the H conformation binds it to form **HD**.\n", + "- **E** — effector (the titrant); the L conformation binds it to form **LE**.\n", + "- **Observable** = $\\mathrm{HD}/(\\mathrm{HD}+\\mathrm{D})$, the fraction of DNA bound (repression).\n", + "\n", + "Three wild-type association constants set the system: `K_conf = [H]/[L]`,\n", + "`K_dna = [HD]/([H][D])`, `K_eff = [LE]/([L][E])`. As effector rises, protein is\n", + "pulled into LE, HD falls, and the observable drops — de-repression.\n", + "\n", + "Mutations perturb the **stability of each state** (HD, H, L, LE) independently in\n", + "units of kT (positive = destabilizing), with optional **in-state pairwise\n", + "epistasis**. That's the only epistasis in the model — yet, as we'll see, the\n", + "*observable* shows plenty of its own, purely from the nonlinear map." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3b8b8dab", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:48.109508Z", + "iopub.status.busy": "2026-07-20T02:46:48.109402Z", + "iopub.status.idle": "2026-07-20T02:46:50.721588Z", + "shell.execute_reply": "2026-07-20T02:46:50.721040Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/harmsm/miniconda3/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from tfscreen.simulate.toy_thermo import (\n", + " ThermoModel, MutationEffects, sample_effects,\n", + " build_titration_df, enumerate_genotypes, solve_species,\n", + ")\n", + "\n", + "# A wild-type system: strong DNA binding, moderate effector affinity, K_conf ~ 1.\n", + "model = ThermoModel(\n", + " ln_K_conf=0.0, # H and L equally populated in the apo state\n", + " ln_K_dna=18.0, # tight operator binding\n", + " ln_K_eff=14.0, # effector affinity\n", + " protein_total=1e-6, # all concentrations share one unit (say, M)\n", + " dna_total=1e-9, # operator is scarce relative to protein\n", + ")\n", + "\n", + "effector = np.logspace(-8, -2, 25) # titration grid" + ] + }, + { + "cell_type": "markdown", + "id": "7535054c", + "metadata": {}, + "source": [ + "## 1. The wild-type titration curve" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "53d83c46", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:50.724000Z", + "iopub.status.busy": "2026-07-20T02:46:50.723712Z", + "iopub.status.idle": "2026-07-20T02:46:50.982826Z", + "shell.execute_reply": "2026-07-20T02:46:50.982193Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "theta_wt = model.observable(effector, genotype=\"wt\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "ax.semilogx(effector, theta_wt, \"o-\", color=\"#2b6cb0\")\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"observable HD / (HD + D)\")\n", + "ax.set_title(\"Wild-type de-repression curve\")\n", + "ax.set_ylim(-0.02, 1.02)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "aed521d9", + "metadata": {}, + "source": [ + "## 2. What the protein is doing under the hood\n", + "\n", + "`solve_species` returns every free/bound concentration at one effector value, so\n", + "we can watch the population shift from operator-bound (HD) to effector-bound (LE)\n", + "as the titrant increases." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "91398efc", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:50.984509Z", + "iopub.status.busy": "2026-07-20T02:46:50.984396Z", + "iopub.status.idle": "2026-07-20T02:46:51.177712Z", + "shell.execute_reply": "2026-07-20T02:46:51.177083Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAF6CAYAAADxk+dgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAe7BJREFUeJzt3Xd8FMX7wPHP3l1y6Y0eIAmQUER6751QRaWKIiBiFxVsoFL8+ZViFxVBqgWl2VCqQuggKFhQikBC7yGFtCvz+wNzEtMul0vukjzv1ysvcruzM8/OHcmT3dkZTSmlEEIIIYQoQ3SuDkAIIYQQorhJAiSEEEKIMkcSICGEEEKUOZIACSGEEKLMkQRICCGEEGWOJEBCCCGEKHMkARJCCCFEmSMJkBBCCCHKHEmAhBBCCFHmSAIkhBuYOnUqmqYRExPj6lDcVnH0kbwPznP+/HlGjhxJ9erV0ev1aJrGtWvX8t0nRHGRBEiUWLGxsWialuXL09OT6tWrM3z4cH777bciazsmJgZN05g6dWqRtSGcT9634jNq1Cg++eQTOnbsyIsvvsiUKVPw8vLKd19R6dy5M5qmFWkbomQxuDoAIQqrVq1a3HPPPQAkJyeze/duPv/8c7788kt+/PFH2rVr5+II8/fYY48xbNgwwsLCXB1KmSbvg3NkZGSwceNGunfvzmeffWb3PiGKkyRAosSLjIzM9hf9iy++yP/+9z9eeOGFEnE7o3z58pQvX97VYZR58j44x/nz57FarYSGhhZonxDFSglRQp04cUIBKjo6Otu+8+fPK0D5+PjYtgGqU6dO6vTp02rEiBGqUqVKStM0tXnzZluZhQsXqpYtWypfX1/l6+urWrZsqRYtWpSl7ilTpiggx68TJ07YyqWnp6s33nhDNWnSRPn4+Cg/Pz/Vvn179c0332SLN7POm2PJPL+RI0eqo0ePqttvv10FBQUpHx8f1a1bN3XgwAG7+2rkyJEKUMeOHVMzZ85UkZGRymg0qoiICDVt2jSVkZGR43H29IdSSm3evFkBasqUKWrbtm2qU6dOys/PTwUGBqo777xTHT16NNsxme9HTsLDw1V4eHiWbTn1kVJKLViwQN12220qPDxcGY1GFRwcrHr27Kk2bdqU4/F5vW+5taGUUt9++63q3LmzCggIUF5eXqphw4bqjTfeUCaTKUs5Z71v9913nwLUli1bctz/xhtvKEDNmzfPtm3Tpk2qV69eqkqVKsrT01NVrFhRtW/fXs2dO9euNvNitVrVggULVNu2bZW/v7/y9vZWzZo1UwsWLMhSrlOnTjn28ciRI/PcV9B2bi6/cOFC1b59exUYGKi8vb1VZGSkeuCBB1RcXJxSSuX6vt/crlKOvcd//vmnuv3221VISEi2nwHCvckVIFGq/fee/5UrV2jTpg0hISEMGzaMtLQ0AgICABg3bhyzZ8+matWqjBkzBoBVq1YxevRo9u/fzzvvvAPcGEsQGxvLkiVL6NSpE507d7bVHxQUBEB6ejq9evUiJiaGxo0bM2bMGEwmE99//z0DBgxg9uzZPPbYY3adQ2xsLK1bt6Z+/frcd999HDt2jG+++YYuXbrw119/UalSJbv748knn2THjh0MGTIEPz8/Vq9ezZQpU/jtt99YuXJllrL29sfNdu/ezfTp0+nVqxePP/44Bw8e5KuvvmLbtm3s3r2bmjVr2h2rvR599FEaNWpE9+7dqVChAmfOnOHrr7+me/fufPnllwwYMACw733LzZtvvsmECRMICQlh+PDh+Pr68u233zJhwgS2bdvGl19+me2zVtj3bcSIESxcuJBPP/2Ujh07Ztv/ySefYDQaGTx4MADff/89/fv3JygoiAEDBlClShUuXbrEr7/+yieffMIDDzxgR2/mTCnF3Xffzeeff05UVBTDhw/H09OTjRs3MmbMGP78809ef/114Mb4nsaNG/POO+/QqFEjbr/9dgAaN27MtWvXct1X0HYArFYrQ4cOZeXKlVStWpW77rqLgIAAYmNjWb58Ob179yYsLIwpU6awePFi4uLimDJliu34zHbBsff477//pnXr1jRo0IBRo0Zx5coVPD09He5nUcxcnYEJ4ai8rgBNnjxZAapLly62bfzzV9/o0aOV2WzOUn7Lli0KUPXq1VPXrl2zbb969aqqXbu2AtTWrVtt22++4pGTSZMmKUC99NJLymq12rYnJiaq5s2bK09PT3XmzBnb9ryuAAFqxowZWep/8cUXFaCmT5+edyf9I/MKUIUKFdSpU6ds29PT01XHjh0VoFauXFno/gDUhx9+mKXtDz/8UAGqX79+WbbjpCtAx48fz3b82bNnVWhoqIqKisqyPb/3Lac2/v77b2UwGFTFihXVyZMnbdvT0tJU+/btFaA+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BfHDDz/w22+/MXnyZHmyJQeNGjXK8pnKnKtMZPfdd9+xa9cuu2bpLavCw8N58803+fXXX9m5cyeRkZFER0dz5coVV4fmVrZs2cLjjz/Onj17WLduHVevXqV3796YzWa765BB0KXArbfeyj333MPzzz9v21auXDneeOONbE/FiazuvvtuTp8+zZYtW1wdilt59tln2bNnj/RLPqZOncp3333Hvn37XB2K27tw4QLNmzdnzZo19O/fn6effprHHnvM1WG5vcTERAIDA4mJicl3AuCy7MSJE9SsWZNff/2Vhg0b2nWMXAFy0KeffsqDDz5I8+bNMRqNaJrG4sWL8zxm79699OnTh6CgIHx9fWndujXLly8vdCxt27blm2++4fz58yilWLFiBenp6W7zn8Wd+upmCQkJfPXVV4wePdqp9TrKnfpp9erVNG3alIEDB1KxYkVatGjBl19+Weh6ncGd+gngr7/+okqVKkRGRjJ69GjOnz/vlHoLy936afTo0YwbN44GDRo4pT5ncre+ypSRkcG8efMIDg52i35z136Cf9f9DAkJsf8gl03BWMKFh4crQJUvX972/aJFi3Itv2nTJuXh4aH8/f3V2LFj1fjx423Hvf7664WKJTU1Vd11110KUAaDQQUEBKj169cXqk5ncqe+utmcOXOUn5+fSk5OdlqdheFO/WQ0GpXRaFRTp05Vv/zyi3rttdeUTqdTW7ZsKVS9zuBO/bRmzRq1fPly9dtvv6m1a9eqFi1aqHr16qm0tLRC1esM7tRPs2fPVh07dlQWi8UW2+zZswtVpzO5U18ppdTWrVuVr6+v0ul0KjQ0VO3bt6/QdTqDu/VTJrPZrLp166b69OlToOMkAXLQxo0bVWxsrFJKqenTp+f5QTCZTKpWrVp5Lu2RWVem5557TgF5fmWaMWOGuuWWW9TatWvVgQMH1P/93/+poKAgdfDgQaeftyPcqa9u1qJFC3Xfffc55RydwZ36ycPDQ7Vv3z7L8bfddpu6++67nXOyheBO/fRfFy9eVF5eXmrlypWFPs/Ccpd++uuvv1TFihWzLF/kbgmQu/RVppSUFHX06FG1e/duNWbMGFWzZk116dIlp56zI9ytn5RSymq1qvvvv19FRUWpixcvFuh8JAFygvw+COvXr1eAGj16dLZ9ixcvVoCaNm1alu0XL15Uf/31V55fSt34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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "species = [solve_species(*model.genotype_ks(\"wt\"),\n", + " model.protein_total, model.dna_total, E)\n", + " for E in effector]\n", + "\n", + "frac = {s: np.array([sp[s] for sp in species]) / model.protein_total\n", + " for s in (\"HD\", \"H\", \"L\", \"LE\")}\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "ax.stackplot(effector, frac[\"HD\"], frac[\"H\"], frac[\"L\"], frac[\"LE\"],\n", + " labels=[\"HD (repressing)\", \"H (apo)\", \"L (apo)\", \"LE (effector-bound)\"],\n", + " colors=[\"#2b6cb0\", \"#63b3ed\", \"#f6ad55\", \"#dd6b20\"], alpha=0.9)\n", + "ax.set_xscale(\"log\")\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"fraction of protein\")\n", + "ax.set_title(\"Protein population vs. effector\")\n", + "ax.set_ylim(0, 1)\n", + "ax.legend(loc=\"center left\", fontsize=8)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "af295354", + "metadata": {}, + "source": [ + "## 3. Manual mutations as `ddg` kwargs\n", + "\n", + "For a quick one-off, pass a per-state `ddg` dict straight to `observable`. Each\n", + "value is a stability change in kT; omitted states are 0. Watch how perturbing\n", + "different states reshapes the curve differently:\n", + "\n", + "- destabilizing **HD** (+kT) weakens repression → the whole curve drops;\n", + "- stabilizing **LE** (−kT) makes effector bind more easily → the curve shifts left." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "287d1795", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:51.179507Z", + "iopub.status.busy": "2026-07-20T02:46:51.179370Z", + "iopub.status.idle": "2026-07-20T02:46:51.373684Z", + "shell.execute_reply": "2026-07-20T02:46:51.373031Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAF6CAYAAADxk+dgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAr0lJREFUeJzs3Xd4FMX/wPH3XpJL74E0SEKvCaH3joBU6YjSQQEB/SHSVBBUQL5gAVFApIlIB+md0Kv0XpMQUiCE9Hp3+/vjyEFIr5cyr+fZ53K7s7OfO47LJzOzM5IsyzKCIAiCIAgliELfAQiCIAiCIBQ0kQAJgiAIglDiiARIEARBEIQSRyRAgiAIgiCUOCIBEgRBEAShxBEJkCAIgiAIJY5IgARBEARBKHFEAiQIgiAIQokjEiBBEARBEEockQAJgiDk0qpVq5AkiVWrVunl+h4eHnh4eOjl2vrWqlUrJEnSdxhCESQSIKHYOHr0KP369aNs2bIYGxtjZ2dHs2bN+Omnn4iPj9d3eDni4+ODJEl88803eVJfSflloe+EpDj55ptvkCQJHx+fEnl9ofgy1HcAgpBbKpWKTz75hGXLlmFubs67775LxYoViYiI4MCBA0yYMIElS5awe/duKlasqO9wBUHIQ2vWrCE2NlbfYQhFkEiAhCJv6tSpLFu2jPr167Nt2zZcXV11x9RqNbNmzWLWrFl07NiRS5cuYWVlpcdoBUHIS25ubvoOQSiqZEEowu7evSsrFArZzs5ODg4OTrfcgAEDZED++uuvU+x3d3eX3d3d5aioKHn8+PGys7OzrFQqZU9PT3nTpk1p1pWQkCAvWLBArl27tmxmZiZbWFjIzZo1k//9998sx61Wq+U//vhDrl+/vmxrayubmJjIrq6ucpcuXeSjR4/KsizLM2bMkIE0t8ePH+te/xdffCHXrl1btrOzk42NjeVKlSrJkydPlqOiolJcM726Bg8enKLc1atX5X79+slOTk6ykZGR7ObmJo8dO1YODQ3N8utLfl9fvnwpf/TRR7Kjo6NsbGwse3t7y+vWrUvzHI1GI//5559ykyZNZEtLS9nU1FSuW7eu/Oeff6Yqm/zeHD16VF65cqVcu3Zt2dTUVG7ZsqU8ePDgdF/r2/GlpWXLlvLbX40ZXU+WZXnlypUyIK9cuVLevn27XL9+fdnU1FR2cHCQhw4dmu5n89GjR/Lw4cPlsmXLykqlUnZycpIHDx4s+/r6pll++/btcr169WQTExO5dOnS8ogRI+SwsLAMX8/bwsPD5blz58otWrSQnZ2dZSMjI9nZ2VkeOHCg/ODBgzTfi7e3rFwrO5/N9GTl+mn9e73577Fjxw65QYMGsqmpqezi4iJ/9dVXslqtlmVZlletWiV7eXnJJiYmctmyZeV58+alGUd2PptC0SFagIQibfXq1Wg0Gj766CMcHR3TLff111+zbt06VqxYwaxZs1IcS0pKon379rx8+ZJevXoRGxvL+vXr6du3L/v27aN9+/a6sgkJCXTs2BEfHx+8vb0ZPnw4SUlJ7N69m+7du7No0SLGjh2badxTp05l3rx5VKhQgQEDBmBpacnTp085efIkhw4dolWrVrRq1QpfX19Wr15Ny5YtadWqle58GxsbALZu3cqff/5J69atadWqFRqNhrNnz/LDDz9w7Ngxjh8/jpGREQAzZsxg1apV+Pn5MWPGDF1d3t7eup937NhB3759USgUdO/enbJly3Lr1i1+/fVX9u/fz7lz57C1tc309QEkJibSrl07oqOjGThwIDExMWzcuJEBAwYQGhrKuHHjdGVlWeaDDz7gn3/+oVKlSgwYMAClUsnBgwcZPnw4t27dYv78+amu8b///Y+jR4/SvXt32rdvj4GBAfXr1yc8PJx///2X7t27p3h9uZXW9d60ZcsW9u/fT+/evWnXrh1nz55l5cqVnDhxgvPnz6d4786dO0eHDh2IiYmhS5cuVKpUCV9fX/7++2/27t3LmTNnKF++vK78mjVrGDx4MFZWVgwcOBAbGxt27dpFu3btSExMRKlUZuk13L59m+nTp9O6dWt69OiBubk5d+7cYd26dezevZtLly7h7u4OwJAhQwA4duwYgwcP1g20Tv78ZSQ7n8305Ob6ANu2bePAgQO89957NG3alN27d/Pdd98hyzLW1tZ89913dO/enVatWrFlyxYmTZqEo6MjgwYN0tWR08+mUAToOQEThFxp1aqVDMgHDx7MtKyLi4sMyP7+/rp97u7uMiB3795dTkhI0O0/dOiQDMgdOnRIUce0adN0LUkajUa3PzIyUq5Xr56sVCrlp0+fZhqLnZ2d7OLiIsfExKQ69uLFC93PR48elQF5xowZadYTEBCQIu5kM2fOlAF57dq1Kfan9ddystDQUNnKykp2dXVN1QLxzz//yIA8duzYzF6aLMuv39cWLVqkiO/Jkyeyg4ODbGxsLAcEBOj2L1u2TAbkoUOHyomJibr9CQkJcteuXWVAvnjxom5/couMubm5fO3atVTXf7MFIL34ctIClNn1AHnfvn0pjk2ZMiXVe5eYmCh7eHjIlpaW8qVLl1KUP3HihGxgYCB36dJFty8iIkK2srKSzc3N5bt376aop0WLFllulZFlbQvQm5+xZEeOHJEVCoU8YsSINF97cstkVmX3s5mezK6fUQuQkZGRfP78ed3+yMhIuXTp0rKZmZns5OQkP3z4UHfM399f1/r7pux+NoWiQ9wFJhRpwcHBAJQtWzbTssllgoKCUh376aefUvwF3bZtW9zd3blw4YJun0aj4ffff6dChQrMnDkzxd1UlpaWTJ8+ncTERLZu3Zql2JVKZaoWBAA7O7ssnQ/g6uqa5l/+ya1Qhw4dynJda9asITIykjlz5uhaAJL179+fOnXqsH79+izXBzB79uwU8ZUpU4ZPP/2UhISEFHX9+uuvmJubs3jx4hStAkqlku+//x6Af/75J1X9H330EZ6entmKKTcyu167du3o0KFDin1ffvklNjY2rFmzBo1GA8CuXbvw9fXliy++oHbt2inKN2vWjO7du7Nnzx4iIyMB2L59O5GRkQwbNozKlSvryhoZGenen6yytrZO8zPWunVratSoka3PTEby8rOZUx9++CH169fXPbe0tKRLly7ExsYyevToFC1sZcuWpVmzZty6dQuVSqXbn9PPplD4iS4wocSzsbGhXLlyqfaXKVOGM2fO6J7fvXuXly9f4uLiwsyZM1OVf/78OQB37tzJ9Jr9+/fnt99+o2bNmvTv35/WrVvTuHFjTE1NsxW7LMusXLmSVatWcePGDSIiInS/ZAECAwOzXNfZs2cBbdfMw4cPUx2Pj48nNDSU0NBQHBwcMq3P0NCQxo0bp9rfvHlzAC5fvgxAbGws169fx8XFhR9++CFV+aSkJCDt97VBgwaZxpGXMrte8mt7k4WFBd7e3vj4+PDo0SMqVqyoe6/v3r2b5hQHwcHBaDQa7t27R7169bh69Wq69Tdu3BhDw+x9lfv4+PDzzz9z7tw5QkNDU/zCz2pXWmby8rOZU2l1fzo7O2d4TK1WExISgqura64+m0LhJxIgoUhzcnLizp07PHnyhCpVqmRY9smTJ8DrL8Bk1tbWaZY3NDRM8YUdFhYGwM2bN7l582a614mJick07l9++YVy5cqxcuVKvvvuO7777jtMTEzo27cvCxYsyFKCATB+/Hh+/fVXypYtS7du3XB2dsbY2BiAmTNnkpCQkKV64PXrW7x4cYblYmJishSfg4MDCkXqRubksVoREREAvHz5ElmWefr0aZqJ5ZvXTa+ugpLZ9dI7/vZrTn6v//777wzrS37NyeeVLl06VRkDAwPs7e0zrOdNmzZtol+/flhYWNChQwc8PDwwMzPTzZvk5+eX5boykpefzZxK647P5GQxo2PJiU1uPptC4ScSIKFIa9KkCT4+Phw+fJh27dqlW+7OnTsEBgbi6uqape6ytCR/Yfbq1YvNmzfnqI5khoaGTJw4kYkTJxIYGMixY8dYuXIla9asITg4mP3792dax7Nnz1i8eDFeXl6cOXMGMzMz3bHg4OAMv7DTkvz6rl+/Ts2aNbP3gtIQGhqKRqNJlQSFhIQArxPP5OvWrVuXixcvZusaOZ3UUaFQkJiYmOax5GQjJ9dLfm3p7X/7Ne/cuZMuXbpkGm/yec+ePUt1TK1W8+LFixTTP2Tkm2++wcTEhP/++49KlSqlOJbdLs705PVnU19y89kUCj8xBkgo0gYNGoRCoeCPP/7QdUGlJbmvftiwYTm+VrVq1bCysuLixYu6vxDzgouLC++//z779u2jYsWKHDp0iLi4OADdGCG1Wp3qvEePHiHLMu3atUvxCwbgxIkTaV4ro/oaNmwIkKLbLzdUKlWadSXHljz2xdLSkmrVqnH79m3Cw8Pz5NoZvU4AW1tbnj17lqLrB7R/yd+/fz/H103rfY+OjubKlStYWVnpxpxk972uVatWuvWfOXMm1evIyMOHD6lWrVqq5CcoKIhHjx6lKp/Ze5mWnHw205OT6+eV/PhsCoWHSICEIq1KlSp8+umnvHjxgq5du6Ya4KzRaPj2229Zu3YtFSpUYOLEiTm+lqGhIaNHj8bPz4+JEyemmQTduHEjzb/S35SQkMDp06dT7Y+JiSE6OhojIyNdq0nyYNXk7rs3JQ9UPn36dIquuoCAAKZOnZrmtTOqb+jQoVhaWvLll1+m2cUXGxurG7uSVdOmTUvR0hIQEMAvv/yCsbEx/fv31+0fP348sbGxjBw5Ms3uhMePH+Pr65vl62b0OgHq169PUlJSii4oWZaZOnVqrrozDh06lKr17vvvvyc8PFyXrAN0794dNzc3fvzxR44fP56qnqSkJE6ePKl73r17d6ysrFixYgX37t1LUe6rr77KVozu7u48ePAgRWtVfHw8o0ePTvMzndl7md41IHufzfTk5Pp5Ka8/m0LhIbrAhCJv3rx5REREsGLFCipVqkTnzp2pUKECkZGRHDhwgPv371OpUiX27NmT61mgZ86cyaVLl1i4cCG7d++mRYsWlC5dmqdPn3L9+nWuXr3KmTNn0hyrkSwuLo6mTZtSuXJl6tati5ubG9HR0ezatYvg4GAmTpyoGytRtWpVXFxcWL9+PcbGxpQpUwZJkhg3bhzOzs706tWLLVu2UK9ePdq2bUtISAi7du2ibdu2aQ5kbtOmDZs3b6ZXr168++67mJiYUKtWLbp27UqpUqX4559/6NOnD7Vq1aJjx45UrVqVhIQEfH19OXbsGE2aNGHfvn1Zeq+cnZ2JiYnBy8uLrl276uYBevHiBQsXLkzRZfPxxx9z9uxZVq9ezalTp2jXrh0uLi6EhIRw584dzp07x7p167K84GfygPKff/6Zly9fUqpUKQBdsjB27FhWrlzJiBEjOHjwIKVKleLEiROEh4dTq1Yt3aDj7OrSpQtdu3ald+/eeHh4cPbsWY4ePUqFChVSzD9lbGzM5s2beffdd2nZsiVt2rTB09MTSZLw8/PjxIkT2Nvb6wbXWltbs3DhQoYMGUL9+vXp378/1tbW7Nq1C1NT01Tj2jIybtw4xo0bR+3atenduzcqlYqDBw8iy3Kar71169ZIksS0adO4efMm1tbW2NjYZDjfVU4+m+nJyfXzUl5/NoVCRH934AtC3jp48KDcp08f2cXFRTYyMpJtbGzkxo0bywsWLJBjY2PTPCe788HIsiyrVCp56dKlctOmTWUrKyvZ2NhYdnNzkzt27Cj//vvvcnR0dIZxJiYmyj/88IPcvn17uUyZMrJSqZQdHR3lFi1ayOvWrUsxv5Asy/LZs2flli1bypaWlqlmgo6KipI///xz2cPDQzfT7rfffisnJibKgG6m4mRJSUnypEmTZDc3N9nQ0DDNmaDv3LkjDx8+XHZ3d5eVSqVsa2sre3p6yuPHj08xp0pGkt/XsLCwFDNB16pVK92ZoGVZljds2CC3a9dOtrW1lY2MjGRXV1e5VatW8oIFC+Tnz5/rymVlbprdu3frZmROft/edOTIEblhw4aysbGxbG9vLw8cOFAOCQnJdCbotKQ3E7S9vb08ZMgQOSgoKM3zAgIC5E8//VSuVKmSbGxsLFtZWcnVqlWTR4wYIR8+fDhV+W3btsl169aVjY2NczwTtEajkZcsWSLXqFFDNjExkZ2cnOThw4fLz549S/czv2rVKtnT01M2NjbO8pxD2f1sZiSj62c2E/TbMvq3TJ5FPPn/15uy+tkUig5JlmW5gHMuQRCKueS/hkXXgCAIhZUYAyQIgiAIQokjEiBBEARBEEockQAJgiAIglDiiDFAgiAIgiCUOKIFSBAEQRCEEkckQIIgCIIglDiFdiLEtWvXcuLECf777z+uX79OYmIiK1euZMiQIdmqR6PRsHjxYpYtW8aDBw+wsLCgXbt2fP/997pp6bNTV2BgIJaWljleg0gQBEEQhPwhyzJRUVG4uLikuRjz24ULJXd3dxmQHRwcdD+nNalVZkaMGCEDco0aNeRJkybJH374oaxUKmU7Ozv53r172arryZMnugnVxCY2sYlNbGITW+Hcnjx5kunv9ELbArR8+XIqVaqEu7s7c+fOzfb6MQBHjx5l+fLltGjRgoMHD6JUKgEYMGAAnTp1YuzYsVladTuZpaUloF2TJrdLKgiCIAiCkLciIyMpW7as7vd1RgptAtSuXbtc1/HHH38A8O233+qSH4B3332XVq1aceDAAfz9/XFzc8tSfcndXlZWViIBEgRBEIRCKivDVIr1IGgfHx/Mzc1p2rRpqmMdOnQA4NixYwUdliAIgiAIelZsE6CYmBiCgoIoV64cBgYGqY5XqlQJgPv37xd0aIIgCIIg6Fmh7QLLrYiICACsra3TPJ7chZVcLi0JCQkkJCTonkdGRuZhhIIgCCWPWq0mKSlJ32EIxYCRkVGaDRxZVWwToLwwZ84cZs6cqe8wBEEQioXo6GgCAgKQxQIEQh6QJIkyZcpgYWGRo/OLbQKU3PKTXgtPcmtOei1EAFOnTmXChAkpzilbtmweRikIglAyqNVqAgICMDMzo1SpUmIuNSFXZFnm+fPnBAQEUKlSpRy1BBXbBMjc3BxnZ2ceP36MWq1O9eYkj/1JHguUFmNjY4yNjfM1TkEQhJIgKSkJWZYpVaoUpqam+g5HKAZKlSqFr68vSUlJOUqAiu0gaICWLVsSExPDqVOnUh1Lnv+nRYsWBR2WIAhCiSVafoS8ktvPUrFIgEJDQ7lz5w6hoaEp9n/00UcAfP311yQmJur27927Fx8fH9q3b4+7u3uBxioIgiAUHT///DPBwcH6DkPIB4U2AVq+fDlDhgxhyJAhbNq0KdW+5cuX68r++uuvVKtWjV9//TVFHa1bt2bEiBEcP36cOnXqMHnyZAYNGsR7772HnZ0dixYtKtDXlJ5rwb6s/O8g14J9i2VdhTGmoIg4Tj8MJSgiLtcxCYKQv/T5/1UkQMVXoR0DdPLkSVavXp1i36lTp1J0Z40YMSLTepYuXYqnpyfLli3jl19+wcLCgh49evD9999ToUKFPI87u6Yc+INdgYuQJBn5OjgqmlDTvhYgIfFm8572+esmP+3R5OcSElee/0eQ5jiSBPJ1cFG0oq5jAxSSNs9VoECSAMlAe+4bdSpQgCSh0Bbg9NNT+Kr2vopLorxRV9qUbY1CYYACBQpJgYFCgcTrR4VkgIGkQCFJSJICBQZsv7+LG/FrdfXUNhvG4Jr9tedIvCqb8vH1z9rXp5C0r/W3/9ZxKnyJrq6WdqOZ2GQQhgoJhULCQJJQKMBQodD9bKDQnm+okDBQaF/rhgv+TNtxEoxCIcmB2d2a0a9+1mYDT0tQRByPQ2Mo52COs3XOxzbkVT2CUNjJskxckjpLZbf8F8CMHTfRyNrvhJndatCrbplMzzM1Msi0i2TZsmVcvHiRZcuWcevWLWrUqMH+/ftp3749s2bNAiAwMJB+/fphamrKqlWr8Pb2zlLcQuEnyeJ+xCyLjIzE2tqaiIiIPFkK41qwLwP2dUOSSt4/gSwrQDYAWYEsGwDan1Pue+M5MgamT3jz+0yWISm8LrLaEmRDZI0SZCNkjRFojJDl5EdliueGljcwdtytTaRkiYSgnthpmmGiNMDIQPFqk1I8Kg0UGL7xs5GBAiNDicehMZx+8AIZkICutVxoVtEBU6UBZkoDTJUGmBoZYKY0TPHc1MgAheL1i9lwwZ+pW6/rvuTn9PTMVVImCIVNfHw8jx8/ply5cmgUhlSfnvV1GHPi1qwOmCkz/hv/0aNHtGvXjkePHvHLL7+wYcMGmjVrxrx582jevDk//PADAwYMYPv27SLxKYTe/EyZmJgA2fs9XWhbgEqC/57eTzP5MdNUwMzQGu2itgAyMm+Wk9GmrbLuaJw6iniFX6q6jDWuGCvMSV4kV3uG5o0z5RSPIJOoiUOleJGqLoXGEgPJ8FV5je7xzZ/ffETSpPvaJUmjO57TYWySBErb/3J4dnIdMsbOW4iMu0CEyhpZZY6cZI4cb6H9Wf1qU5kjq82A1HcaSIYRGChD0SQ6sOMq7LgamKVrmxgpMFMaojRQEBwZr9uvkWHKluvcCYqirJ0Z9hZKbM2U2Jm/3kyMMr7jQbQmCULmypcvD2gToUOHDjFnzhw+//xzoqOjuXXrFg0aNNBzhEJ+EgmQHtV1rYR8XUqRBMmyxB/v/oyXk0e26kqrNUmWJVa8uyTP6vrr3XXZqiu9en5vvYbKDs6oZBUqzRvb28/f2B69DOKXKz+kagF6r3wfLE2MSVAnEK+KJ14dT7wqnjhVHHGq+BT74tXxxCXFoSFl07skgaGZfxZekYSJwgITAytMFFZokswJjHqBgdljbbejLJEQ1IMaVu9gbGhAfJKa2ETtFpekJjZRRXzS66QwPklDfFJimleSgZWnfdONxNTIQJcM2ZorsTMzws7cGDtzIx4+j2b75UBkRGuSUDiZGhlwa1aHTMsFR8TT7sdjaN74+08hwaEJLXGyNsn0GlnRrl079u7dy/3792nZsiWyLLNlyxYaN26MoaH4FVmciX9dPfJy8qCLy7jXY4BkiS4u47KdsBTWutKrp7m7d7ZjausOD5/FparruxYjs1VPcEww7Te3T9GiJiExpcEUZGTC4sN4Gf+Sl/EvtT8naH+OSIhARiZeE0W8Jgp4CoCh+eu6ta1JW7Es5U+TMg3wcvCipoM31savJ9vUaGTiVa+SolfJ0ZOXsYxcc5E3O6MloEdtVxLUGl7GJBL2ansZm0iSWjt+4ml4HE/DMx4UqpFh8pbr/HPuCVWcLClfypxyDuaUL2WBm50ZSsNCex+EUIxJkpRp9xRA+VIWzOnpybStN1DLMgaSxOyeNSlfKmcz/6alXbt2TJo0STclSps2bZgxYwafffYZoF02KaMlk4SiS4wByoa8HgOU7FqwL5eePqCOa8UcJSyFva7CFtPW+1uZeWYmGlmDQlIwo/EMelbqmeE5Ko2KiIQIbXKUoE2OLodc5u87f2d6PQ8rD7xKeeHl4IVnKU8q2VbCSGGUosyGC/6pvuTTarWRZZnoBFWKhCgsJomwmATCYpK4ExSJz73nWXofDBQSZW1NKV/KgvIO5pQrZU55BwsqlDKnlKWxbgCp6E4T8kJa4zWyKigiDt/QWDwczPL8M/jixQtKlSrFqlWrGDRoEAcOHKBDhw5cu3YNT09Pli9fzrx58zAzMxODoAuZ3I4BEglQNuRXAiQUvOCYYJ5EPaGsZVmczJ1yXEeHLR3QyK+7tRSSgtG1RuMX6ce159fwj0rdtWZiYEJ1++p4OnhqE6NSXjiaOXI9xI//nj6gbi6Su6CIOJrOPZKqy2BG1+qExSTxKDSGx6HRPHoeQ2xi+nfhWBgbUs7BHAMFXH0SIbrThFzLTQIkCGkRCVABEgmQ8LbMWpPC48O5Hnqda6HXuP5c+xiVGJWqHgsjC6KTogHtdAUzmmTeKpWerLQmybJMSGQCj14lQ4+ev0qMQmN4EhabIoF625SOVele20W0BgnZIhIgIa+JBKgAiQRISEt2WpM0sga/SD9tUvT8GteeX+Nu2F00pLxjTiEp2N9rf45bp3LTZZCgUvMkLJY914P58eC9dMtVcbSkZZVStKpciroethgbZn8tHqHkEAmQkNdEAlSARAIk5IcTAScYc3hMqv0jao7g07qf6iEirbS60ySguosVt4MiU+w3UxrQpIIDraqUomXlUpS1MyvweIXCTSRAQl4T8wAJQhFXybYSCkmRYiwRwPIby4lIjGByg8kYGxgXeFzO1qZp3oHTr74bL2MSOfEglGN3n3Ps3nNCoxM4dDuEQ7dDAKhQypyWlUvTqkopGpSzw8TIQAymFgShUBEtQNkgWoCE/PL2WKIWri04FnAMGZlqdtVY0GoBZS3L6iW2zLrTNBqZW0GRHLv3HJ+7z7jkH476jeYhEyMF7nZm3AuJFoOpSzDRAiTkNdEFVoBEAiTkp7fHEp1+eprJJyYTnhCOpdKS75t+T2u31voOM1MRcUmcehCKz91nHLv3nJDIhFRlFBKcmtJGtASVICIBEvJabhMgMQuaIBQSTuZO1Heqrxv43MS1CZu6bsKrlBdRiVGMPzqeH//7EZVGpedIM2ZtakQnT2fm9a7F2altmdvTM1UZjQxTt1zjSVisHiIUBEEQCZAgFGpO5k6s6rCKD6t9CMDKGysZcWAEz2OzNtmhvkmSRMsqpVCkseCbz71QWs/3YerWawS8FImQoH8ODg74+vrm6FxfX1+WLFmS5fKSJBEeHp7mMW9vb6KitNNleHh4cOXKFQBGjBjB0aNHcxTf23x8fFJN6ujr64uNjY3uuYeHB1WqVKFWrVpUrFiR7t27c/r06Qzr7dOnD2fOnMlyHI8fP6Zu3bp4e3tTs2ZN+vTpw8uXLwEICQmhQYMGqFT580efSIAEoZAzMjBicoPJLGi5AHMjc/4L+Y8+O/twPui8vkPLkuTB1AavZpY2kCRGt6pA80oOqDQy/5x/Quv5Pny1/TpBERkv7SGUUBFP4fFx7WMhld0EKCNXrlzB0tIy1f7ly5fTunXBdoNv2LCBq1ev8uDBAwYPHkynTp04d+5cmmXPnz9PWFgYjRs3TnVs1apVfPPNN6n2u7i4cPLkSa5cucKNGzdwcXHRlXN0dKRJkyasWbMmL1+SjkiABKGIaO/RnvWd11PZtjIv4l8w8uBI/rj2R6q7xwqjfvXdODmlNf+MbMTJKa2Z3LEqfw1vyKZRjWlSwZ4ktczas/60nOfDjH9vEBIZr++Qhfwky5AYk7Xt/B/wc01Y3VX7eP6PrJ2XheGtO3bsoFq1anh5eTFp0qQUx+7fv0/nzp2pX78+Xl5e/PrrrwDExcXRr18/qlevTq1atWjfvj0Ao0aN4u7du3h7e9OtWzcAJk6cSP369fH29qZFixbcvXs3xTXmz59P7dq1qVy5Mn///XpZnfRah1q1asX27dsBqFevHt7e3nh7e+Pi4qJLjIKDg+nbty8NGjTA09OTr776Kmv/JlnQs2dPRo0axfz589M8vnTpUgYMGJCtOo2NjTE11Y4FVKvVxMTE6JbhAXj//fdZunRpzoPOgLgNXhCKEA9rD9Z2Wsv3Z7/n34f/svDyQi4/u8yc5nNSLLpaGDlbm6Ya9Fzfw451Ixtx5uELfjp0j/OPw1h9xo/1F57wQUN3RrUqT2lLMWC22EmKhdku2T9P1sCeidotM9MCQWme7uFnz54xdOhQTpw4QfXq1Vm2bBkvXrwAtL+I33//fdauXUvVqlWJjY2lUaNGNGzYkICAAMLDw7l16xYAYWFhACxZsoTPPvtM110FMHnyZF2ysH79ej799FP27dunOy5JEpcvX+bRo0fUq1ePpk2b4uHhkaW34uLFiwAEBQXRokULXaIzePBgpk2bRsuWLVGpVHTp0oVNmzbRp0+fVHUkJ2zJEhMTM71uw4YN2bFjR5rHfHx8+L//+78sxf+mxMREGjRogJ+fH15eXinqr1u3LteuXSMyMjLPbz4SCZAgFDGmhqZ81+w76jrW5ftz33Pi6Qn67uzLglYLqOlQU9/h5UjjCvY0Kt+I0w9f8OPBe/zn95IVpx6z7rwfAxu583HLCjhYFPxcSELxdfbsWby8vKhevToAw4cPZ9y4cYA2Mbh58yb9+/fXlY+KiuLWrVs0b96c27dvM2bMGFq2bEmnTp3SvcbBgwdZtGgRUVFRaDQaXbKUbMSIEQCUL1+eFi1acPz48SwnQMkxde7cmenTp9O2bVtiYmI4fPgwISEhujLR0dGpWp6SValSJUXC5uvrm+lirxndOB4QEICjo6Pu+ebNm/nuu+8AbaKYmJioa8EaNmwY48ePB0CpVHLlyhUSExMZN24cS5cu1bXIGRoaYmtrS2BgoEiABEHQ6lGpB9XsqzHBZwJPop4wcO9AJtWfRP8q/VM0IRcVkiTRtKIDTSrYc+J+KD8evMeVJ+H8ceIxa8/6M7iJBx+1KI+duVJMqljUGZlpW2gyExkIixtoW36SSQbwyTmwyqQFySh7s5G/+X9GlmXs7OxSJAdvunXrFkeOHOHQoUNMmjQpzXL+/v6MHTuWCxcuUKFCBa5du0aLFi2yHENmVCoVffr0oVevXgwcOFAXN2iTu/yaauDChQvUrJn2H1pmZmbEx7/uvu7duze9e/cGtGOAfH190xwHlEypVDJ06FBGjhyZoksyPj5e102Wl8QYIEEowqraVWVDlw20c2uHSqNi9rnZTD4+mccRjzkfdJ7gmGB9h5htkiTRonIpto1pwsqh9fEqY01ckpolxx7S/IcjDFt5gaZzjzDgj3M0nXuEDRf89R2ykF2SpO2eymxzqARdf9EmPaB97Pqzdn9m52aSTDRu3Jhr165x584dAFasWKHrAqpSpQpWVlasXLlSV/7BgweEhYUREBCAJEl069aN+fPnI8syT548wcrKioiICF35iIgIjIyMcHZ2RpZl3RiiNyXX7+vry4kTJ2jevHmW38KPP/4YNzc3vvzyS90+CwsLWrduzdy5c3X7AgMDCQgIyHK9Gfn333/5/fff+fzzz9M87uXllW5rU3r8/PyIjdXeBarRaNi0aRNeXl664yEhIUiSRNmyeT8RrGgBEoQizlJpyY+tfuSvW3/x038/sdd3L3t99wKkuUJ9USFJEq2rlKZV5VIcvv2Mnw7d42ZgJEfuPtOV0cgwbesNWlQuJVqCiqs6g6BCWwh7BHblwdo1T6otVaoUK1asoEePHiiVSjp27Ii9vT2g7XbZtWsXn332GT/99BNqtRoHBwfWrVvH9evXmTp1KrIso1KpGDhwIF5eXqhUKmrUqEHNmjUpX748O3bsoH///tSoUQN7e3vee++9VDGo1Wpq165NTEwMCxcuzHL3l5+fHytWrKBmzZq6Lqt69eqxfPly/v77byZMmEDNmjWRJAlzc3OWLl1KmTJlcvQ+9evXDxMTE2JiYqhevTp79uyhYcOGaZbt3bs3+/fvp127dlmu/9q1a7okTqPRUKdOHRYuXKg7vm/fPnr06IFCkfftNWIm6GwQM0ELhd0RvyN86pNyAdXcrixfWMiyzKIjD9Jcof6fkY1oXMFeD1EJWSVmgi7+oqOjadKkCWfOnMHcPP0B6NnRvHlzli1bRrVq1VIdEzNBC4KgY6G0SLVPI2t4EvVED9HkLUmS6FOvTJqTKtpbKAs+IEEQUrCwsOCnn37i8ePHeVJfSEgIo0ePTjP5yQsiARKEYsTNyg2FlPK/tUJS6G0h1bz29qSKycb8fQm/FzF6ikoQhGRt27ZNd5B0djk6OmZ7XqHsEAmQIBQjTuZOzGg8I0US1K1CtyLf/fWmNydVXDW0Pk5WJjx4Fs17i09x7tELfYcnCEIRIRIgQShmelbqyf5e++lVqRcA159fLxKzRWeHs7UpjSvY06pKaf4d2xSvMta8jE3iwz/PsfFi0e/uEwQh/4kESBCKISdzJz6v9zkWRhY8jHjI8YDj+g4p3zhambDho8Z09nImSS0zafM15uy5jVoj7u8QBCF9IgEShGLKUmlJnyra6e9X3Fih52jyl6nSgEX9azO+bSUAlh5/xMd//UdMQv6sIi0IQtEnEiBBKMYGVhuIkcKIy88uc/nZZX2Hk68UCokJ71Tml/7eKA0VHLodQu8lZ3gaLlaYFwQhNZEACUIxVsqsFN0qaFemXnG9eLcCJevu7cr6jxrhYGHM7aBIuv96isv+L/UdllBE+Pr6smTJkiyXT2/ldgBvb2+ioqIA8PDw0C2ZMWLECI4ePZrbUAHtAqTprd8lSRKenp66VeO9vb11C76+bdeuXYwaNSrNY48fP6Zu3bp4e3tTs2ZN+vTpw8uX2v9TISEhNGjQAJWq6LW2igRIEIq5ITWGICHhE+DDg5cP9B1OgajjZsu/Y5tS1cmS0OgE+i07y46rWVh7SiiUgmOCC2xpl+wmQBm5cuUKlpaWqfYvX76c1q1b58k1MnPixAmuXLmi25Jnu37b1KlTmTp1aprHXFxcOHnyJFeuXOHGjRu4uLjo1vRydHSkSZMmrFmzJr9eQr4RCZAgFHMe1h60dWsLwMqbKzMpXXy42piyeXQT2lUrTaJKw/h/LvPjwXsZrmYtFAxZlolNis3Stv7Oejps7sDwA8PpsLkD6++sz9J5mf07x8XF0a9fP6pXr06tWrVo3749AKNGjeLu3bt4e3vTrZu29XTixInUr18fb29vWrRokWq9q/nz51O7dm0qV67M33//rdufXutQq1atdKui16tXT9c64+LiokuMgoOD6du3Lw0aNMDT05Ovvvoqp293pk6cOIGNjQ3u7u5pHjc2NtYtRqpWq4mJiUmxcOv777/P0qVL8y2+/CLWAhOEEmBozaEc8j/Enkd7GFd7XLGaFygjFsaGLB1Yj3n77rD0+CMWHr7Pw+fRLOhTCxMjA32HV2LFqeJouC7t9aQyokHD9+e+5/tz32da9tyAc5hlsCL8vn37CA8P59atWwCEhYUBsGTJEj777LMUK7xPnjyZ+fPnA7B+/Xo+/fRT9u3bpzsuSRKXL1/m0aNH1KtXj6ZNm2Z5Xa+LFy8CEBQURIsWLXSJzuDBg5k2bRotW7ZEpVLRpUsXNm3aRJ8+fbJUb7LmzZtjYKD9rNva2qbZ9ebj45Pu+l7JEhMTadCgAX5+fnh5ebFjxw7dsbp163Lt2jUiIyOL1DJRIgEShBLAq5QX9RzrcTHkImturWFS/Un6DqnAGCgkpnaqRoVSFny5/Tq7rwUREBbLH4PqUdpKrElVUtWqVYvbt28zZswYWrZsSadOndIte/DgQRYtWkRUVBQajUaXLCUbMWIEAOXLl6dFixYcP348ywkQQFRUFJ07d2b69Om0bduWmJgYDh8+TEhIiK5MdHR0tldah9etOxkJCAigYsWKGZZRKpVcuXKFxMRExo0bx9KlS5k0Sfs9YmhoiK2tLYGBgSIBEgSh8BlWcxgXQy6y+d5mPvb6GGtja32HVKD61i+Lm70Zo9b+x9WACLovPsWcnp4oDRWUczAXq8kXIFNDU84NOJdpuZDYEN7b/h4aXk/kqZAUbO++HUczx0yvkZHy5ctz69Ytjhw5wqFDh5g0aVKKVp9k/v7+jB07lgsXLlChQgWuXbtGixYtMqxbktJYsC4dKpWKPn360KtXLwYOHAig6747e/ZsgSwca2ZmRnx8vO75+PHjOX5cO3fYX3/9haenp+6YUqlk6NChjBw5UpcAgXZh0uRusqJCjAEShBKimWszKttWJk4Vx4a7G/Qdjl40Km/Pv580pUIpc4Ii4hmy8gID/jhH07lH2HDBX9/hlRiSJGFmZJbpVs66HDOavF7aRSEpmNF4BuWsy2V6bmZJSEBAAJIk0a1bN+bPn48syzx58gQrKysiIiJ05SIiIjAyMsLZ2RlZlvn1119T1bVypXZsna+vLydOnKB58+ZZfi8+/vhj3Nzc+PLLL3X7LCwsaN26NXPnztXtCwwMJCAgIMv1ZoeXl1eK1qWFCxfqBk17enri5+dHbGwsABqNhk2bNuHl5aUrHxISgiRJlC1btNYcFAmQIJQQkiQxtOZQAP6+/TfxqvhMziie3O3N+f3Duin2aWSYtvUGQRFizqDCJnlplxUdVrC/1356VuqZJ/Vev36dpk2bUqtWLWrXrs3AgQPx8vLCy8uLGjVqULNmTbp164anpyf9+/enRo0a1K9fHzc3t1R1qdVqateuTfv27Vm4cGGWu7/8/PxYsWIFZ86c0Q2ETu5O+/vvv3nw4AE1a9bE09OTnj17pnsL+61btyhTpoxue3OcUPPmzVPcBp9WN1qXLl04deoUarU6zfqvXbtGo0aNdO/P8+fPWbhwoe74vn376NGjBwpF0UopJFncEpFlkZGRWFtbExERUaT6OQUhmUqjovPWzgTGBPJVw6/oV7WfvkPSi9MPQxnwR+oumH9GNqJxhbRvExZyJz4+nsePH1OuXLkC6dYRsueTTz6hVatW2R5kDdoka9myZVSrVi0fIktfWp+p7PyeLlrpmiAIuWKoMGRQjUEArLq5CpWm6E1elhfKOZijSKOHpLSlccEHIwiFwKxZs0hISMj2eSEhIYwePbrAk5+8IBIgQShhelTsgY2xDQHRARzyO6TvcPTC2dqUOT09MXhrnMiPh8Q8QULJZG9vz4cffpjt8xwdHRkwYEA+RJT/CnUCdOHCBTp16oSNjQ3m5uY0atSIjRs3ZquOwMBAPv30U6pXr465uTmOjo40a9aMv/76K93+TkEozsyMzBhQVfuFteLGihL7C79ffTdOTmnNPyMbsWxgHYwMJHZfC+KPE4/0HZogCAWg0CZAR48epWnTppw8eZK+ffsyatQogoOD6devHwsWLMhSHY8ePaJWrVosWrQId3d3xo4dS8+ePXn48CGDBg3SDTYThJLm/arvY2poyu2w25wJOqPvcPTG2dqUxhXsaV/DmeldqgMwd+8dTj8I1XNkgiDkt0KZAKlUKkaOHIlCoeD48eMsW7aMBQsWcPXqVSpXrsy0adPw8/PLtJ758+cTGhrKTz/9xN69e/nhhx/4/fffuX37Nm5ubqxatSpL9QhCcWNjYkOPij0AWHmj5CyPkZEPG7nTq04ZNDKM/eeyWEVeEIq5QpkAHTlyhIcPHzJgwIAUq9xaW1szbdo0EhMTWb16dab1PHqkbcp+e4ZPGxsbmjVrBkBoqPhLTyiZBtUYhIFkwNmgs9x8cVPf4eidJEl836MmNVysCItJZPTa/4hPEt3kglBcFcoEyMfHB0C3ON2bOnToAMCxY8cyradmzZoA7NmzJ8X+8PBwTp06hZOTE9WrV89ltIJQNLlauNKxXEdAtAIlMzEyYMmHdbE1M+JaQATT/71RYsdICUJxVygToPv37wNQqVKlVMecnJywsLDQlcnIF198QeXKlfm///s/3n33XSZPnqy7Xc/AwIBt27ZlOHV3QkICkZGRKTZBKE6G1tBOjHjQ7yBPIp/oOZrCoaydGQvfr41Cgo0XA1h3XswQrW9JwcHEnD1HUnBwvl/L19eXJUuWZLl8eiu+A3h7exMVFQWAh4eHbqmNESNGpLkoaU74+Pik6CnJyK5duxg1alSaxx4/fkzdunXx9vamZs2a9OnTh5cvXwLaW90bNGiASlW8ps0olAlQ8jTk1tZpr1X09lTl6XF0dOTMmTN07NiRffv2MW/ePJYsWUJERASDBg2iVq1aGZ4/Z84crK2tdVtRm+ZbEDJTxa4KzVyboZE1rLq5St/hFBrNK5Xiiw5VAfhmx00u+b/Uc0TFiyzLaGJjs7SFrVvHgzZt8R8yhAdt2hK2bl2Wzstpy112E6CMXLlyBUtLy1T7ly9fTuvWrfPkGtkxdepUpk6dmuYxFxcXTp48yZUrV7hx4wYuLi588803gPZ3aZMmTVizZk0BRpv/ivViqA8ePKBr165YWFhw4sQJvL29CQ8PZ+3atXz11Vfs37+fEydOYGBgkOb5U6dOZcKECbrnkZGRIgkSip1hNYdx8ulJtj/Yzmjv0TiYOug7pEJhVMvyXAsIZ++NYEav/Y9d45pTSkyUmCfkuDju1qmbecG3aTSEzPqWkFnfZlq0yqX/kMzM0j0eFxfHkCFDuH79OkZGRjg6OnLgwAFGjRqFn58f3t7euLm5sWPHDiZOnMixY8dISkrCysqKP/74gypVqujqmj9/Prt37yYmJoYZM2bwwQcfANrWoZcvX6Zajb1Vq1Z89tlnvPfee9SrV0/XsvLs2TOqVKnC0aNHCQ4OZvz48fj6+hIXF0f37t357rvvsv+evZK8Kry7u3uax42NX3+21Wo1MTExWFhY6Pa9//77jB8/nmHDhuU4hsKmULYAJbf8pNfKkzzVdWaGDBmCn58fO3fupFmzZlhYWFCmTBmmTJnCuHHjOHPmDOvXr0/3fGNjY6ysrFJsglDc1HOsh5eDF4maRNbdXqfvcAoNSZL4X59aVChlTkhkAp+su0SSWpP5iUKRsG/fPsLDw7l16xZXr17V/S5YsmQJVapU4cqVK+zYsQOAyZMnc+HCBa5cucKYMWP49NNPU9QlSRKXL19m3759jBs3Dl9f3yzHcfHiRa5cucLevXsxNzfnq6++AmDw4MF88sknnD9/nsuXL3Px4kU2bdqU49fr4+NDw4YNMyyTmJiIt7c3Dg4O3L9/n5kzZ+qO1a1bl2vXrhWroSCFsgUoeezP/fv3qVs35V8JwcHBREdH06BBgwzriIqK4tSpU9SpUwcnJ6dUx1u3bs3PP//M5cuXddm6IJREkiQxrOYwPvP5jPV31zPcczjmRub6DqtQsDA2ZOnAery3+BTnH4cxZ88dpncVN07klmRqSpVL/2VaLikkhEedu4DmjcRToaD87l0YOTpmeo2M1KpVi9u3bzNmzBhatmyZ6m7hNx08eJBFixYRFRWFRqMhLCwsxfHkOeXKly9PixYtOH78eJYXRAXt76vOnTszffp02rZtS0xMDIcPHyYkJERXJjo6Os2FTLMqICCAihUrZlhGqVRy5coVEhMTGTduHEuXLmXSpEkAGBoaYmtrS2BgYLFpDCiULUAtW7YE4MCBA6mO7d+/P0WZ9CQmJgLp3+b+/PlzIGWznyCUVK3dWuNh5UFUYhSb723WdziFSsXSFizoqx0vuOLUY/698lTPERV9kiShMDPLdDMuVw7nWTMheZVxhQLnWTMxLlcu03MlKY3F3t5Qvnx5bt26RceOHTl16hQ1a9bUDfp9k7+/P2PHjmXt2rXcuHGD9evXEx8fn+nryyqVSkWfPn3o1asXAwcOBNCNXzp79ixXrlzhypUrPHjwQNc6lBNmZmYp4h4/frxuhfjr16+nKKtUKhk6dCh//fVXiv3x8fEZ3jhU1BTKBKht27aUL1+edevW6UbNg7ZLbPbs2SiVSgYNGqTbHxQUxJ07d1J0mdnb21OlShX8/f1Zvnx5ivrDw8OZP38+gF4GoglCYaOQFAypMQSANbfWkKRO0m9AhUyHGk6Mba3963nylmvcDio+3QCFnU3v3lQ8chi31aupeOQwNr1750m9AQEBSJJEt27dmD9/PrIs8+TJk1Q32URERGBkZISzszOyLPPrr7+mqmvlSu00Er6+vpw4cYLmzZtnOY6PP/4YNzc3vvzyS90+CwsLWrduzdy5c3X7AgMDCQgIyMlLBcDLyytFC9LChQt1yZWnpyd+fn7ExsYCoNFo2LRpE15eXrryISEhSJJUrMbBFsoEyNDQkOXLl6PRaGjRogUfffQRn3/+ObVq1eLevXvMnj07RfPi1KlTqVatGtu2bUtRz08//YShoSEjR46kXbt2fPHFF4wYMYLKlStz584devXqRbt27Qr41QlC4dS1QldKmZbiWewzdj/ere9wCp3/e6cyLSqXIj5Jw8d//Ud4bKK+QyoxjJycMG/YAKM0hjPk1PXr12natCm1atWidu3aDBw4EC8vL7y8vKhRowY1a9akW7dueHp60r9/f2rUqEH9+vVxc3NLVZdaraZ27dq0b9+ehQsXZrn7y8/PjxUrVnDmzBlda0xyd9rff//NgwcPqFmzJp6envTs2ZMXL16kWc+tW7coU6aMbuvTp0+qMl26dOHUqVPproF57do1GjVqpHsPnj9/zsKFC3XH9+3bR48ePVAoCmXakDNyIXbu3Dm5Y8eOspWVlWxqaio3aNBAXr9+fapygwcPlgF55cqVqY6dP39e7tOnj+zs7CwbGhrKFhYWcv369eVFixbJKpUqW/FERETIgBwREZHTlyQIhdqf1/+Ua66qKXfb1k1Wa9T6DqfQeRmTIDf74bDsPnmXPOjPc7JKrdF3SEVGXFycfOvWLTkuLk7foZRYY8aMkTdu3Jijc5s1aybfunUrjyPKnbQ+U9n5PS3JspjmNKuS7z6LiIgoNoPABOFNUYlRtN/cnuikaBa2XkhrN9FF/LabgRH0/O00CSoN49tUZEL7KpmfJBAfH8/jx48pV64cJiYm+g6nRHrx4gV79+7lww8/zNZ5ISEhHD58mAEDBuRTZDmT1mcqO7+ni1FbVtGVl7OcFsa6RExFh6XSkr5V+gKw4sYKPUdTONVwsWZuL08AFh55wMFbIZmcIQiFg729fbaTH9BOhFjYkp+8UChvgy9JwjdvJujr6SDLIElYtm+PqZcXvH0XQarnbx+WiLt6lci9+3R1Wb37Lqbe3q/P1T2mrFOSpDfq1z7GXrpE5M6durqsu3fHrH691ycnnyO9cb4k8WqHbn/MuXOEb9ykq8fm/f5YNG36xvkSkkLx6meF9jyF4tXPEpLiVTmFgqijPoQtX66ry2HMaCzbd0AyUIDCQFvWwEB7voEBKBRIyY+v9kkKBeH/7iDk22+1t9a+uqskpwMrwzdvJmj6jDypKyk4mERfP5Qe7nk61iG7Pqz2IX/d+osrz69wKeQSdRzr6C2WwqpH7TJcfRLBqtO+TNhwhX/HNqV8KYvMTxTE2mpCnsntZ0l0gWVDXneBJQUH86BN25RzXAh6IZmYIBkbIxkaptgwMkQyNEpzn6xSEXvmzFsVSdgMGICRgz2S0hjJxBiFiQmSsQkKE+PXj6+ul3ws6sB+QubMzZNECnKfTH1z+hu23N9CyzIt+bVt6rteBEhSa/jgj3Oc9w2jUmkLlnxYl5CoeMo5mONsXXxuFc4rarWa+/fvY2ZmRqlSpbJ1q7ggvE2WZZ4/f05sbCyVKlXSreiQnd/TIgHKhrxOgGLOnsN/yJBU+82bNsHQwSF1dvvm0zePyTKq0FBiz51LVZdZwwYY2NmlPD/53LcfXxVIevGC+EuXU9VlUqsWhjY2yMjac+RX5yZvyNqYX+1XR0SQcOdOqnqUFSqgsDDXltNoQJaRZU2K58gaZM2rejUaNHFxqEJSdzUorKyQFApkjQY0Gu2jWq17LMrJpbJiRQxtbVFYWWFgaYnCyhIDi1ePllavHi1RWFpikFzGwoKI7dtz3SrlG+FLt+3dkJHZ2m0rlWxTL0wswLOoeLosPMmzqATdPoUEc3p60q9+6ruFSrro6GgCAgJEK5CQJyRJokyZMimW7BAJUD4pkBYghYKKRw5n+6/2wlhXYYhJfpVAJSdHSYGBac4s6/7XGgxsbZGTVMiqJFCpkFWqV8/T3qd6EcrzBT+mTEYlCesePZAMDNAkxCPHJ+ge5fh4NAlpPMbFpawjr0kSdsOGYlyhIkZOjhg6OmLo6ISBRcazPU/wmcBBv4O8Z9WM94wb4Vy1Di7lPfMvziLqwM1gPvor5azGBpLEySmtRUtQGtRqNUlJYp4pIfeMjIxSreWZnd/TYgyQHhk5OeE8a2aqv9Zz0mVRGOsqDDFJknZcEAYGSKCbWfbteszq5mBhRsDQxibXrS2JQUE8bNsuVVLmPGcOCiND1JFRaKKjUEdGoY6KRBMVrX1M8TwK+dUkZqnIMmF/ph7QrLCwwNDRESPHV0mRkyNGjk4YOpbGyMmJYa69UP27j/77fFDIPryU4PbYHrT9ZHa2Xl9xZ2GS+mtULcv4hsaKBCgNBgYG6S5ALQgFSbQAZUN+3QafFBxMop8/Sne3XA9+LYx1iZgylxeDqeWkJOIfPcK3R8+UyZQkYdmxA5rIKFTPQkgKDkETFZW1Okk53l4jgdWfiyjTuK0Yw/FKUEQcTeceQfPGN6loARIE/RBdYPlEzAMk5Ke8SsqykkxpYmJICnmGKiSYpJAQVMEhJIUEowp5hio4mKRnz1Cns44egMLSEpMqVTCuVg2TqlUxqVYVZcWKKJTKHMddlG244M/Urdd1SdCIZuX4qotYNFUQCppIgPKJSICEoiIvkqmn9y4R3v0DFG+OtwftnXAqVeoTDA0xrlABk6pVMa5WFZOq1TCpWgUDG5vXMRWC2/zzS1BEHDN33mTfjRAqlrZg76fNMTIQU60JQkESCVA+EQmQUNIcXjwNx1+3YSCDWoKQsT1oM/IbEh4/Jv72bRJu3yH+jnbTvLGA5JsMXZwxsLIm4e5d7WDvPLjNv7CKiEui9XwfwmIS+bpLdYY3K6fvkAShRBEJUD4RCZBQEu0/v44V+74n2tGS7SNOYGRglKqMLMuogoK0ydDt2yTcuUP87TskZbB6tdOM6Vh17Zbp3WhFzfrz/kzZeh1LY0OOTGxFKUtjfYckCCWGSIDyiUiAhJJIrVHTbnM7QuNCWdRmEa3Ktsr6uZGRRGzbpp3kMQ2SUol5kyZYvvMOFm1aY2hrm0dR649GI/Peb6e4FhBB33plmNe7lr5DEoQSQ6wFJghCnjFQGNDRoyMAex7tyd65VlZYdugAire+aiQJI1dX5MREon18CPryS+43a47fkKGE/f03SWlMellUKBQSM7rWAGDjxQCuPAnXb0CCIKRJJECCIGSqc/nOABx9cpTYpHTmG0pH8hxOuiRIocD521lUOHSQ8jt34DB+HMbVqoFaTezZs4R8+x0PWrbicb9+vFi+nEQ/v7x+OfmurrstPeu4AjBjx000GtHQLgiFTa67wB4+fMjSpUs5ffo0z58/p3v37sybNw+Ac+fOcfXqVfr164e1tXWeBKxPogtMKKlkWabr9q74Rfoxp/kcupTvku06MrszLfHJE6IOHiLq4EHiLqdcisW4cmUs33kHy3faYVylCqqQkEJ/R9mzyHjaLDhGdIKK//X2ok+9svoOSRCKvQKbCXr16tWMGjWKhATtOjiSJBH6xtwhsbGxjB49GqVSyZA01rwSBKFokCSJTuU68fvV39nzaE+OEiAjJ6cMkxVl2bLYDxuK/bChJD17RvSRI0QdOEjM+fMk3LtHwr17hC5ejIGtLerw8EJ/R1lpKxPGt63I7D13+GHfXTrUdMLKJPUAckEQ9CPHXWBnz55lxIgRKJVK5s2bx7lz51ItcNeyZUusra3ZuXNnrgMVBEG/3i33LgCnA08TFh+Wr9cyKl0a2/79cVvxJ5VPnsB57hws2rYFpRL1y5ev107TaAiaPoOk4OB8jSenhjQpR3kHc0KjE1h0+L6+wxEE4Q05ToDmzZuHLMvs3r2biRMnUr9+/dSVKxR4e3tz69atXAUpCIL+lbMuR3X76qhlNQd8DxTYdQ1sbLB57z3KLv6VsosWpi6g0RAyZy6qly8LLKasUhoqmN5VOyP0ylO+PHiWtSVIBEHIfzlOgE6dOkWDBg1o1qxZhuWcnJwICgrK6WUEQShEOpXrBMCex9m7GyyvGFepkvqOMiBq/34etnuH578uRh0do4fI0teqSmnaVXNEpZGZufNWqpZyQRD0I8cJUHh4OG5ubpmWi4uLIzExMaeXEQShEOno0REJicvPLvM0+mmBXz+tO8psBw3CuHo1NDExhP76Kw/feYcXq1aheTU2sTD4uks1lAYKTtwP5eCtonuLvyAUJzlOgOzt7fHLwu2pDx48wKmQ3qUhCEL2OJo7Ut9J29299/FevcRg07s3FY8cxm31aioeOYzTtKmU27wZ159/QunhgfrlS57N/YGHHd8lfPNm5LTWLStg7vbmjGyhXRbj2923iE9S6zkiQRBynAA1atSIixcvcvPmzXTLnDp1ips3b2baTSYIQtGRPCeQvrrBQNsSZN6wge6uMkmhwKpjR8rv2onzd99i6OSEKiiIoK++5lGXrkTu3Yus0egtXoAxrSriZGXCk7A4/jj+SK+xCIKQiwTok08+Qa1W06tXL65cuZLq+O3btxk2bBiSJDFmzJjcxCgIQiHS1q0tRgoj7r+8z72X9/QdTgqSoSE2vXtTYf8+Sk+ZjIGtLYm+vjz9vwk87t2b6BMn9DYGx9zYkGmdqwGw2OcBgeFxeolDEAStHCdAbdu2ZcKECdy7d4+6detSuXJlJEli//79eHl54enpyf379/niiy9o1KhRXsYsCIIeWRtb09y1OZD9pTEKisLYGPshQ6hw8AAOY8eiMDcn4dZtnoz8CP+Bg4i9pJ1oMSk4mJiz5wrsNvquXs408LAjPknD93tuF8g1BUFIW65ngv7jjz/45ptvUt3p5eDgwIwZM/jkk09yFWBhImaCFgSt/b77mXhsIi7mLuzttReFVLhX1VG9fMmLpct4uW4d8qubMoyrVCHh3r0Cn1DxZmAEXRedRCPDPyMb0biCfb5fUxBKigJfDV6WZS5fvsyjR4/QaDSULVuW+vXrY2iYq4mmCx2RAAmCVrwqnpYbWhKrimXNu2uoXbq2vkPKkqSgIEJ/+43wLVvh7TFBCgUVjxwukKU1vtp+nbVn/anqZMmucc0wNCjcCaQgFBUFvhq8JEnUqVOH3r1707dvXxo3blzskh9BEF4zMTShnXs7AHY/2q3naLLOyNkZ52+/xXn296kPajQk+vkXSByfv1MFGzMj7gRH8fe5grmmIAgpiT87BEHIkc7ltHeDHfA9QJImSc/RZI95o0ZpTqiYFFwwk7bamiv5vH0VABYcuEtYjJgrTRAKWpabaY4fP56rC7Vo0SJX5wuCULg0cG6AnYkdYfFhnA08S/MyzfUdUpYlT6gYNH1Giq6woClTUYe+wG7YUCRJytcYBjRwY905f24HRTL/wF1m9/DM1+sJgpBSlscAKRSKHH8hSJKEqhBMRpZbYgyQIKQ059wc1t1ZR5fyXZjTfI6+w8m2pOBgEv38MXJ25sUfywjftBkA6+7dcJo1C4Wxcb5e//zjMPouPYMkwc6xzajpap2v1xOE4i47v6ez3ALUokWLfP+LSBCEoqVT+U6su7OOw/6HiU2KxczITN8hZYuRk5Nu0LPTrFkYV6lKyJw5RPy7g4THvpRZtAgjx9L5dv0G5ezoVsuFHVcD+WbHTTaNaiy+ZwWhgOTJXWAlhWgBEoSUZFnm3a3v8jT6KfNazOPdcu/qO6RcizlzhoDP/g9NRASGpUpRZvGvmHp55dv1giLiaDP/GHFJan7u5817tV3z7VqCUNwV+F1ggiCUTJIkvV4hvpBOiphd5o0bU27TRpQVK6B6/hy/DwcSsWNHvl3P2dqUsW0qAjB7z22iE4r+cAFBKApEAiQIQq4krw128ulJwuPD9RtMHlG6ueGxfj0WrVsjJyYSOGkyz+bPR1bnzyKmw5uVw93ejGdRCfx65EG+XEMQhJRynQCFhoYyd+5cOnbsSM2aNalRowYdOnRg7ty5PHv2LC9iFAShEKtgU4GqdlVRySoO+h/Udzh5xsDCgjKLf8X+448BeLH8T56MGYM6KirPr2ViZMDXnasDsPzEQ7ZeCiAoQqwVJgj5KVdjgP7991+GDh1KREREqgUGJUnC0tKSlStX0qNHj1wHWhiIMUCCkLaVN1by438/Us+xHis7rtR3OHkuYtdugr78EjkhAWX58pT9bTFKD488vYYsy3T4+Tj3QqIBUEgwp6cn/eq75el1BKE4K5AxQOfOnaNPnz6Eh4dTt25dFi5cyM6dO9m5cyeLFi2iXr16REZG0q9fP86dO5fTy5QMEU/h8XHtY2GqSxCyKHnw838h/xEcUzALixYk6y6dcV+7FkNHRxIfPeJx335EnzqVp9cIjoznwbNo3XONDNO23hAtQYKQT3KcAM2aNQu1Ws3//vc/zp8/z9ixY+ncuTOdO3fmk08+4dy5cyxYsACVSsW3336blzEXL5fWwM81YXVX7eOlNYWjLsi7ZEokZcWek7kTdR3rIiOz9/FefYeTL0w9a+KxaSOmtWqhiYzkyciPCFu9OlXrd049Do1B81ZValnGNzQ2T+oXBCGlHHeB2dra4ubmxtWrVzMsV6tWLfz9/Xn58mW2r3HhwgVmzJjB6dOnSUpKwtPTkwkTJtC3b99s1fPs2TPmzJnDrl27ePLkCebm5lSuXJlBgwYxevToLNeT511gEU+1iYr81qKMjjXA0BQkBSgMtI/Jm+65wRvPJVAlwv39b11Aglr9wdRWW05hBApDMDDK/LnfSbi4CpC19TedADXeA0MTMDRO/Zje3CWX1sDOT7WvUVJA11+gzqDcvWdhD8GuAliL24ULk413N/Lt2W+paleVTV036TucfKNJTCR4xjdEbNsGgHXPnjh9MwOFUpmreoMi4mg690iKJEghwakpbXC2Ns1V3YJQUhTIavAWFha89957rF27NsNyH3zwAf/++y/R0dEZlnvb0aNH6dChAyYmJvTv3x9LS0u2bNmCn58f8+fP5/PPP89SPVeuXKF9+/a8fPmSzp07U61aNaKjo7l9+zZKpZI9e7J+626eJ0CPj2tba4oDg7SSIgMIvfNWQQnqDgErFzC2fGOzerW9sU9pnjKxyutkSshT4fHhtN7YGpWsYnv37VSwqaDvkPKNLMuErV7Ns3n/A40GU29vHL/+Ck1UNEoP9xyvKL/hgj/Ttt5A/epruYytKScmtRaTIwpCFhVIAlSvXj3Mzc05duxYhuVatWpFVFQU//33X5brVqlUVK1alYCAAM6ePYu3tzcAERERNGjQAF9fX+7du4e7u3uG9URGRuLp6UlcXByHDh3C663JzFQqVbZWrS+QFiBJAd0Xg4mNdr+s1j5qXj2m+PmNY7EvwWc28OY/pwQNR4GRCWhUoFZpHzVJaTxXg/rV/pjnEHwtdbymdtr6VQmQFPfWtfKBpHidHBmawIv7bxeAzgu0LWYWjmDpBEZZ/EtZtCTli3GHx+ET4MNHXh8xrvY4fYeT76JPnOTphAlo3rwzTKHAedZMbHr3zlGdQRFxXHsSzqfrrxCv0rB8UD3aVXfMo4gFoXjLl6Uw3vbxxx8zatQojh07RsuWLdMsc+zYMU6cOMHvv/+erbqPHDnCw4cPGTp0qC75AbC2tmbatGkMGTKE1atXM3369Azr+e233/D39+fPP/9MlfwA2Up+8oW1q7YVY+dn2mRGMoCuP4P3gJzVZ+WUuq6ctJCkmZgZwKiTr5MFWdYmTKp4bUKU1mNEAGz7mFRJWd0h2n0JURAfqX3UbZHaLTnZi4/QbmmSYfeElLtMrMHCCSwdXz2+2iwcwdJZ+/PDI7B3kmhJygedynfCJ8CHPY/2MNZ7bLFvubBo3owyixfjP+iNz49GQ9D0GZg3a5ajliBna1OcrU0Z3DScpcce8fPhe7StVrrYv5eCUNBynAGMHDmSO3fu0LlzZ0aNGsWgQYMoV64cAI8fP+avv/7i999/57PPPuOjjz7KVt0+Pj4AtG/fPtWxDh06AGTa8gSwYcMGJEmiV69e3L17lwMHDhAXF0fVqlXp2LEjylz22eeJOoOgQlsIewR25XPXGpFXdaWXmL1ZnySBoVK7ZUSdkP2kTJYhKTZlUvTiEWwdSapkytkb4l9CVLA26UpOmELvZu21yhrYMV776NEcbD2046GEHGlZpiWmhqYERAdwPfQ6XqXybwmJQuON1eTf3Jfo55/jrjCAj1tU4K8zftx4Gsmh2894R7QCCUKeynIXmIFBzn8pZHc1+D59+rB582YuXrxI3bp1Ux23tLTE1tYWf3//dOtITEzEzMwMe3t7xo0bx4wZM9C88UVVvnx5tm/fjqenZ7p1JCQkkJCQoHseGRlJ2bJlS848QBFP8yYxy6t6Lq1JP5mSZW3iEx0CUUEQFQLRwdrHqKBX+4O1sajj07+GoSmUrgqla0DpauBYXfuzRen0B3qL7rQUppyYwu5Hu/mg2gdMaTBF3+Hku6TgYB60aZsqESp/4ADGbmVzVfcP++7wu89DarhYsWtcM9EKJAiZyJcxQApF7iaN1qT1V1I62rdvz8GDB7l//z4VK1ZMddzV1ZXo6GgiItLrGoHg4GCcnZ0xMDBAoVAwZ84cBg4cSFJSEkuXLuW7777Dzc2NO3fuYGJikmYd33zzDTNnzky1v8QkQIVRbpOpNO+8k7RJT9hjbStSWszsoXR17ZacFJWuCje3iYHZbzkecJxPDn+CnYkdh/scxlCh567mAhC+eTNB02ekSIKsunbFZd4PuUpawmISaf7DEWIS1SwbWJf2NXLeoiQIJUGBDILOT3mRAAUGBuLqqv0F+emnn/Lzzz+nON6vXz82btzIX3/9xYcffphmHSW+Bai4Sq8lSaPWJlfPbkHILXh2U/sY9ogsD/iWDOCz6yW6JShJk0SbjW0ITwhnabulNHFtou+QCkRScDCJfv4kPQshaMpUUKtx+OQTSo0bm6t65+27w28+D6nubMXu8aIVSBAyUiCDoPOTtbU1QLoJTmRkJLa2tlmqA6Bbt26pjnfr1o2NGzdy8eLFdBMgY2NjjI2Nsxq2UFSkN1ZKYQAOlbRb9e6vyyfGascUhdzSJkfJCVJ0GjMey2o48CU0+AjKNiyR44mMFEZ08OjAhrsb2P14d4lJgIycnHRjfuS4OIKnzyB08WKU7m5Yp/EdlFUjm5dn9WlfbgVFcuBWCB1EK5Ag5IlCuRp8pUqVALh//+3bnrVdW9HR0boy6TE3N9e1ANnY2KQ6nrwvLk5MM18iWbtCueZZa6lRmoFLbaj9AXT4HgZug4l3YfTptMcF3dwGK9+F/1WEbaPh9k5IyN48WEVd8grxh/0PE59et2IxZtu3L/YjhgMQ+OVXxF64kPO6zJUMaeoBwM+H7qN5e7poQRBypFAmQMm31R84cCDVsf3796cok5E2bdoAcOvWrVTHkvd55PGChkIJ4lgDui7UdnuB9rHeUPDqp53HKS4Mrq6DDR/CvPLwd1+4uFI7GLuYq1WqFi7mLsQkxXA84Li+w9GLUhMmYNm+PSQl8WTsOBIeP85xXSObl8fC2JDbQZEcuFX8Pz+CUBByNQYoKSmJhQsXsmnTJu7evUtkZGTaF8nmXWAqlYoqVarw9OnTdCdCvHv3ri55CQoKIiIiAmdn5xRdX6dPn6Zp06bUqFGDkydP6lp9goODqVevHkFBQdy+fZvKlStnKS6xGryQprQGZquTwP8s3N0Dd3ZDuF/Kc1zrQpV3oUpn7d1mklTs7ib7+b+f+fPGn7R1a8vPrX/Wdzh6oYmPx2/wYOKvXsPI3Q2P9esxzKT7Pj0LDtxl0ZEHVHWyZM/45igUYiyQILytQAZBJyQk0LZtW86cOZOlxQCzcxcYZG8pjOSJEVeuXMmQIUNS1PP555/z448/UrZsWbp27UpSUhL//vsvz549Y/bs2UydOjXLMYkESMgRWYZnt7XJ0N098PStWdFt3MGuHDw+pi1bTO4mu/fyHr129MJIYYRPPx+slCXz/4wqNBTffv1JevoU0zp1cFu5AkUOxhaGxybS/IejRCWo+P2DOrzr6ZwP0QpC0Zad39M57gL75ZdfOH36NO3bt+fu3bsMGjQISZJISEjgxo0bTJ48GWNjY77++utsJz8ArVu35uTJkzRt2pQNGzbw+++/4+joyPr167O8DhjAggULWLlyJY6OjqxatYp169ZRuXJltm7dmq3kRxByTJK0t863mAgjj8Dnd6HLz1Cpg3YNtXA/eOSjTX5Ae0v9zs+0LUJFWGXbylS0qUiSJolDfof0HY7eGDo4UHbpEhSWlsRdukTQl1/laAV5GzMlQ8VYIEHIMzluAapfvz737t3D398fa2trhg4dypo1a1Cr1boyO3fu5L333mPDhg30zuG6OIWJaAES8lxCNJxZ/Godt7fUH6kddG1YdO9EXH59Ob9c+oWGTg1Z3mG5vsPRq5jTp/H/6GNQqXAYM4ZS47O/VlpEbBLN5h0hKl7Fbx/UoZNoBRKEFAqkBejevXs0bNhQN+YmeW6KNxOgrl27Urt2bRYtWpTTywhC8WZsAbU/1HZ7ve3CH7CoLlz6S7twbRH0brl3ATgffJ5nsc/0HI1+mTdpgvM3MwAI/e03wrdvz3Yd1mZGDGuqXXLoF9EKJAi5kuMEKCkpiVKlSumem5pqV+F+eyB0lSpVuH79ek4vIwjFX/Laa2/eTebVX7t4a8QT2DEWfm8MN7enve5UIeZq4Urt0rWRkdn3eJ++w9E7m969sR85EoCgr6cTc+58tusY1qwcliaG3A2JYs+NoLwOURBKjBwnQE5OTgQFvf7P5+ysbYq9fft2inKBgYEpWoUEQUhDnUHaGaQH79I+9lwK4y9D++/A1A5C78GmwfBHK3hw6PV4oSKgU7lOAOx5vEfPkRQOpf7vMyw7doSkJALGjyfhUfZuj7c2NWJ4M9EKJAi5leMEqFq1ajx48ED3vEmTJsiyzLx583SDno8dO8aJEyeoUqVK7iMVhOLu7ckZjUyhyTj49Cq0nAxKCwi6Cmt7warO4H9Ov/FmUXuP9hhIBtx8cZMdD3YQHFOy57GRFApc5s7BtFYtNBERPPn4Y1RhYdmqY2jTcliZGHL/WTS7r4tWIEHIiRwnQB06dCAgIIDz57VNuK1ataJ69ers3LkTV1dX6tatyzvvvIMsy4wZMybPAhaEEsfEClpP0yZCjT7R3jnmdwpWtNdOrhhcuLuY7UzsKGetbbH48tSXdNjSga33t+o5Kv1SmJhQ5rfFGJUpQ9KTJwR8MhbNG+sOZkbbClQegF8O30ctWoEEIdtynAANGDCAv/76SzcIWqFQsH37dmrWrElISAiXL19GlmXGjx+fam4eQRBywNwBOs6G8Ze0XWaSAdzfD0uawebh8OKhviNMU3BMMA/DX8emkTXMPDOzxLcEGdrbv749/vJlgqZOQ87GGK+hzTywMjHkgWgFEoQcyZfV4O/evUtYWBiVK1fG3t4+r6vXG3EbvFCohD6Ao9/DzVetKZIB1BkILSZpnxeSWaXPB51n+IHhqfav6LCC+k719RBR4RJz9iz+I0aCSoX96FGU/vTTLJ+76PB9Fhy8R4VS5hz4v5YYiNmhhRKuQGaCLolEAiQUSkFX4ch3cP/V2nmSoXZVegrHrNLBMcF02NIBjfy6dUMhKdjfaz9O5mJlc4DwLVsJ+vJLAEpPnoxJtWooPdx1q8unJyo+iWY/HCUiLolf+nvT3bvoL6EiCLlRIPMAff7552kuMioIQgFzrgUfbIKhe8GlDsgqoPDMKu1k7sSMxjNQvPF1M6XBFJH8vMGmV0/sP/4YgGc//ID/kCE8aNOW8M2bMzzP0sSIkc1f3REmxgIJQrbkOAH66aef8PT0pGHDhixZsoSIiIi8jEsQhOxybwLtvkm9X1ZrF2vVo56VerKv1z4czRwBMDYourNb5xebfn1T7tBoCJo+g6TgjMdKDW7igY2ZEY+ex7DzamA+RigIxUuOE6AJEyZQunRpLly4wCeffIKzszMffPABhw6V3DV/BEHv7CumPav03X16nzvI2cKZD6p9AMCW+1v0GkthlOT/JPVOjYZEP/8Mz9O2AmnvCFt4+D4qddGaLFMQ9CXHCdD8+fMJCAhg+/btdOvWDbVazT///EOHDh1wd3dnxowZPHqk3786BaHEeXtWaV4Nij37K2wdCUnxegsNoGuFrhhKhlx7fo37L+/rNZbCRunhDoq3vpIlCaW7W6bnDm7iga2ZEY9CY9h5TbQCCUJW5DgBAjAwMKBbt25s27aNp0+fsmDBAmrWrMmTJ0/49ttvqVSpEq1bt+avv/7Kq3gFQcjMm7NK/99NbUKkMITrm2BNN4h+rrfQHEwdaFm2JUCJnwvobUZOTjjPmpkyCVIoUIeHZ3quhbEhI1sktwI9EK1AgpAF+XIX2KVLl1i5ciX//PMPYWFhKBQKVKqiuZjjm8RdYEKR9cgHNgyChAiwcYMBm6B0Vb2EcjzgOJ8c/gRrY2uO9DmC0kCplzgKq6TgYBL8/Hjxxx/EnjyFceXKeGzehEKZ8fsUnaCi+Q9HeBmbxII+tehVt0wBRSwIhUeB3AWWkTp16jBgwAA6ddKuASTutBcEPSvfCkYcAlsPCPeHP9+BB4f1EkpTl6aUNitNREIER/yP6CWGwszIyQmLhg1x/eEHDOzsSLh3j9BFv2Z6noWxIR+1qADAoiNiLJAgZCZPE6DAwEDmzp1L1apVadasGX///TcGBga6REgQBD0qVRlGHAG3xpAQCX/3gQvLCzwMA4UBPSr2AMRg6IwY2tvjNPMbAF78+Sexly5nes6gxu7YmSvxfRHL9itiLJAgZCTXCVBiYiIbNmzg3Xffxd3dnS+//JJ79+5RqVIlZs+ejb+/Pzt37syLWAVByC1zexj0L9R6X3t7/O7PYe8U0KgLNIwelXogIXE26CwBUQEFeu2ixOqdd7Du3h00GgKnTEETG5theXNjQz56NRZItAIJQsZynACdP3+eMWPG4OzszIABA9i/fz+mpqYMHTqUEydOcOfOHSZPnoyzs3NexisIQm4ZGsN7v0Obr7XPz/0O/7wPCVEFFoKrhSuNnBsBsP3B9gK7blHk+OU0DJ2dSfL359n8+ZmWH9TYHXtzJX4vYtl2WX8TYApCYZfjBKhRo0YsXbqUly9f0qxZM1asWEFwcDDLly+nadOmeRmjIAh5TZKgxUToswoMTbSLqv7ZAcLTmIsmn/Ss3BOAbQ+2oS7gFqiixMDKCpfZ3wPwct0/RJ88lWF5M6UhH7fUtgL9dOgeJ+4/JygiLt/jFISiJscJkIuLC1OmTOHevXscO3aMIUOGYGZmlpexCYKQ32r0gCF7wLw0PLsJf7SBgP8K5NJtyrbBxtiGZ7HPOBWY8S/1ks68cWNsP9BOIhn05ZeoM5l5/8NG7lgYGxAYHs/AP8/TdO4RNlzIeEJFQShpcpwAHT9+nFGjRlGxYsW8jEcQhIJWpi6MPAKla0DMM1jVCW5uy/fLKg2UdCnfBRBzAmVF6Ymfo/TwQBUSQvD332dYNiIuiZiE161qGhmmbb0hWoIE4Q05ToAqVapE//798zIWQRD0xaYsDN8PlTqAKh42DYHj/8v35TN6VtJ2gx17cozQuNB8vVZRpzA1xWXuHFAoiNyxk8j9B9It+zg0hrf/5dSyjG9oxoOoBaEkyXECZGlpSbly5fIyFkEQ9MnYEt7/BxqO1j4/8h1sHw1hj+Hx8XxZUb6SbSW8SnmhklXseLgjz+svbky9vbEfORKA4BkzUIWmnTSWczBHIaXcp5DAw0EMUxCEZDlOgKpXr86TJwU3YFIQhAKgMIB350LnBdr1xK7+Awu9YXVX+LkmXFqT55fsVakXANvubxOTpmZBqU/GYFy1KurwcIK+np7me+Zsbcqcnp4pkqD3vF1xtjYtwEgFoXDLcQI0cuRITp06xYULF/IyHkEQCoP6I7S3yr9J1sDOz/K8JaijR0fMDM3wjfTl0rNLeVp3cSQplbj88AOSkRHRR48SsTXt8Vr96rtxakobBjVyB+Dc4zASVWJeIEFIluMEaOjQoYwZM4b27dsze/Zs7t69S0JCQl7GJgiCPlmlMYeXrIawR3l6GTMjM94t9y4gBkNnlUmVyjiMHwdAyOzZJD1NOyl1tjZlWudqlLY05ml4HBsvilZ7QUiW4wTIwMCAxYsXExkZyddff0316tUxMzPDwMAg1WZoaJiXMQuCUBDsKoD09leEBHbl8/xSyYOhD/geIDIxMs/rL47shw3DtHZtNDExBE6dhqxJu3XHxMiAT1pr79ZdfPQBCSox55IgQC4SIFmWs7xp0vmPKQhCIWbtCl1/0Y4FelPIzTy/lKeDJxVtKhKvjmfvo715Xn9xJBkY4DJ3DpKpKbHnz/Ny7dp0y/arXxYnKxOCIuLZcEG0AgkC5CIB0mg02doEQSiC6gyCz67D4J3g2QeQYfMweHY7Ty8jSZKuFUgskJp1Snd3HCd9AcCzBT+S8Cjt7kltK5B2pfjFRx8QnyRagQQhT1eDFwShGLJ2hXItoPtv4N4MEqPgn/4Q8yJPL9OlfBeMFEbcDrvNrRe38rTu4symf3/MmzVDTkggcPIUZJUqzXJ965fFxdqEkMgE1p8Xs0ILgkiABEHIGkMl9PsLbD3gpS9sHASqxDyr3tbElrZubQExGDo7JEnC+fvvUFhZEX/9OqHLlqVZztjQgDGvxgL95vNQtAIJJV6uE6CHDx8yadIkmjVrRpUqVZg0aZLu2Llz51i2bBkRmaxbIwhCEWFmB++vB6Ul+J2EPRPzdLbo5G6wPY/2EKcSyzZklZGjI05ffwVA6G+/E3cz7XFafeuVxdXGlGdRCaw7J1qBhJItVwnQ6tWrqVmzJvPnz+f06dM8ePCA0DdmJo2NjWX06NFs25b/6woJglBASleD3n8CElxaDeeW5lnVDZ0b4mrhSlRSFIf8DuVZvSWBVZcuWHboACoVgZMno0ljWhKloYKxbV63AsUlilYgoeTKcQJ09uxZRowYgVKpZN68eZw7dy7VjKQtW7bE2tqanTt35jpQQRAKkcodoP232p/3T4UHh/OkWoWkoEfFHoDoBssuSZJw+mYGBg4OJD54yPNfFqZZrnfdMpSxNSU0OoG/z/kVcJSCUHjkOAGaN28esiyze/duJk6cSP369VNXrlDg7e3NrVtiQKMgFDuNx4L3h9oZojcNhef38qTa7hW7o5AUXAy5iG+Eb57UWVIY2triPGsWAGErVxK5/wAxZ8+RFBysK2NkoGDcq1agJcceEpuY9qBpQSjucpwAnTp1igYNGtCsWbMMyzk5OREUFJTTywiCUFhJEnT5Eco2goQI+KcfxIbluloncyeauWq/V7Y9EN3n2WXZpjXWvXqCLPP000/xHzKEB23aEr55s65MzzplcLMzIzQ6kbVnRSuQUDLlOAEKDw/Hzc0t03JxcXEkJubdnSKCIBQihsbQby1Yu2mXyNg0BNRJua62Z0XtYOh/H/xLkib39ZU0dsOGpdyh0RA0fYauJcjI4PVYoKXHHolWIKFEynECZG9vj59f5n85PHjwACcnp5xeRhCEws6iFLz/DxiZw+NjsG9qrqtsUbYFdiZ2vIh/wfGA43kQZMmifh6aeqdGQ6Lf6zu/etZ2xd3ejBcxiaw5I1qBhJInxwlQo0aNuHjxIjfTud0StN1kN2/ezLSbLD0XLlygU6dO2NjYYG5uTqNGjdi4cWNOQ+bly5e4uroiSRIdO3bMcT2CILzFqSb0+gOQ4MIfcGF5rqozUhjRvWJ3QAyGzgmlhzso3vp6VyhQur9utTc0UDC+TSUAlh57SHSCaAUSSpYcJ0CffPIJarWaXr16ceXKlVTHb9++zbBhw5AkiTFjxmS7/qNHj9K0aVNOnjxJ3759GTVqFMHBwfTr148FCxbkKOaxY8eKOYkEIb9U7Qxtp2t/3jMJHh3LVXXJ3WAnn54kOCY4k9LCm4ycnHCeNTNFEmTerBlGb7XGd/d2oZyDOS9jk1h92reAoxQE/cpxAtS2bVsmTJjAvXv3qFu3LpUrV0aSJPbv34+Xlxeenp7cv3+fL774gkaNGmWrbpVKxciRI1EoFBw/fpxly5axYMECrl69SuXKlZk2bVqWut/etGXLFtatW8cPP/yQrfMEQciGZv8Hnn1BVmtnin7xMMdVeVh7UNexLhpZw78P/s3DIEsGm969qXjkMA6fjgcg5tQp4u/cSVHG0EDB+LbasUB/nHhEVLwYbyWUHLmaCHH+/PksXboUJycnHjx4gCzLBAUFcePGDezs7Fi0aBFz587Ndr1Hjhzh4cOHDBgwAG9vb91+a2trpk2bRmJiIqtXr85yfc+fP2f06NEMHDiQzp07ZzseQRCySJKg2yJwrQfx4bCuH8SF57i6XpV6Adq7wTSyWFQ5u4ycnCg1ejSW7duDWk3Q19OR1SknP+xWy5XypcwJF61AQgmT66UwRo4cSUBAABcvXmTjxo2sX7+eU6dOERgYyCeffJKjOn18fABo3759qmMdOnQA4NixrDevjxo1CgMDA3755ZccxSMIQjYYmUD/dWDlCi/ua1ePV+dsfEk793ZYGlnyNPop54PP53GgJYfjV1+isLQk/vp1Xq5dm+KYgULi07basUB/nHhMpGgFEkqIPFkMVZIk6tSpQ+/evenbty+NGzfG0NAwx/Xdv38fgEqVKqU65uTkhIWFha5MZtauXcvWrVtZsmQJtra2OY5JEIRssHTUJkGGpvDwMBz8OkfVmBqa0ql8JwC23hODoXPKqHRpSk+cCMCzn38hMeBpiuNdvFyoWNqCiLgkVp3y1UOEglDw8mU1+Li4OK5evcqLFy9ydH7yQGVra+s0j1tZWWVpMHNgYCDjx4/n/fffp3v37tmOIyEhgcjIyBSbIAhZ5OINPZZofz77G5z8GR4fh4inGZ2VSvICqYf8DxEeH56nIZYkNn16Y1avHnJcHMEzZ6ZYuihlK9AjIuJEK5BQ/OU4ATpx4gQTJkzg6tWrKfb//ffflC5dmjp16uDs7MysV9Oy68OIESMwMjJi4cK018TJzJw5c7C2ttZtZcuWzeMIBaGYq/EetJqm/fnQDFjdFX6uCZfWZLmK6vbVqWZXjSRNErse7cqfOEsASaHAadYsJKWSmBMniNy1O8Xxzp7OVHa0ICpexYqTj/UUpSAUnBwnQMuWLePXX3/F1dVVt+/JkycMHz6cmJgYrK2tUalUzJw5M1vjdeB1y096rTyRkZHptg4lW716NXv37mXx4sU4ODhk6/rJpk6dSkREhG578uRJjuoRhBKt9gcpn8sa2PlZtlqCkluBttzfkmrRZSHrjMuXw2H0KABCZs9G9fKl7phCIfFp28oArDj5mIhY0QokFG85ToDOnTtHrVq1UiQXf/31F4mJiXzzzTeEhYXpEp/ffvstW3Unj/1Ja5xPcHAw0dHRaY4PetPly5cB6NOnD5Ik6bZy5coBsH//fiRJSnGX2duMjY2xsrJKsQmCkE1hj1Lvk9Vp709Hp/KdMDYw5kH4A66HXs/D4Eoe++HDMa5UCfXLlzybm3JakHdrOlHVyZKoBBV/nsz6v48gFEU5ToBCQ0MpU6ZMin1HjhxBqVQyYcIEAJo3b06jRo10yUhWtWzZEoADBw6kOrZ///4UZdLTuHFjhg8fnmrr168fAGXKlGH48OH07NkzW7EJgpBNdhVAevurRgK78lmuwkppRXt37V2hYmbo3JGUSpy/+xYkiYh//yX61CndMcUbY4FWnPIlPFas4ygUX5Kcw/ZkpVJJ7969WbduHQCyLGNjY4OnpycnT57Ulfvggw/4999/iY6OznLdKpWKKlWq8PTpU86ePatrpYmIiKBBgwb4+vpy9+5dPDw8AAgKCiIiIgJnZ+dMu8Z8fX0pV64cHTp0YN++fdl6zcldbxEREaI1SBCy49IabbeX/GoOGskAPj4GTp5ZruJi8EWG7h+KmaEZR/sexczILH9iLSGCv/uel2vXYlSmDOV3/IvCTPt+ajQynRae4E5wFGNbV2Rihyp6jlQQsi47v6dz3AJkZ2eHr6+v7vnly5eJioqiSZMmKcolJSWhVCqzVbehoSHLly9Ho9HQokULPvroIz7//HNq1arFvXv3mD17ti75Ae1YnWrVqrFt27acvhxBEPJTnUHw2XUYvBPKt9YmQltGQFJclquo61gXdyt3YlWx7Pfdn4/BlgylPvsMQ2dnkgICeP7rYt1+hULi/97RjgVaeeoxYTGiFUgonnKcANWvX5/z589z5swZAH755RckSaJNmzYpyt2/fx9nZ+ds19+6dWtOnjxJ06ZN2bBhA7///juOjo6sX7+ezz//PKdhC4KgL9auUK4F9FoOFo7w/A4cnJ7l0yVJokfFHgD8c+cfzgedF2uE5YKBhTlOM7Tvf9iqVcS9sbB1++qO1HCxIiZRzR8nxFggoXjKcRfYoUOHdLMyJzc3lS9fnrt376J4tQBfaGgoTk5O9O/fn7VvzT5aFIkuMEHII/cPwd/aZS4YsAkqp571PS2hcaG02dgGGe3XlkJSMKPxDN1dYkL2PZ0wgcg9ezGuXo1yGzcivZrE9uCtEEauuYiZ0oATk1pjb2Gs50gFIXMF0gXWrl07VqxYgbu7O4mJibRs2ZKdO3fqkh/Q3hWm0WgyHbAsCEIJU6kdNNTejs2/YyD6eZZOU2lUuuQHQCNrmHlmpmgJygXHadNQWFuTcOs2YW+ssdiuWmk8Xa2JTVSzTLQCCcVQrmaCHjx4MI8ePSI6OpojR45QtWrVFMdHjRrFy5cvGTZsWK6CFAShGGr3DZSqBjHPYcdYyEJjtH+kf6p9GlnDkygxR1dOGTo44DhpEgDPF/1K4qv5ziRJ4rN22jvC1pz2IzQ6QW8xCkJ+yJelMJKZmppibW2NgYFBfl5GEISiyMhUOx7IQAn39sHFFZme4mblhuKtW+oVkoKylmKW9tyw7tkDs0aNkOPjCZ4xQzfZZJuqpalVxpq4JDXf7LhJUETWB60LQmGXJwlQYGAg69evZ/78+fzvf//jn3/+4enT7K33IwhCCeRUU9sSBLD/S3h+L+Pi5k7MaDwDxRtfXZ/W/hQnc6d8DLL4kyQJ55nfIBkbE3P6DBHb/9Xtr+OuXUR617Ugms49woYLqVvhBKEoylUC9OLFCz744APc3d354IMPmDx5MlOmTOHDDz/Ew8OD999/n9DQ0LyKVRCE4qjhaO2t8ao42DIcVBnfdt2zUk/2995PVVttl7t/lPiFnBeU7u44jP0EgGdz56J68YKgiDhWn/bVldHIMG3rDdESJBQLOU6AwsPDad68OevXr0ej0VC7dm169uxJz549qVOnDhqNho0bN9K8eXPCw8PzMGRBEIoVhQLe+x1M7SD4Ghz9LtNTnMydmNZIu8jq9gfb0xwbJGSf/ZAhGFetijoigpA5c3kcGoPmraFZalnGNzRWPwEKQh7KcQI0a9Ys7ty5Q+PGjbl69SoXL15k06ZNbNq0iQsXLnDt2jWaNm3KvXv39LoivCAIRYCVM3RbqP351EJ4fCLTU2qXrk0z12aoZTVLri7J5wBLBsnICOdvvwWFgshduyhz7woKKXU5d3vTgg9OEPJYjhOgrVu3Ymdnx549e6hZs2aq4zVq1GDnzp3Y2tqydatYu0cQhExU66qdMRoZtn0McS8zPWVs7bEA7H68m0fh4lbtvGDqWRO7QYMASPjfbH7oVBEDKWUWdO5xmD5CE4Q8leMEKDg4mFatWmU40ZC1tTWtWrUiJCQkp5cRBKEk6TBHu0hq5FPY9X+Z3hpfw74Gbd3aopE1/Hb1twIKsvgrNX4cRq6uqAKDaHFyKyentOafkY0Y3Uq7gO3sPXeIik/Sc5SCkDs5ToAcHR3JyiTSsixTunTpnF5GEISSxNhCe2u8whBuboOr6zM9ZYz3GCQk9vvu527Y3QIIsvhTmJnh9M03AISt+Qsbv/s0rmDPZ+0qU87BnOdRCSw8fF+/QQpCLuU4AerSpQs+Pj5ERESkWyY8PJyjR4/SuXPnnF5GEISSxrUutJqi/XnPRAh7nGHxyraV6ViuIwC/Xvk1v6MrMSyaN8Oqa1eQZQInTyH61CkUoc+Z3rU6ACtP+fLgWZSeoxSEnMtxAvTtt99iZ2dH586duX79eqrjN27coGvXrtjb2/Ptt9/mKkhBEEqYZhPArTEkRsPWj0CtyrD46FqjUUgKfJ74cP156u8jIWccp05BMjUl8fFjngwfwYM2bal9/Tjtqjmi0sh8s+NWlnoCBKEwyvJiqGktZxEWFsaOHTuQJIlatWpRrlw5AB4/fsy1a9eQZZlu3bphZ2fHn3/+mbeR64FYDFUQClC4P/zeFBIiodU0aDU5w+JfnfyKfx/+SxOXJix9Z2kBBVm8JQUH86B1m5RjsRQKTLfsov3fd0hUafj9gzq86+msvyAF4Q3Z+T2d5QTozUVOs0uSJNRqdY7PLyxEAiQIBezaRtg6EiQDGLYfytZPt2hAVABdt3VFJatY2WEl9ZzqFWCgxVPM2XP4DxmSar/b6tUsjbRh4eH7uFibcPjzVpgqxZJHgv5l5/e0YVYrXblyZa4DEwRByBavvnD/AFzfBFtHwKiTYGyZZtEylmXoWaknG+9tZNHlRazquApJSmMSGyHLlB7u2okqNZrXOyUJpbsbo+1KseW/AJ6Gx/GbzwM+b19Ff4EKQg5kuQVIEC1AgqAXceGwpBlEPAHvD+C99G93D44JpvPWziRqEln6zlKauDQpuDiLqfDNmwmaPuN1EiRJeGzcgKmnJ/tuBDFq7SWUhgoO/l8L3O3N9RusUOJl5/d0niyGmpiYyJkzZ9i8eTObN2/mzJkzJCZmvJ6PIAhClpjaQM9lICngyt9wc3u6RZ3MnehbpS8Av17+VQzQzQM2vXtT8chhyq5ehXmrVtq7wr6YhCY2lg41nGheyYFElYZvd93Sd6iCkC25SoBUKhVff/01pUuXplmzZvTr149+/frRrFkzSpcuzfTp01GpMr57QxAEIVPuTaDZ/2l/3vkpBPwHj49DxNNURYd7DsfU0JTrodc5FnCsgAMtnoycnLBo2BDXuXMwdHQk0deXkP/9D0mSmNG1BoYKiUO3n3Hkjpj0Vig6cpwAaTQaunXrxuzZs4mMjMTGxobatWtTu3ZtbGxsiIyM5Pvvv6d79+5o3uw/FgRByIlWU8GlNsSHw/I2sLor/FwTLq1JUczB1IEBVQcAsPjKYjSy+P7JKwY2NrjMmQ1A+D/riT52jIqlLRjeTHsH8Mydt4hPKvo3vAglQ44ToOXLl7Nv3z7c3d3ZvHkzL1684OLFi1y8eJEXL16wZcsW3N3d2bdvX7G4BV4QBD0zMIL236fcJ2tg52epWoKG1hyKhZEFd8LucMjvUMHFWAKYN2mC7aCBAAR++RWqsDDGta1EaUtj/F7E8ufJjCeuFITCIscJ0Jo1azA1NeXIkSP07Nkz1fEePXpw+PBhjI2NWb16da6CFARBAEBOo3VBVkNYyoVQrY2tGVRdu6Dn4iuLUWtEq0ReKj1hAsqKFVCHhhI0fTrmSgO+7FwNgF+PPCAwPE7PEQpC5nKcAN24cYNWrVrh4eGRbply5crRpk0bbty4kdPLCIIgvGZXQTsY+k2SgXYB1bd8WP1DrI2teRTxiD2P9xRQgCWDwsQE1//9D4yMiD50mIitW+lWy4UGHnbEJan5fs9tfYcoCJnKcQKUkJCAtbV1puUsLS1JSEjI6WUEQRBes3aFrr9ok55kpaqCZeqZiC2VlgytMRSA36/+TpJGrF6el0yqVaPU+HEAhHw/m6SAAL7pVgOFBLuvBXH6QaieIxSEjOU4ASpbtixnzpzJcIZntVrN2bNnKVOmTE4vIwiCkFKdQfDZdei6EBRKeHYTzqS9COr7Vd/HzsSOJ1FP2PFgRwEHWvzZDxuGab26aGJjCZw0mWqlzRjYyB2AGTtukqQWA9CFwivHCVCHDh3w9/fn008/JSkp9V9WiYmJjB8/Hn9/f959991cBSkIgpCCtSvUHQydftA+P/QN+J9LVczMyIwRniMAWHJtCYlqMT9ZXpIMDHCZ+wMKc3PiLl/mxfLlTHinCnbmSu4/i2b1aV99hygI6crxTNBPnz7Fy8uL8PBwXFxc6N+/v24x1EePHrFhwwYCAwOxs7PjypUruLq65mng+iBmghaEQkaWYctwuLEFrMrAqBNgZpeiSII6gU5bO/Es9hlTG0xlQLUBegq2+Arfvp2gKVPB0BCPf/5hR7wVk7dcx8LYkCMTW1La0kTfIQolRL4shpqWCxcu0KdPH/z9/VOtuSPLMm5ubmzevJl69YrHooQiARKEQig+Epa1grCHUKkDvL9eu37VGzbe3ci3Z7/FwdSBPT33YGpoqp9YiylZlnn62f8RtX8/ynLlcN+8mV4rL3E1IIJedcqwoG8tfYcolBAFlgCBtqtr06ZN+Pj48PSpdi4OV1dXWrVqRZ8+fVAqlbmpvlARCZAgFFLB1+GPtqBOgHe+habjUxxOUifRdXtXnkY/5fO6nzOk5hD9xFmMqV6+5HG37qieP8d2wACCh47jvcWnANgyujF13e0yqUEQcq9AE6CSRCRAglCIXVwJuz7T3iE2dC+4NUxxePuD7Xx96mtsjG3Y12sf5kZi4c68Fn3yFE9GaMdclf1jGd+GWLPh4hNquFixY2wzDBRSJjUIQu4U+GKogiAIeld3CNTsrZ0YcfMwiA1LcbhL+S54WHkQnhDO2ltr9RNjMWfRrCm2H34IQOC0aUxsVBorE0NuBkbyz3l/PUcnCCmJBEgQhOJBkqDrz9rJEiMDYNsoeGMdQkOFIWO8xwCw+uZqIhIi9BRo8VZ64ucoK1RA/TyUhB++5/N3KgMw/8BdXsaIu/CEwkMkQIIgFB/GltB3NRgYw/39cGZRisMdPDpQ0aYiUUlRrLm1Jp1KhNxQmJjgMu8HMDQk6uAhugRdoqqTJeGxSfzvwF19hycIOiIBEgSheHHyhHeT5weamWJ+IIWkYGztsQCsvbWWsPiwtGoQcsm0Rg1KjdPOEh06ezaz6tsA8M95f248FS1vQuEgEiBBEIqfFOOBhqYYD9SmbBuq21cnVhXLyhsr9RdjMWc/YjimdbWzRJde/APveTkhyzBlyzVO3Q8lKEIsmCrol0iABEEoflKMB3qaYjyQJEmM9da2Aq27vY4DvgcIjgnWY7DFk2RggMsPc7WzRF+6xGfPz6I0kLgRGMkHf56j6dwjbLggBkYL+iMSIEEQiqfk8UCGJqnGAzVzbUZZi7IkahL5/NjndNjSga33t+ox2OJJWaYMjl9+CUDcsiW4v3iiO6aRYdrWG6IlSNAbkQAJglB8pRoPdBaAkNgQAqIDdMU0soaZZ2aKlqB8YN3jPSzfeQfUKiZeXIex6vWdYGpZxjc0Vo/RCSWZSIAEQSje6gwGzz6v5weKeYF/pD8yKeeA1cgankQ9SacSIackScJp1kwkewfcop8x5upWvJ4/wCEuHAnwcDDTd4hCCSUSIEEQijdJgi4/gX1F7Xig7aNwsyiLQkr59SchUdayrJ6CLN4MbW0pM3c2AO2fXOSHU0tYvf973vE9x4n7oXqOTiipCnUCdOHCBTp16oSNjQ3m5uY0atSIjRs3ZulcWZbZu3cvo0ePxsvLC2tra8zMzKhVqxazZ88mPj4+n6MXBKHQMLaEPsnjgQ7gdHUDMxrPSJEEycicCzqXQSVCbhhXqpTiuQKZ8Vc389O6k1x9Eq6foIQSrdCuBXb06FE6dOiAiYkJ/fv3x9LSki1btuDn58f8+fP5/PPPMzw/Pj4eU1NTjI2NadWqFZ6ensTHx7N//37u379P/fr18fHxwcws682vYi0wQSji/lsFOz99tV7YHoLtPXgS9QSfJz6subUGEwMT1nVeRyXbSpnVJGRTzNlz+A8Zkmr/pKajCK1Ykx1jm1HK0rjgAxOKlSK/GKpKpaJq1aoEBARw9uxZvL29AYiIiKBBgwb4+vpy79493N3d060jKSmJefPmMWbMGGxtbVPs79WrFzt37mTevHl88cUXWY5LJECCUMTJMmwdCdc3gZUrfHwCzO3RyBpGHxrN6cDTeFh5sL7LerFYah5LCg7mQZu2KZYnAZjebyYXEsxpUM6Ov0c0xMigUHdMCIVckV8M9ciRIzx8+JABAwbokh8Aa2trpk2bRmJiIqtXr86wDiMjI7788ssUyU/y/qlTpwJw7NixPI9dEIRC7O3xQJuGwMNjKCKDmNN8DqXNSuMb6cvM0zMphH8bFmlGTk44z5oJipS/dr59dgJLpYLzj8P4fvdtPUUnlESFMgHy8fEBoH379qmOdejQAchd8mJkZASAoaFhjusQBKGISh4PpDAE3+PwVzf4uSZ2t3axoOUCDCVD9vruZcPdDfqOtNix6d2bikcO47Z6NS4L5oOREfLRQyyPPQ3AqtO+bPkvIJNaBCFvFMoE6P79+wBUqpS6H97JyQkLCwtdmZxYsWIFkHaC9aaEhAQiIyNTbIIgFAOmtqBRv34ua2DnZ3gbl+Kzup8BMO/CPG6G3tRPfMWYkZMT5g0bYN25My6ztXeGWe3azI9o3+tp265zPUCsFybkv0KZAEVEaD/81tbWaR63srLSlcmuvXv3snTpUqpVq8bw4cMzLDtnzhysra11W9my4hZZQSgWwh7CW/MAIash7BGDqg+irVtbkjRJfH7scyISxC/j/GLdtQulv5gIQLXtKxktPyJBpWHU2v94EZ2g5+iE4q5QJkD55cKFC/Tr1w9ra2s2bdqEsXHGdxxMnTqViIgI3fbkiZgkTRCKBbsKIKXx9RcfgSRJzGo6izIWZXga/ZSvTn6FRtakLivkCbthw7D98EMAuu35g/aJT3kaHscn6y6hUov3Xcg/hTIBSm75Sa+VJ3mUd3ZcvHiR9u3bo1Ao2L9/PzVq1Mj0HGNjY6ysrFJsgiAUA9au0PUX7e3wb9o+GgKvYKW04sdWP6JUKPEJ8GHVzVV6CbMkkCQJx6lTsGzfHpKS+L/jf1AtNoSzj8KYs/eOvsMTirFCmQAlj/1Ja5xPcHAw0dHRaY4PSs/Fixd555130Gg07N+/n/r16+dZrIIgFFF1BsFn12HwLhj7H7g1gYRI+KsHPLtNNftqTGk4BYCFlxZyMfiingMuviQDA1z+Nw/TunUhOpp5F1fiEBvOnycfs/3yU32HJxRThTIBatmyJQAHDhxIdWz//v0pymQmOflRq9Xs27ePhg0b5l2ggiAUbdauUK45OFSEARvApQ7EhcGa7vDiIb0r9aZz+c6oZTWTjk8iNE4s25BfFMbGlF38K8oKFTAMC2XxtdWYJ8YxZes1bjwV47CEvFcoE6C2bdtSvnx51q1bx5UrV3T7IyIimD17NkqlkkGDBun2BwUFcefOnVRdZv/99x/vvPMOKpWKvXv30rhx44J6CYIgFDUmVvDhFnCsCdEhsKY7UkQA0xtNp7x1eZ7HPWfK8Smo37x7TMhTBjY2uP2xDMNSpbAKfsKP1/9GHZ/Ix3/9R1hMYuYVCEI2FMqZoCF7S2EMGTKE1atXs3LlSoa8mmo9LCyMihUr8vLlSzp27Jhmy4+NjQ2fffZZlmMSM0ELQgkQ/QxWdoIX98GuPAzdy0N1DO/vfp84VRwfe33M2Npj9R1lsRZ/5w5+H3yIJiaGi+XqMN2rP00qlWL10AYYipmihQwU+aUwkp0/f54ZM2Zw+vRpkpKS8PT0ZMKECfTr1y9FubQSIF9fX8qVK5dh/e7u7vj6+mY5HpEACUIJEfEUVnaEcH8oVQ2G7GZXyBmmnpiKhMTv7X6nqWtTfUdZrMWcOYP/Rx9DUhI7K7fkt+pd+ahFeaZ1qqbv0IRCrNgkQIWNSIAEoQQJewwr34WoIHCuBYN3MuvyL2y6twkbYxs2dd2Ek7mTvqMs1iJ27iLw1XqNy2p2ZVvFlix8vzbdarnoOTKhsCrya4EJgiDonV05GLQDzBwg6Cr83YfJtT6hml01whPCmXhsIkmaJH1HWay9OVHiRzd20iLgCpM2X+VWoJiVX8g9kQAJgiCkp1RlGLQdTKzhyTmMNw1hQbPZWBpZcvX5VX767yd9R1jsvTlR4heX11Mp8B4fr71IeKwYFC3kjkiABEEQMuLkCR9uBaUFPD5O2T3T+LbxDAD+uvUXh/wO6TnA4u3NiRIN1Sq+ubAag8ePGPfPZQJexnL6YShBEXH6DlMogsQYoGwQY4AEoQTzPQlre4MqDqq/x3yPGqy+/RcWRhZs6LIBNys3fUdYrGkSEvAfNpy4//7jhak1nzUfxwszG2RAIcGcnp70qy/+DUo6MQZIEAQhr3k0g/5rQWEEt7bz/+3de1xUZf7A8c+ZgRnuchEkULkEpmneL6WSbl7wklpq5upuapTVum1tN1d3f4K7/bLWyp/lbrlbaqtZqdlF84KWmGWabnmpNE0FkUQElItcZ+b5/YFMIggDzDAjfN+95iU8z5lnvufLwHw75znPeezMCXoEd6ewvJAndz5JWn4aX5/9msxLmc6OtFm6cqHEoOI8/vbVv2mbl0nX8z8RWHSRueu/kyNBol7kCFA9yBEgIQRHNsCaaaDMZPb8DZOKDnOh9IK1W6fpSLwtkfGx450YZPNV/vPPHJs4CV1uDgrQAAsai7tPZMb/PsZtNwY5O0ThRHIESAghHKXTGLj7dUAj9JtVzPaMrtJtURbmfzVfjgQ5iHtYGL6Jf7UWPwA6FH84uI7QUrllhrCdFEBCCFFfXSfBmP8DIPjIpmrdFmUhvSC9iYNqOYJaeVuLn0p6pXhl5Q6yCkqcEpO4/kgBJIQQDdFrOsQvoH25CV0NMwlCvEKaPqYWwhAZAbqqH18K0I4d5a4lX8o6QcImUgAJIURD3fY7QvvNIjE795ci6PK/f9rxBFlFWU4MrvlyDw3lhr/Or1IEacCD329kws5VTP7H52z/4ZzzAhTXBZkEXQ8yCVoIUc3JnfCfsWTq9aS7u5Gj0/Fs60Dy9HpCPENYfMdiurTu4uwom6XyzEzK0k7j3q4t+R9/zPnFr4BSHA1ox3N9pzFzwm08EBeFpl19wkw0V3IvMAeRAkgIUU1eBvxfF1AWa1O6mxuPxnbjRMl5jHojf+3/V0ZFj3JikC1D4a5dZDz5FJb8fC4avFnQ57d0Gvkr/nZXFwxucsKjJZCrwIQQoqm0Cocxi0HTW5vamUysOvotgzzCKDWXMnvXbF755hUsVxRJwv584uKIWv8+xk4d8S+7xHNfLsX0zkp++8YeLlySW2eIquQIUD3IESAhxDXlZUDuSfBpA18uhgOrMAOvtO/EMv0lAH7V7lcsiFuAt7u3c2Nt5izFxWQmJZH30ccAfB7WlfVDp/PPB+O4MdjHydEJR5JTYA4iBZAQwmbfvQ8bHofSfDb4B5EU6EeZMhMbEMsrv3qFtr5tnR1hs6aU4sLq1Zx7bgGYzaT5tmFR3P3M+90oBsS0dnZ4wkHkFJgQQjhblwnw8BfQti9jLuawPCOD1pqB4xeOM+WTKezP3O/sCJs1TdMInDqViJUr0bUOJqLgHM8lv8w/n32Tt/emOTs84QKkABJCCEcJiIAZm+H2p+laWs47aafoZNa4UHqBB5MfZN2xdc6OsNnz6tmDGz94H2PPXniZSvnLnhUc+9+F/PWjw5gtcgKkJZMCSAghHEnvBnf8BaZvJNQ7lLfSTzPiUjEmZWL+V/N5/uvnMVlMzo6yWXMLDibqreUE/Pa3AEw+9inRL/2F37++g4KScidHJ5xF5gDVg8wBEkI0SlEufPwo6uhG/uXvx5IAfwBuu+E2Fg5aSCtjK+fG1wLkbdjImT//BV1ZKZleAawa8Ttmje+N6fRpwrt0ILxDpLNDFI0gk6AdRAogIUSjKQX/XQ5b5vCpQWNOcGuKdRoRfhG8eserRLWKcnaEzV7Jjz9y8pFZaD9nUK7p0CsLOiruKp+R8DjDn57p7BBFA0kB5CBSAAkh7CbrCKxL4MeLx3i0TTBn3dzwdfdhbr8/E+IVQnu/9oR6hzo7ymbLnJfH0Yd+h+7AN1XbNY2AjzbJkaDrlFwFJoQQri6kEzz4GTd1n8E7GZn0LCmhoLyQOV/MISE5gfh1w1l/fL2zo2y29K1aceme31ZvV4p33vmUvCKZG9TcSQEkhBDO4u4BoxYSdO9qns03W2+kCmBBkbQ7ibOFZ50YYPMWfktHLFS/T9jgNa+y6L5neH3TQS6VygT15koKICGEcLabRnJ2xN/gqpt2KhQJm+9je9p2uY2GA4R3iCQj4XHMl/NuQaPILwBvcyn3freFnrNn8NJ9f+I/27+n1GR2crTC3mQOUD3IHCAhhKNkHv2I+D1/xnJlEaSUtSjq4B/DQ90eYWjEUHSa/L+rPWUcS+Xn748T1jmWsJj25G3eQurLizFmnAYg392LbbcM4+ZZCYy7LQa9Tu4u76pkErSDSAEkhHCYvAzWv9GX+UEBWDQNnVI8k3uBXJ2e1a18KdRVFD0xfpE81GMWwyOGSyHkQMps5sLGT0hb9AoemRkA5Bm82dEjnp6PJjC8VxSaJoWQq5ECyEGkABJCONQ3/yFz0xOku+loZ7IQGv8C6NzI272YVeZs3vbzpUBfUfTc6NOWmT0eJT4yHr1OX8fAoqGUyUT2Rxs4vfhVvLIq5mNdMPrwZe+R3Pb4TAbeIvd0cyVSADmIFEBCCIervKt8YDS0Cq9os1jg2Gbyv1zE2wU/stLPz1oIRXmFMrPXY4yMHCmFkAMpk4nM9z8k45UleOecAyDX6Mu+/ncy6MmZ9IiRJQtcgRRADiIFkBDC6U7voeCLRazO2s1//HzJ11cUPZEerZnZ63FGRo/GTefm5CCbL1Vezpn33ifzH6/hcyELgGwPPw7dfhdDn56JT3EBGd8dk1WlnUQKIAeRAkgI4TLOH6Nw9yLeSd3MW75e5F0uhNob/JnZ8zFGx95FdtZ3nD67n/Y39CY0tLtz421mVFkZp95eQ87rr+OTlwNAgbsn3uXFsqq0E0kB5CBSAAkhXE5BJpe+WsK7P77LW94GLlwuhALQc1GZUJcnVCe2HcH4oS86Odjmx1JWxvHlb5O/dCk+RXlV+9DIfPkNBo+4Ta4cayJSADmIFEBCCJdVWkDRvjd479AbLPNQXNRXnQ+kKcW6O16nQ/uBTgqweft6zSf4znuqWrsZjR/bxFDYuz9R40bSr38XjG4yV8tRpAByECmAhBAuz1zO55seZVbul9W6NAXdPIKJC48jrtMkOgbdLJdy20nGsVQujh2Fjl8+UhVUW2c6tVUY57v1I3TUcG4bMQBfD/cmjbO5kwLIQaQAEkJcDzIzDxC/5TfXXFSxUrDmzkD/jsTFjuPWG0fha/Bt4kibl+SF/yJs2f+hVwqzpvHz/Y8zYMJwfnj/Ey599hnBqUfQXfGRm+3ZivSOvWg1ZAh9JwwnOMDHidE3D1IAOYgUQEKI68X67U8x/8wW66KKicFx9A/syK6Tm9lVkMoeo55i3S8LKbop6GEMIi5sALd3nkp0UCfr0aHMzAMymdpGV64qffVVYOW5F/h+/WZytm4j8Mi3eJhKrX1FbkZORnXFfdBget4zGn1psVxN1gBSADmIFEBCiOtJZuYB0s/+l3Y39KpauJjLKTv9Ff89soZdZ/eyy1JAqqHqqZgw3IhrdRN6i4l384/+UkjJZGq7MJeU8NOWHZzesAW/b/fgV5T/Sx8aOhQaFROpT0x6kDHzH5fTlTaQAshBpAASQjRLl3JIP/I+u05s4vP84+xz0yi7xlVLmlL8o8fT9L75HjzdvZo40OZJWSyc3r2P4+s3Ydi9k+CL56ptk+3RipywKExRN+LTqRPhvbsS0/0mPD0MTojYdUkB5CBSAAkhmj2lKD57gH3fr2Zt+qek6Mtr3ExTinDciHH3J9q3LTFBnYkO60t0+K14GryvObycTqvd1+uT8Z37mE3blugNZAaGUdQuCvfYDrTu3oXoft0IDQuusl3GsdQWczqt2RRA+/btIzExkd27d1NeXs4tt9zCE088waRJk2weo7S0lBdeeIGVK1eSnp5OYGAgd955J88++ywhISH1ikcKICFES3KtydR+Fot1BeqraUoRpvTEuLci2iecmKBO3BjWl6i2A9iya37VeUlyOq2amq4mM2saBU8nUZyZRcnRoxhOn6T1+TMYzTUXp+e9A7kQ2h5LdAxcvEiHfZ+iQ7WIxRmbRQG0Y8cO4uPj8fDwYPLkyfj6+vL++++TlpbGiy++yJNPPlnnGBaLhVGjRrF161ZuvfVWBg0axPHjx/nggw+Iiopiz549BAcH1zlOJSmAhBAtTbXJ1G1HMH7Qs+T8/F9O/vwVP53/jhP5aZwoy+UE5daFGKup/Ki5opjSlGJB1D3EtBtA68AYAvzaoavH/cya69Gkmq4mu7posZhMnP3hJ05/fYCL3x1BnThOq7NpBBbm1jq2Ak5E3IypTTi6kBA8bgjFJ/wGAtqH0ya6HQEhAeiumBx/pevhSNJ1XwCZTCY6duzImTNn2LNnD927dwcgLy+Pvn37kpqayrFjx4iIiKh1nOXLl3P//ffz61//mrfffts6gez111/nkUceYebMmSxdutTmuKQAEkK0RNecTH01czm5mQc5ceZLTpw/zE/5qZwsyeEEZeTqa/5QvZJeKYKURpDmRmu9J8HuvgQZA2jtFUJrnzCC/drTOiCaoKAObPnyObsdTXLFQqq2q8lqU5idy4m9B8n69jvKvvqS6BMH6/W6xXoD+d7+FPkFUh4QhGodjHubNpSfOkns3u3WI0lnZjxG/OyH6rlXv3BUMXXdF0DJycnEx8czY8YMli1bVqXvrbfeYvr06cyfP5958+bVOk7//v356quvSE1NrVIsKaWIiYnh3LlznD9/Hk9PT5vikgJICCEawGLh6JH1TNqXhLrqdFqkWSNfs9hUIFVx9bpGSjFMF0CQ0Q8fdy+83b3xNvjiY2yFt6EV3h4B+HgG4u3VGh/vELy8gvH0COCDz56x62k5exVT9hinptNpFjTS7votlpIS1Pks9LnZeOTl4lt4EZ+yIpvHVkCRmwfFRi9KPbwo9/DC5OmNxcsb5e2D5uOD3tcXt1Z+GC8/PAP98QlsxbGPthK5bplDTsvV53PaJW8ZnJKSAsDw4cOr9cXHxwOwc+fOWscoKSlh79693HTTTdWOFGmaxrBhw1i6dCn79+8nLi7OPoELIYSoTqejY+eJJJ3dc81io7wkj9yc42RfPElOfhrZBRmcL8oiuySXnLICss3FnFflZGuKEp1WbVFHNI1t6iKUXIQS28LSlKooDS6PZdE0Es9s4YPln+GtM2DU6TFqbrjr3DDq3DHqDRh07hh0Box6IwY3Iwa9AaObJwY3T749+zVrStJRmoZ2UDHdpwNDOt2Lm84Nvd6Am97j8r/uuLld/trN83KbATd3T/R6Ix/tmMtfM7ZW5Olgw4uy8A6RfDG2A503/IhegVmD78d04N7n59S4fXFBIVmnMshJPUNe+s+UnM2k/FwWbqeOEXXmWNXcAd6mErxNJXCp9tNuVzIB0Vd8r0MRvmwRGeOGN/lpNZcsgI4fPw5AbGxstb7Q0FB8fHys21zLiRMnsFgsNY5x5djHjx+/ZgFUWlpKaekvC1Xl5+fXuJ0QQoi6jR/6Iv0zf1Pj6TR3j1a0Ce9Nm/DetY6hLBZST33GuF2PVzmapCnFA74d0TQdRaYiCk3FXDKXUmgp45KlnEvKTCEWijRFoaahLj+q0TQOaOVAOVgut5nrsZOXx1SaxvJLx1m+/9l6PLn6OJVF2d+Xb8YNDR2gU1T8C+gq267+XtMwKwsnb1YEttMTekGRGaCR6/sT65d3x0OrmGulXd5eo+LgQMV/oPPRIBZ0HTTcupTx0OsVr1vJrMGaez1wM7hjKFEYSlXFvyUKQ6nF+r2xtKLPWKowlik8ixXGq+Zu6xSc2JNMeIemnZztkgVQXl7FHXVbtWpVY7+fn591m8aMceV2NVmwYAHz58+vM14hhBC2CQ3t3qhTQ5pOR9SNQ0k6NaLBp66U2UxxcTan0r/g13sTqxVSc0IH4+XuQ6m5mDJTKaXmUsrMZZSZSym1lFd8bSmnVJkos5goVSbOm4o5qrdUe63WZoWeihrKrFUcATFd8bWlpiKs2k5rXLJ5EUR11b8auX6Q6/fL83/QzNhc1SnAHxipY+Zmi/VI0r9G6tgRVbk3de4AlXdFC8xX/POf5mrFlCmo9s90R3DJAshVzJkzhyeeeML6fX5+Pu3atXNiREIIIaD2o0l10fR6vHza0LnTBJIyvrLLHKCalgzQKcU7o1bVGpvFbMJsKcNUXoLZXMrZzANM/PLpauO82f1JAlu1x2IxY7aUoyxmzMqEspixKAsWi6miT5lQyoLZYiI3/wxz0j6sVuAlhg+nlWcwFmVGoVBKoZQFhcKiLFRMDVYV/UpxsTiHl7vu4WDUL0eSLvjCY/498PXwr3j+5YKrcjyu+r6yP98vj3+NOMyDW5S1mHpjhMafeg2rd84byyULoMqjNtc6OpOfn09AQECjx7hyu5oYjUaMRmOd8QohhGh6jT2aBI0rpK6OJbFt9aNSdY2n07uh07vhfnlV7VjfG0hM+7TaOL27z2hQXKXbS+xS4PldXg4h10+HTimSGjFZfL3vUzwatZmQi5DlD4/ePNIpV9+5ZAF05fycXr16VenLzMyksLCQvn371jpGdHQ0Op3umnOFaptnJIQQouWwRyEF9ium7DWOS8fUxT5jNYZLFkCDBg1iwYIFJCcnM3ny5Cp9W7dutW5TG09PT/r27cuePXtIS0urdhn8tm3b8Pb2pnfv2ifcCSGEELayVzFlr3HsOZYrxtQYLlkADRkyhOjoaFavXs0f/vCHKgshPvfccxgMBu677z7r9mfPniUvL48bbrihyimtmTNnsmfPHubMmVNlIcSlS5dy8uRJZs6cafMaQACVSybJ1WBCCCGE66n8fLZpiUPloj777DPl7u6ufH191YMPPqieeOIJFRERoQD14osvVtl22rRpClDLly+v0m42m1V8fLwC1K233qpmz56tJkyYoDRNU1FRUSorK6teMaWnp1fO7JKHPOQhD3nIQx4u+khPT6/zM90ljwAB/OpXv+KLL74gMTGR9957z3oz1BdeeIF7773XpjF0Oh0fffQRzz//PCtXrmTRokUEBgaSkJDAs88+W6/7gAGEhYWRnp6Or6+v9WhSpT59+rBv375a2671feXVZenp6Q5ZYbqm2OzxnNq2uVZfY/IEODRXzsjTtfob2tYU7ylH5amu7RzxnpLfPcmTLe11tbXEv1HXanf2e0opRUFBAWFhYXVu67IFEEDfvn3ZvHlzndutWLGCFStW1NhnNBpJTEwkMTGx0fHodDratm1bY59er6/2Q7y6ra7v/fz8HPLHpabY7PGc2ra5Vp898gSOyZUz8nSt/oa2NcV7ylF5qms7R76n5HfPtu8lTzW3tcS/Uddqd4X3VG1Xd1+pnjdfEdcya9asOtvq+t5RGvI6tjyntm2u1Sd5sq2/oW1NkStH5amu7eQ9Zds2kifbtmlMnq5ua855qq3fld9TtnDJm6G2NHKTVdtJrmwjebKN5Mk2kifbSa5s4wp5kiNALqDyNJ0sulg3yZVtJE+2kTzZRvJkO8mVbVwhT3IESAghhBAtjhwBEkIIIUSLIwWQEEIIIVocKYCEEEII0eJIAdQMFBQU8PDDDxMWFoa3tzc9evRg3bp1zg7LJUVGRqJpWrXHwoULnR2ayzl9+jSTJk0iICAAb29v+vTpQ0ZGhrPDcilJSUnV3ktyf8HaPfLII2iaxpIlS5wdiktatGgRnTt3xsfHB39/f+644w727t3r7LBczoIFC+jduze+vr60adOGSZMmkZqaWq8xpABqBv74xz+SkpLCmjVrOHz4MJMmTWLy5MkcOnTI2aG5nH379nH27FnrY/Xq1QCMHz/eyZG5lpycHAYOHIi/vz/bt2/n0KFDzJs3T65sqUG3bt2qvKcqb9gsqtu4cSNfffWVTav0tlQRERG8/PLLHDx4kN27dxMTE0N8fDw5OTnODs2l7Ny5k0cffZS9e/eyZcsWcnNzGTlyJCaTyeYx5CqwZqBLly785je/4U9/+pO1LSgoiJdeeonp06c7L7DrwNSpUzlz5gw7d+50digu5ZlnnmHv3r2SlzokJSWxceNG9u/f7+xQXN65c+fo3bs3mzZtYsyYMTz11FP8/ve/d3ZYLq9yvZyUlBQGDRrk7HBc1qlTp4iOjubgwYN07drVpufIEaAGWrVqFQ899BC9e/fGaDSiado1b8dRad++fYwaNQp/f3+8vb259dZbWbNmTaNj6d+/Px999BGZmZkopVi7di2lpaUu88viSrm6Ul5eHh988AEzZsyw67gN5Up52rBhAz179mTChAmEhITQp08f1q9f3+hx7cGV8gRw5MgRbrjhBmJiYpgxYwaZmZl2GbexXC1PM2bM4A9/+AO33HKLXcazJ1fLVaWysjL+9a9/ERAQ4BJ5c9U8QcXfc4DAwEDbn1Sv26ELq8o707du3dr69dV3o79Sfe5uX1/FxcXq17/+tQKUm5ub8vPzU1u3bm3UmPbkSrm60muvvaZ8fHxUYWGh3cZsDFfKk9FoVEajUSUlJalvvvlGLVy4UOl0OrVz585GjWsPrpSnTZs2qTVr1qhDhw6pzZs3qz59+qhOnTqpkpKSRo1rD66Up1dffVXdfvvtymw2W2N79dVXGzWmPblSrpRS6vPPP1fe3t5Kp9OpsLAwtX///kaPaQ+ulqdKJpNJDRkyRI0aNapez5MCqIG2bdumUlNTlVJKLViwoNY3Qnl5ubrxxhuV0WhU3377rbX94sWLqkOHDspgMFjHqjR79mwF1Pqo9Pzzz6ubb75Zbd68WR04cED97W9/U/7+/ur777+3+343hCvl6kp9+vRR999/v1320R5cKU/u7u5q4MCBVZ4/duxYNXXqVPvsbCO4Up6ulpWVpTw8PNS6desavZ+N5Sp5OnLkiAoJCVGnTp2yPtfVCiBXyVWloqIidfz4cbVnzx6VkJCgoqOj1fnz5+26zw3hanlSSimLxaIeeOABFRsbq7Kysuq1P1IA2UFdb4StW7cqQM2YMaNa34oVKxSg5s+fX6U9KytLHTlypNaHUhW/KO7u7mrLli1Vnj906FA1a9Ys++ygHTkzV1f67rvvFKB27dpll/2yN2fnqV27diohIaHK82fPnq3i4uIav3N25Ow81aRjx45q4cKFDd4nR3BmnpYvX640TVN6vd76AJROp1PdunWz9642miu+p2JiYtTf//73Bu+TI7hCniwWi3r44YdVZGSkOn36dL33wa3mE2PCnlJSUgAYPnx4tb74+HiAapNNg4ODCQ4OrnPs8vJyysvL0ev1Vdr1ej0Wi6WBETuPI3N1pWXLlhEbG8vAgQMbFqiTOTpP/fv356effqrSduzYMSIiIhoQrfM01fup0oULF0hLSyMyMrJBz3cWR+bprrvuqrY0QHx8PNOnT3eZ+Xf10dTvKQClFKWlpQ1+vjM4Ok9KKWbNmsUnn3zCzp07adeuXb1jlAKoCRw/fhyA2NjYan2hoaH4+PhYt6kvPz8/4uLieOqpp1iyZAlhYWF8/PHHbNu2jU2bNjUqbmdwZK4qmUwmVq1axeOPP96ocZzJ0Xn64x//yIABA3jppZcYN24c27dvZ8OGDdfdVWGOztPTTz/NmDFjaN++PWfOnGHu3LmEh4czatSoBo/pDI7Mk7+/P/7+/lXa3N3drRPHrzeOfk/Nnj2bsWPH0rZtW3Jzc/nnP//JmTNnmDBhQoPHdAZH52nWrFm88847bNiwAU9PT+vFB4GBgRgMBpvGkAKoCVTOTm/VqlWN/X5+ftZtGuLdd99l9uzZ3HPPPeTl5RETE8OKFSusVfb1xNG5Avjkk0/Izs7mvvvua9Q4zuToPPXr14+1a9fy5z//mb/85S906NCBtWvX0r9//waP6QyOzlN6ejqTJ08mOzubNm3aMGjQIFauXImXl1eDx3SGpvi9ay4cnauff/6ZyZMnk5WVRWBgIH369GHXrl106tSpwWM6g6Pz9NprrwEQFxdXpX3Hjh0MHjzYpjGkAGoGwsLCWLlypbPDuG6MGzcOs9ns7DBc3t13383dd9/t7DBc2rvvvuvsEK5L9V2xtyWRv+W2UXZYwlDWAWoClRXwtardyoWuhOTKVpIn20iebCN5sp3kyjbXQ56kAGoCledAazrfmZmZSWFhYY3nSVsiyZVtJE+2kTzZRvJkO8mVba6HPEkB1AQqV2ROTk6u1ld53yBXWbXZ2SRXtpE82UbyZBvJk+0kV7a5HvIkBVATGDJkCNHR0axevZoDBw5Y2/Py8njuuecwGAzX9YRce5Jc2UbyZBvJk20kT7aTXNnmesiT3Ay1gd544w2++OILAA4fPsw333zDgAEDrJd1Dhw4kAceeMC6/Y4dO4iPj8fDw4PJkyfj6+vL+++/T1paGi+++CJPPvmkU/ajKUiubCN5so3kyTaSJ9tJrmzT7PJU76UThVJKqWnTptW6XPe0adOqPWfv3r1qxIgRys/PT3l6eqq+ffuqd999t+mDb2KSK9tInmwjebKN5Ml2kivbNLc8yREgIYQQQrQ4MgdICCGEEC2OFEBCCCGEaHGkABJCCCFEiyMFkBBCCCFaHCmAhBBCCNHiSAEkhBBCiBZHCiAhhBBCtDhSAAkhhBCixZECSAghhBAtjhRAQgiXlJqaiqZpREZGNsnrKaVYuHAhXbp0wdPTE03T0DTN2l9aWsrcuXOJjY3FaDQ2aWxCCPuTAkiIFiIyMhJN00hNTbXruElJSWiaRlJSkl3HbWqvvfYazzzzDGfOnGHkyJFMmzaNadOmWfv/53/+hwULFlBQUMC4ceOYNm0aEydObNIYm0uuhXAFbs4OQAghXMGaNWsAWLt2LcOGDbtm/65du4iNjW3S2IQQ9idHgIQQAjh9+jTANYubuvqFENcXKYCEaOZWrFiBpmmkpaUBEBUVZZ3fomkaKSkpVbb/+uuvmTRpEmFhYRgMBkJCQhgzZgzbtm2rNramacyfPx+A+fPnVxl3+vTp1u1++OEHEhMTGTBgAOHh4RgMBoKCghg6dKj1yIo9mUwm3njjDQYPHkxgYCBGo5GoqCgeeeQR0tPTq2w7ePBgNE3j1KlTQNX8JCUlWU8dKqWs+1z5WLFiRZWx/vvf/zJ16lTat2+P0WgkMDCQ+Ph4Nm3aVGusy5YtY+jQobRu3Rqj0Ujbtm0ZOnQor776qnU7W3MNkJuby9y5c+ncuTNeXl74+vrSq1cv/v73v1NcXFwthpSUFDRNY/DgwRQVFTFv3jw6deqEl5eXzHMSzZacAhOimYuJiWHatGmsW7eOS5cuMWHCBHx8fKz9oaGh1q///e9/8/DDD2OxWOjRoweDBw8mLS2NjRs3snHjRpKSkkhMTLRuP23aNA4cOMDBgwfp1q0b3bt3t/YNHDjQ+vXLL7/Mm2++SceOHbnlllvw9/fn9OnT7Nixg08//ZQ9e/bw8ssv22V/CwoKGDt2LCkpKfj4+NCrVy+Cg4M5fPgwr7/+OmvXrmXbtm306NEDgBEjRhAZGVljfrp3787EiRPJzs7mrbfesu7zlbmttHjxYp544gksFgvdu3enX79+ZGZmkpKSQnJyMvPnz2fevHlVYs3Ly+POO+/kiy++wN3dnf79+xMWFkZmZiaHDh3i008/5dFHH61Xrk+ePMkdd9xBWloawcHBjBo1ivLycnbs2MHs2bN577332L59OwEBAdVyV1JSwuDBg/nhhx+4/fbb6datGzk5OY38iQjhopQQokWIiIhQgDp16lSN/YcOHVJubm5K0zT1n//8p0rfpk2blMFgUIBKTk6u0peYmKgAlZiYeM3XTklJUSdOnKjWfvToUdW2bVsFqL1791bpO3XqlAJURESETftXacqUKQpQd955pzp37lyVvkWLFilAxcbGKpPJVKWvrvwA6lp/Mrds2aI0TVOtW7dWO3furNJ36NAh6z6mpKRU6Rs/frwCVI8ePaq9bnl5ufrwww+rtNmS6379+ilAjR07VhUWFlrbs7KyVM+ePRWgpkyZUuU5O3bssO5f165d1dmzZ685vhDNhRRAQrQQdX3AJyQkKECNHz++xv7f//73ClDDhg2r0m7Lh3Jtli5dqgD19NNPV2lvSAH0ww8/KE3TVFhYmMrPz69xm1GjRilAbdiwoUp7YwqgyqJj3bp1NfavWbNGAWrChAnWtgMHDihAeXh4qDNnztiwd3XneteuXQpQXl5eKjMzs1r//v37FaB0Op1KT0+3tl9ZAH3++ec2xSLE9U5OgQkhAKxzga6eT1IpISGBJUuWsGvXLsxmM3q9vl7jFxYWsnnzZr799luys7MpKysD4OzZswD8+OOPDY690qZNm1BKMXLkSHx9fWvcZvDgwWzatIndu3dz5513Nvo1s7Oz+frrr/H09GTMmDHXfE2A3bt3W9u2bNkCwOjRowkPD290HPDLz3DEiBG0adOmWn+vXr3o1q0bBw8eZOfOnUydOrVKf0hICHFxcXaJRQhXJwWQEAKAjIwMoGIScE1uvPFGoGKeSE5ODiEhITaPvWHDBmbMmFHrfJL8/Px6RFuzkydPAvDmm2/y5ptv1rrt+fPnG/16AKdOnUIpRXFxMUaj0ebXrJyU3rFjR7vEAXX/DKHi53jw4EHrtleSCc+iJZECSAjhUBkZGdx7770UFxfzzDPPMHXqVCIjI/Hx8UGn05GcnEx8fLz1KqvGsFgsQMXk5W7dutW6bb9+/Rr9ele+po+PDxMmTLDLmM7i6enp7BCEaDJSAAkhAAgPD+fEiROcPHmSLl26VOuvPLri4eFBYGCgzeNu2LCB4uJi7r77bl544YVq/cePH2940Fdp164dAAMGDGDJkiV2G9eW19Q0jWXLlqHT2ba6SPv27QE4evSo3WKpPJVW+bOqSWWfvU67CXG9knWAhGghDAYDULHuTE0q56lcvbZNpWXLlgEQFxeHm9sv/+9U17i5ubkAREREVOtTSrF69eq6g7fRyJEjAfj4448pKSmx27i1CQsLo2vXrhQUFFjn9dhixIgRQMW8pZ9//tmm59j6M9yyZQvnzp2r1v/tt99y4MABdDodt99+u82xCtEcSQEkRAvRtm1bAL7//vsa+x977DHc3Nz48MMPWbVqVZW+5ORkli5dCsBTTz1Vr3E7deoEwLp166wTngHMZjPz5s2rMjG4sXr06MGECRNIT09n/PjxNd737NKlS7z99ts1FggN9eyzzwIwY8YMNmzYUK1fKcXevXtJTk62tnXv3p1x48ZRXFzMuHHjrCtNVzKZTHz88cdV2urK9cCBA+nXrx/FxcU89NBDFBUVWfuys7N56KGHAJg8ebL1yJUQLZZTr0ETQjSZJUuWKED5+Pio8ePHq4SEBJWQkKCOHj1q3Wbp0qVKp9MpQPXs2VNNmTJFDRgwQGmapgCVlJRUbdzMzEzl7e2tADVgwAA1ffp0lZCQoJYtW6aUqljPplevXtbXHj16tJo0aZKKiIhQ7u7uavbs2QpQgwYNqjJuQ9cBys/PV0OGDFGAMhgMqk+fPmrSpEnqnnvuUX369LGuZ3TkyJEqz2vMZfBKKbV48WLl5uamABUTE6NGjx6tpkyZooYNG6ZCQkIUoGbPnl3lObm5uerWW2+1xjp48GA1ZcoUdccdd6jg4OBqr1dXrpVS6sSJE9Z9CQkJURMnTlTjxo1Tfn5+1p9rbm5ulXErL4O/+mcgRHMmBZAQLYTZbFYLFixQnTt3Vh4eHtYP9B07dlTZbs+ePWrixIkqNDRUubm5qaCgIDV69OhqCyBe6fPPP1dDhw5VAQEB1gJq2rRp1v6CggI1d+5cddNNNykPDw8VEhKi7rrrLrV///5rfvg2tACq3NfVq1erUaNGqTZt2ih3d3cVFBSkunTpombMmKE++OADVVZWVuU5jS2AlFLq8OHDaubMmSo2NlZ5eHgoLy8vFR0dreLj49Urr7yiMjIyqj2ntLRUvfbaayouLk75+/srg8Gg2rZtq4YNG6b+8Y9/VNu+rlwrpVROTo6aM2eO6tSpkzWOHj16qOeff14VFRVVG1MKINESaUrZ4dILIYQQQojriMwBEkIIIUSLIwWQEEIIIVocKYCEEEII0eJIASSEEEKIFkcKICGEEEK0OFIACSGEEKLFkQJICCGEEC2OFEBCCCGEaHGkABJCCCFEiyMFkBBCCCFaHCmAhBBCCNHiSAEkhBBCiBZHCiAhhBBCtDj/D1Xr8vjYTd8GAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "variants = {\n", + " \"wt\": {},\n", + " \"destabilize HD (+3)\": {\"HD\": 3.0},\n", + " \"stabilize LE (-3)\": {\"LE\": -3.0},\n", + " \"stabilize L (-3)\": {\"L\": -3.0},\n", + "}\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "for label, ddg in variants.items():\n", + " ax.semilogx(effector, model.observable(effector, ddg=ddg), \"o-\", ms=3, label=label)\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"observable\")\n", + "ax.set_title(\"One state perturbed at a time\")\n", + "ax.set_ylim(-0.02, 1.02)\n", + "ax.legend(fontsize=8)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "c6728397", + "metadata": {}, + "source": [ + "## 4. A named mutation catalog + a titration table\n", + "\n", + "`MutationEffects` holds named mutations and their (optional) in-state pairwise\n", + "epistasis. `build_titration_df` then enumerates wt + singles + doubles and emits\n", + "the long-form table the analysis tools consume.\n", + "\n", + "> **Naming:** `cat_response` accepts any labels, but `extract_epistasis` requires\n", + "> the `XsiteY` convention (e.g. `A1V`, `A2V` → `A1V/A2V`). We use it throughout so\n", + "> both tools are happy." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fcd3e37b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:51.376026Z", + "iopub.status.busy": "2026-07-20T02:46:51.375881Z", + "iopub.status.idle": "2026-07-20T02:46:51.389079Z", + "shell.execute_reply": "2026-07-20T02:46:51.388571Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "genotypes: ['A1V', 'A1V/A2V', 'A2V', 'wt']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
genotypetitrant_nametitrant_concobservableobservable_std
0wteffector1.000000e-080.9703050.02
1wteffector1.778279e-080.9702200.02
2wteffector3.162278e-080.9700700.02
3wteffector5.623413e-080.9698020.02
4wteffector1.000000e-070.9693200.02
\n", + "
" + ], + "text/plain": [ + " genotype titrant_name titrant_conc observable observable_std\n", + "0 wt effector 1.000000e-08 0.970305 0.02\n", + "1 wt effector 1.778279e-08 0.970220 0.02\n", + "2 wt effector 3.162278e-08 0.970070 0.02\n", + "3 wt effector 5.623413e-08 0.969802 0.02\n", + "4 wt effector 1.000000e-07 0.969320 0.02" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "effects = (MutationEffects()\n", + " .add_mutation(\"A1V\", HD=3.0) # weakens repression\n", + " .add_mutation(\"A2V\", LE=-3.0) # tightens effector binding\n", + " .add_epistasis(\"A1V\", \"A2V\")) # NB: zero thermodynamic epistasis\n", + "\n", + "df = build_titration_df(model, effector, effects=effects,\n", + " observable_std=0.02)\n", + "print(\"genotypes:\", sorted(df['genotype'].unique()))\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a61fc694", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:51.391032Z", + "iopub.status.busy": "2026-07-20T02:46:51.390885Z", + "iopub.status.idle": "2026-07-20T02:46:51.578349Z", + "shell.execute_reply": "2026-07-20T02:46:51.577823Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAF6CAYAAADxk+dgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAqvJJREFUeJzs3Xd8U9X7wPHPTbpX2lKgg9KWvSkgCFKgDClbGcpSZIhfUBw/UAQciAMQwYEDRdkIlL2RvTfIni10UVqgdO+0ub8/Qiuhe96O83698mpzx7lP0jR58txzz5FkWZYRBEEQBEGoRFRKByAIgiAIglDaRAIkCIIgCEKlIxIgQRAEQRAqHZEACYIgCIJQ6YgESBAEQRCESkckQIIgCIIgVDoiARIEQRAEodIRCZAgCIIgCJWOSIAEQRAEQah0RAIkCDmQJAlvb+9SP667uzvu7u6lftzywNvbG0mSiqWtkSNHIkkSgYGBxdJecVHidffFF18gSRKHDh3K9z5K/X8IQnERCZBQph06dAhJkvjiiy+UDkUoZ8RrRygMpV83hUlGhcIxUjoAQSirbty4gYWFhdJhCIIgCCVAJECCkIMGDRooHYIgCIJQQsQpMKHEREVFoVar6dOnj8HyixcvIkkSkiTh7+9vsM7b2xtzc3NSUlL44osv6Ny5MwAzZszI3Keo/TY2bNhAp06dqFatGmZmZjg7O9OtWzc2bNhgsF12fRwy+o0EBAQwf/58GjRogKmpKW5ubsyYMQOdTpfleImJiUyePBlXV1fMzMxo0qQJf/75Z4FL7bIss3jxYtq3b4+NjQ0WFhY899xzLF68OMu2ycnJzJs3j+bNm6PRaLC0tMTd3Z1XX32VS5cu5fu5ys6mTZsYOnQoderUwcLCAo1GQ4cOHbI8fwCBgYFIksTIkSPx9/enf//+2NnZYWlpSbdu3XKM5dixY3Tq1AlLS0uqVKnC4MGDCQkJyXeMBXntyLKc778lwJYtW+jatSt2dnaZf8+5c+eSnp6e7/gA/vrrL5o0aYKZmRmurq5MnjyZ5OTkHLcPCgpizJgxuLi4YGJiQo0aNRgzZgzBwcFZts2tH1le/agWLVpE06ZNMTMzw8XFhf/7v/8jLi4u348rNTWV77//npYtW2JpaYm1tTUdOnRg69at+W5j6dKlSJLE0qVL2bZtG88//zwWFha4uLjw2WefZf5tli1bRvPmzTE3N6dmzZp89913WdrKra/Xs6eb8vO6uX37NpMnT6Zly5ZUqVIFMzMz6tWrx5QpU4iPj89yjIznW6vV8sUXX+Du7o6pqSn16tXjt99+y7LtjBkzAOjcuXPmsUWfwJIhKkBCibGzs6N58+YcPXqU9PR01Go1AAcPHszc5uDBg9SpUwfQf2ifOnWKF154AVNTU7y9vQkMDGTZsmV06tTJIBmxtbUtVEwLFizg7bffxsnJif79+1OlShXCw8M5c+YMmzZtYuDAgflq56OPPuLw4cP06dMHHx8fNm/ezBdffEFqairffPNN5nbp6en06dOHgwcP0rRpU4YNG0ZkZCSTJk0qUAdSWZYZPnw4q1evpm7dugwbNgwTExP27t3LmDFjuH79OnPnzs3c/o033mDt2rU0a9aMUaNGYWpqSkhICAcPHuTs2bM0b94838d+1tSpUzExMcHLywsnJycePXrE1q1bGTRoEPPnz+fdd9/Nsk9gYCBt27alcePGjB49mjt37rBlyxY6d+7MjRs3qF69eua2+/fvp2fPnqhUKgYPHoyzszP79++nffv22NnZ5SvGgrx28vu3zHjss2fPxsXFhQEDBqDRaDh69CgfffQRp0+fZt26dfmK76uvvuLzzz+nevXqjB07FmNjY3x9fblx40a229++fRsvLy8ePXpE3759ady4MVevXmXx4sVs27aNY8eOUa9evXwdOzfff/89+/fvZ/DgwfTu3Zt9+/bx448/curUKY4cOYKxsXGu+6ekpNCjRw8OHTqEp6cnY8aMQavVsmPHDl566SV+/vlnJkyYkO94Nm3axJ49e3j55Zdp3749O3bs4Ouvv0aWZTQaDV9//TUvvfQS3t7ebNiwgcmTJ1O9enVGjBhRqMefn9fNxo0bWbRoEZ07d8bb2xudTsepU6f49ttvOXz4cI7P09ChQzlz5gw9e/ZErVazdu1a3nnnHYyNjRk7diygT9YADh8+zBtvvJGZ+BT2/U7IgywIJWjixIkyIJ8+fTpzWd++feV69erJrq6u8tChQzOX79+/XwbkL7/8MnPZwYMHZUCePn16scTTsmVL2cTERH7w4EGWdREREQb3AblTp04Gy9544w0ZkD08POT79+9nLn/06JFsa2srW1tbyykpKZnL//rrLxmQe/bsKaelpWUuv3btmmxmZpbt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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "for geno in enumerate_genotypes(effects.mutations, order=2):\n", + " sub = df[df[\"genotype\"] == geno]\n", + " ax.semilogx(sub[\"titrant_conc\"], sub[\"observable\"], \"o-\", ms=3, label=geno)\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"observable\")\n", + "ax.set_title(\"wt, singles, and the double mutant\")\n", + "ax.set_ylim(-0.02, 1.02)\n", + "ax.legend(fontsize=8)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "1b5c912d", + "metadata": {}, + "source": [ + "## 5. Classify each curve with `tfs-cat-response`\n", + "\n", + "Feed the table straight in: `genotype` is the grouping axis, `titrant_conc` is\n", + "x, `observable` is y, `observable_std` is the weight." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3e0d9f14", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:51.580030Z", + "iopub.status.busy": "2026-07-20T02:46:51.579894Z", + "iopub.status.idle": "2026-07-20T02:46:51.664816Z", + "shell.execute_reply": "2026-07-20T02:46:51.664125Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
genotypebest_modelresponse_classomnibus_q
0wtrepressorreal0.0
1A1Vrepressorreal0.0
2A2Vrepressorreal0.0
3A1V/A2Vrepressorreal0.0
\n", + "
" + ], + "text/plain": [ + " genotype best_model response_class omnibus_q\n", + "0 wt repressor real 0.0\n", + "1 A1V repressor real 0.0\n", + "2 A2V repressor real 0.0\n", + "3 A1V/A2V repressor real 0.0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from tfscreen.analysis.cat_response.cat_response import cat_response\n", + "\n", + "results, predictions, assessment, delta = cat_response(\n", + " df, x_obs=\"titrant_conc\", y_obs=\"observable\", y_std=\"observable_std\",\n", + " progress=False)\n", + "\n", + "results[[\"genotype\", \"best_model\", \"response_class\", \"omnibus_q\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "dbb5187c", + "metadata": {}, + "source": [ + "## 6. The punchline: observable epistasis with *no* thermodynamic epistasis\n", + "\n", + "We set `add_epistasis(\"A1V\", \"A2V\")` with **all-zero** state effects — there is no\n", + "thermodynamic interaction between the two mutations. Yet `extract_epistasis`\n", + "reports substantial epistasis in the observable, and it **flips sign across the\n", + "titration**. That is the pure nonlinearity of $\\mathrm{HD}/(\\mathrm{HD}+\\mathrm{D})$,\n", + "not a property of the underlying biophysics — exactly the trap to watch for when\n", + "interpreting real epistasis curves." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "f7b22c30", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:51.667210Z", + "iopub.status.busy": "2026-07-20T02:46:51.667051Z", + "iopub.status.idle": "2026-07-20T02:46:51.879271Z", + "shell.execute_reply": "2026-07-20T02:46:51.878756Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from tfscreen.analysis.extract_epistasis import extract_epistasis\n", + "\n", + "ep = extract_epistasis(df, y_obs=\"observable\", y_std=\"observable_std\",\n", + " group_by=\"titrant_conc\")\n", + "\n", + "sub = ep[ep[\"genotype\"] == \"A1V/A2V\"]\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "ax.axhline(0, color=\"0.6\", lw=1)\n", + "ax.errorbar(sub[\"titrant_conc\"], sub[\"ep_obs\"], yerr=sub[\"ep_std\"],\n", + " fmt=\"o-\", color=\"#805ad5\", capsize=3)\n", + "ax.set_xscale(\"log\")\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"observable epistasis (ep_obs)\")\n", + "ax.set_title(\"Epistasis in the observable — with zero thermodynamic epistasis\")\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "011ad456", + "metadata": {}, + "source": [ + "Try flipping on a real interaction — e.g. `.add_epistasis(\"A1V\", \"A2V\", HD=2.0)` —\n", + "and re-running from cell 4. The curve's shape and baseline both change, and you\n", + "can start to build intuition for which thermodynamic interactions produce which\n", + "observable signatures." + ] + }, + { + "cell_type": "markdown", + "id": "200e0c38", + "metadata": {}, + "source": [ + "## 7. Random libraries with the sampler\n", + "\n", + "Instead of hand-setting every effect, draw them from per-state Normals. Each\n", + "value is either an sd (mean 0) or a `(mean, sd)` tuple. Handy for generating a\n", + "library of curves to stress-test the analysis tools." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "04fe6860", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-20T02:46:51.881040Z", + "iopub.status.busy": "2026-07-20T02:46:51.880895Z", + "iopub.status.idle": "2026-07-20T02:46:52.186706Z", + "shell.execute_reply": "2026-07-20T02:46:52.186109Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAF6CAYAAADxk+dgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnXecVcX9v5/be9neG7tL70V6FURFxIolohhrflGTaGLUxIgajfpVoybRRI01doMoKlgQkCrSe9ve++39nvn9cdkb1l1gQZp6ntfrujgzd86ccs95n5lPUQghBDIyMjIyMjIyPyGUp3oAMjIyMjIyMjInG1kAycjIyMjIyPzkkAWQjIyMjIyMzE8OWQDJyMjIyMjI/OSQBZCMjIyMjIzMTw5ZAMnIyMjIyMj85JAFkIyMjIyMjMxPDlkAycjIyMjIyPzkkAWQjIyMjIyMzE8OWQDJyJxE8vPzyc/PP9XDOCKvvPIKCoWCV155pUN5V+OfN28eCoWCZcuWnbTxycjIyHxfZAEk84OlvLwchULR4aPRaMjKymL27NmsX7/+VA9R5ifAwoULufXWWxk7diwmkwmFQsG8efOO+L3GxkZ+85vfUFxcjF6vJykpidGjR/Pcc8+d+EGfJsjiWeZUoj7VA5CR+b4UFhZy1VVXAeD1etmwYQPvvfceCxYs4Msvv2TChAmneIQ/HpYsWXKqh3Da8cQTT7B8+XKsViuZmZns37//iN/ZvHkzZ511Fm1tbcyYMYNLLrkEj8fDrl27WLhwIb/4xS9OwshlZH7ayAJI5gdPUVFRpzfuRx55hLvvvpt7772X5cuXn5qB/QgpLCw81UM47XjwwQdJT0+nqKiId955hyuuuOKw7V0uF7NmzQJgw4YNDBw4sEN9JBI5YWOVkZH5H/ISmMyPkuuuuw6IPWC+y0svvcSsWbPIz89Hr9eTmJjI9OnTWbp0aae2y5Ytiy9prF+/nmnTpmGxWLDZbFx44YWUl5d3uf0PP/yQESNGYDAYSEtL44YbbqCtre2Q421ububXv/41BQUF6HQ6UlNTmT17Ntu3b+/Udu7cuSgUCkpLS3n88cfp2bMnBoOBvn378vbbbwMQCoX4wx/+EN/HgQMHsmjRou4cusNyJBumf//73wwYMAC9Xk9WVha/+c1vcLvdHdq0L13OnTuXXbt2ceGFF5KUlIRCoYgfzw8++IArrriCoqIijEYjNpuN8ePH89///rfTNg/X35YtWzCZTPTr16/L8UqSRH5+PgkJCfj9/mM6JuPHj6e4uBiFQtGt9s8++yyVlZU88sgjncQPgFp9dO+l//rXv+jXrx96vZ6cnBzuvPNOAoEACoWCSZMmdWrvdru577776NevHwaDAbvdzvTp01m5cmWntpMmTUKhUBAOh5k3bx75+fnodDp69uzJs88+2+V4vF4v9913H717947/vmbMmMGqVas69X3//fcDMHny5Pgydn5+PpIkkZeXR1JSEsFgsMvtTJgwAbVaTXV1NdDRbu3DDz/kjDPOwGg0kpKSws9//nMaGhq67KesrIzrr7+e3NxcdDodGRkZzJ07l4qKik5tN27cyCWXXBJvm5KSwogRI3jooYe67FvmNEfIyPxAKSsrE4CYPn16p7rGxkYBCJvN1qlOr9eLkSNHiuuuu07cddddYs6cOcJisQilUikWLFjQoe3SpUsFIM4991xhMBjEueeeK+644w4xZcoUAYjCwkLh9/s7fOfVV18VgLBareKGG24Qv/vd70SfPn3E0KFDRUZGhsjLy+s01sLCQgGISZMmibvuuktcdtllQqVSCaPRKFasWNGh/TXXXCMAMWvWLJGeni5uuOEGcfPNNwu73S4UCoVYvHixmDFjhigoKBD/7//9P/Hzn/9c6PV6odFoxP79+7t1bF9++WUBiJdffrlDeV5eXqfx33fffQIQM2fOFEajUVx77bXi97//vRg2bJgAxKhRo0QoFIq3bz9vY8eOFVarVYwdO1bcfvvt4pprrhE1NTVCCCF69eolBgwYIK655hpx1113ieuuu06kpKQIQDzzzDMdtn+k/n7+858LQKxatarTfi5evFgA4pe//GWH/b7mmmu6dZy+y1tvvSUAcd999x2yzeDBg4VCoRAOh0Ps3r1bPPPMM+LRRx8VH374oQgGg0e1vXvvvVcAIi0tTdxyyy3iN7/5jcjLyxMzZ84UgJg4cWKH9i0tLaJfv37x4/XrX/9a/PznPxdJSUlCrVaLDz74oEP7iRMnCkBcfPHFIicnR9x4443iF7/4hUhKShKAeP755zu09/v94owzzhCAGDp0qPj9738v5s6dKwwGg1CpVOLdd9+Nt3355Zfj/V9zzTXivvvuE/fdd5/461//KoQQ4oEHHhCAeOONNzrt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xc3NrdI6UVFRbN68mf379zNs2LAa95mRkcHhw4fRaDQMHz78tn0C7N+/v/zY1q1bARg6dChBQUE1Hgf8+jN85JFHaNKkSYXyrl27EhERwalTp9i9ezcTJ060Kvfz86Nv3761MhZJauhkAiRJEgDXr18HSicBV6ZFixZA6TyRzMxM/Pz8bG57w4YNTJkypcr5JHl5edUYbeViY2MBWLZsGcuWLauybnp6eo37A4iLi0MIgV6vR61W29xn2aT0Nm3a1Mo44M4/Qyj9OZ46daq87q3khGepMZEJkCRJder69es88cQT6PV6Xn31VSZOnEhoaCharRalUsn27duJjo4uf8uqJiwWC1A6eTkiIqLKuj169Khxf7f2qdVqGTNmTK20WV80Gk19D0GS7hqZAEmSBEBQUBDXrl0jNjaW9u3bVygvu7vi7OyMTqezud0NGzag1+sZNWoU//znPyuUX7lyxf5B/0bTpk0B6N27Nx988EGttWtLnwqFguXLl6NU2ra6SLNmzQC4ePFirY2l7FFa2c+qMmVltfXYTZLuVXIdIElqJFQqFVC67kxlyuap/HZtmzLLly8HoG/fvjg6/vr/ne7UblZWFgAhISEVyoQQfPnll3cevI0effRRAH744QeKi4trrd2qBAYG0rFjR/Lz88vn9djikUceAUrnLd24ccOmc2z9GW7dupWbN29WKD9x4gQnT55EqVTSr18/m8cqSfcjmQBJUiMRHBwMwLlz5yotf/HFF3F0dOS7775j1apVVmXbt29n6dKlAMyaNata7bZt2xaAdevWlU94BjCbzcydO9dqYnBNde7cmTFjxpCUlMTo0aMr3fessLCQL774otIEwV5vvPEGAFOmTGHDhg0VyoUQHDp0iO3bt5cf69SpEyNHjkSv1zNy5MjylabLmEwmfvjhB6tjd4p1nz596NGjB3q9nueee46ioqLysoyMDJ577jkAxo8fX37nSpIarXp9B02SpLvmgw8+EIDQarVi9OjRYtq0aWLatGni4sWL5XWWLl0qlEqlAESXLl3EhAkTRO/evYVCoRCAmD9/foV2U1NThaurqwBE7969xeTJk8W0adPE8uXLhRCl69l07dq1vO+hQ4eKcePGiZCQEOHk5CRmz54tANG/f3+rdu1dBygvL08MHDhQAEKlUonIyEgxbtw48fjjj4vIyMjy9YwuXLhgdV5NXoMXQoj//Oc/wtHRUQCiZcuWYujQoWLChAli8ODBws/PTwBi9uzZVudkZWWJnj17lo81KipKTJgwQQwYMED4+vpW6O9OsRZCiGvXrpVfi5+fnxg7dqwYOXKkcHd3L/+5ZmVlWbVb9hr8b38GknQ/kwmQJDUSZrNZLFy4ULRr1044OzuX/0LftWuXVb2DBw+KsWPHCn9/f+Ho6Ci8vb3F0KFDKyyAeKuff/5ZDBo0SHh5eZUnUJMmTSovz8/PF3PmzBGtW7cWzs7Ows/PTzz22GPi6NGjt/3la28CVHatX375pRgyZIho0qSJcHJyEt7e3qJ9+/ZiypQpYv369cJgMFidU9MESAghzpw5I5599lkRHh4unJ2dhYuLiwgLCxPR0dHiv//9r7h+/XqFc0pKSsSSJUtE3759haenp1CpVCI4OFgMHjxYfPjhhxXq3ynWQgiRmZkpXnvtNdG2bdvycXTu3Fm89dZboqioqEKbMgGSGiOFELXw6oUkSZIkSdI9RM4BkiRJkiSp0ZEJkCRJkiRJjY5MgCRJkiRJanRkAiRJkiRJUqMjEyBJkiRJkhodmQBJkiRJktToyARIkiRJkqRGRyZAkiRJkiQ1OjIBkiRJkiSp0ZEJkCRJkiRJjY5MgCRJkiRJanRkAiRJkiRJUqMjEyBJkiRJkhqd/wc2MNWa6uR42gAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "muts = [f\"A{i}V\" for i in range(1, 6)] # A1V ... A5V\n", + "rng = np.random.default_rng(0)\n", + "\n", + "rand_effects = sample_effects(\n", + " muts,\n", + " effect_sd={\"HD\": 1.5, \"H\": 0.5, \"L\": 0.5, \"LE\": 1.5},\n", + " epistasis_sd={\"HD\": 0.3, \"LE\": 0.3},\n", + " rng=rng,\n", + ")\n", + "\n", + "rand_df = build_titration_df(model, effector, effects=rand_effects,\n", + " observable_std=0.02, noise_sd=0.02, rng=rng)\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "for geno, sub in rand_df.groupby(\"genotype\"):\n", + " ax.semilogx(sub[\"titrant_conc\"], sub[\"observable\"], \"-\", lw=0.8,\n", + " color=\"#2b6cb0\" if \"/\" in geno else \"#dd6b20\",\n", + " alpha=0.5 if \"/\" in geno else 0.9)\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"observable (with noise)\")\n", + "ax.set_title(f\"Random library: {rand_df['genotype'].nunique()} genotypes\\n\"\n", + " \"(orange = wt/singles, blue = doubles)\")\n", + "ax.set_ylim(-0.02, 1.02)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "d6cdcf38", + "metadata": {}, + "source": [ + "From here you can run `cat_response` / `extract_epistasis` on `rand_df` just as\n", + "above, or tune `ln_K_*`, the concentrations, and the effect distributions to\n", + "explore how the observable curves — and their apparent epistasis — respond." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/tfscreen/simulate/toy_thermo/__init__.py b/src/tfscreen/simulate/toy_thermo/__init__.py new file mode 100644 index 0000000..ed51da8 --- /dev/null +++ b/src/tfscreen/simulate/toy_thermo/__init__.py @@ -0,0 +1,22 @@ +""" +Toy four-state thermodynamic TF model for teaching / interactive exploration. + +A self-contained (NumPy/SciPy/pandas only) simulator of a monomer TF whose two +conformations (H, L) bind DNA and effector respectively. Activity is fixed at 1 +(pure repressor) and effector is the sole titrant. Mutations perturb per-state +stabilities with optional in-state pairwise epistasis, and the builder emits a +long-form table ready for ``tfs-cat-response`` / ``tfs-extract-epistasis``. + +See the module docstrings in ``core`` and ``genotypes`` for the model math. +""" + +from .core import fraction_bound, solve_species # noqa: F401 +from .genotypes import ( # noqa: F401 + ThermoModel, + MutationEffects, + enumerate_genotypes, + build_titration_df, + parse_genotype, + STATES, +) +from .sampling import sample_effects # noqa: F401 diff --git a/src/tfscreen/simulate/toy_thermo/core.py b/src/tfscreen/simulate/toy_thermo/core.py new file mode 100644 index 0000000..0c67257 --- /dev/null +++ b/src/tfscreen/simulate/toy_thermo/core.py @@ -0,0 +1,81 @@ +""" +Self-contained equilibrium solver for a four-state monomer TF system. + + HD <-> H + D <-> L + D <-> LE + D + + H, L : two protein conformations in intrinsic equilibrium + D : DNA (operator); H binds D to form HD + E : effector; L binds E to form LE + observable = HD / (HD + D) (fraction of DNA bound) + +Three association constants define the wild-type system: + + K_conf = [H]/[L] (dimensionless) + K_dna = [HD]/([H][D]) (1 / concentration) + K_eff = [LE]/([L][E]) (1 / concentration) + +Given total protein, DNA, and effector, the free concentrations satisfy mass +balance. Substituting the closed-form expressions for free DNA and free +effector into protein conservation leaves a single monotonic equation in free +[L] on (0, protein_total], solved here with Brent's method. All concentrations +must share the same unit; K_dna and K_eff carry its inverse. +""" + +import numpy as np +from scipy.optimize import brentq + + +def _solve_free_L(Kc, Kd, Ke, protein_total, dna_total, effector_total): + """Solve protein conservation for free [L]. Returns a scalar.""" + KcKd = Kc * Kd + + def conservation(L): + D_free = dna_total / (1.0 + KcKd * L) + E_free = effector_total / (1.0 + Ke * L) + # [L] + [H] + [HD] + [LE] - protein_total + return L * (1.0 + Kc + KcKd * D_free + Ke * E_free) - protein_total + + # conservation(0) = -protein_total < 0; conservation(protein_total) > 0. + return brentq(conservation, 0.0, protein_total, xtol=1e-30, rtol=8.9e-16) + + +def fraction_bound(Kc, Kd, Ke, protein_total, dna_total, effector_total): + """ + Fraction of DNA bound, HD / (HD + D), for the four-state system. + + Parameters + ---------- + Kc, Kd, Ke : float + Association constants K_conf, K_dna, K_eff (see module docstring). + protein_total, dna_total : float + Total protein and DNA concentrations (same unit). + effector_total : float or array_like + Total effector concentration(s). Scalar in -> scalar out; array in -> + array out (one observable per effector concentration). + + Returns + ------- + float or numpy.ndarray + Observable HD / (HD + D) in [0, 1]. + """ + eff = np.atleast_1d(np.asarray(effector_total, dtype=float)) + theta = np.empty(eff.shape, dtype=float) + for i, E in enumerate(eff): + L = _solve_free_L(Kc, Kd, Ke, protein_total, dna_total, E) + w = Kc * Kd * L # [HD]/[D] + theta[i] = w / (1.0 + w) + return theta if np.ndim(effector_total) else float(theta[0]) + + +def solve_species(Kc, Kd, Ke, protein_total, dna_total, effector_total): + """ + Return every free/bound species concentration for one effector value. + + Handy for teaching/plotting the full occupancy breakdown. Returns a dict + with keys L, H, HD, LE, D, E. Scalar effector only. + """ + L = _solve_free_L(Kc, Kd, Ke, protein_total, dna_total, effector_total) + D = dna_total / (1.0 + Kc * Kd * L) + E = effector_total / (1.0 + Ke * L) + return {"L": L, "H": Kc * L, "HD": Kc * Kd * L * D, "LE": Ke * L * E, + "D": D, "E": E} diff --git a/src/tfscreen/simulate/toy_thermo/genotypes.py b/src/tfscreen/simulate/toy_thermo/genotypes.py new file mode 100644 index 0000000..10e4768 --- /dev/null +++ b/src/tfscreen/simulate/toy_thermo/genotypes.py @@ -0,0 +1,200 @@ +""" +Mutation / genotype layer on top of the four-state equilibrium core. + +Mutations perturb the free energy (stability) of the four protein states +HD, H, L, LE independently, in units of kT (positive = destabilizing). The +wild-type is pinned to the gauge g_L = 0, so its state energies follow from the +three wild-type association constants: + + g_L = 0 + g_H = -ln K_conf + g_HD = -ln K_conf - ln K_dna + g_LE = -ln K_eff + +A genotype's energies are the wild-type energies plus every present mutation's +per-state effect plus every present pair's per-state epistasis. The perturbed +energies are converted back to K_conf/K_dna/K_eff and handed to the core. +""" + +from itertools import combinations + +import numpy as np +import pandas as pd + +from .core import fraction_bound + +STATES = ("HD", "H", "L", "LE") + + +def _energies_from_ks(ln_K_conf, ln_K_dna, ln_K_eff): + """Wild-type state energies (g_L gauge = 0) from log association constants.""" + return {"L": 0.0, + "H": -ln_K_conf, + "HD": -ln_K_conf - ln_K_dna, + "LE": -ln_K_eff} + + +def _ks_from_energies(g): + """Association constants (Kc, Kd, Ke) from a state-energy dict.""" + ln_K_conf = -(g["H"] - g["L"]) + ln_K_dna = -(g["HD"] - g["H"]) + ln_K_eff = -(g["LE"] - g["L"]) + return np.exp(ln_K_conf), np.exp(ln_K_dna), np.exp(ln_K_eff) + + +def parse_genotype(genotype): + """Split a genotype string into its mutation names ('wt' -> []).""" + if genotype in (None, "wt", ""): + return [] + return genotype.split("/") + + +class MutationEffects: + """ + Catalog of per-mutation state effects and per-pair in-state epistasis. + + All effects are ddG values in kT keyed by state name (HD, H, L, LE); + omitted states default to 0. + """ + + def __init__(self): + self._singles = {} # name -> {state: ddG} + self._pairs = {} # frozenset({a, b}) -> {state: ddG} + + @staticmethod + def _as_state_dict(kwargs): + bad = set(kwargs) - set(STATES) + if bad: + raise ValueError(f"unknown state(s) {sorted(bad)}; valid: {STATES}") + return {s: float(kwargs.get(s, 0.0)) for s in STATES} + + def add_mutation(self, name, **ddg): + """Register a mutation's main effect, e.g. add_mutation('A', HD=2.0).""" + self._singles[name] = self._as_state_dict(ddg) + return self + + def add_epistasis(self, m1, m2, **ddg): + """Register in-state epistasis for a pair, e.g. add_epistasis('A','B', HD=1).""" + if m1 == m2: + raise ValueError("epistasis requires two distinct mutations") + self._pairs[frozenset((m1, m2))] = self._as_state_dict(ddg) + return self + + def ddg_for(self, genotype): + """Summed per-state ddG for a genotype string ('wt', 'A', 'A/B', ...).""" + muts = parse_genotype(genotype) + total = {s: 0.0 for s in STATES} + for m in muts: + if m not in self._singles: + raise KeyError(f"mutation {m!r} not in catalog") + for s in STATES: + total[s] += self._singles[m][s] + for a, b in combinations(muts, 2): + pair = self._pairs.get(frozenset((a, b))) + if pair: + for s in STATES: + total[s] += pair[s] + return total + + @property + def mutations(self): + return list(self._singles) + + +class ThermoModel: + """ + Wild-type four-state system plus a mutation layer. + + Parameters + ---------- + ln_K_conf, ln_K_dna, ln_K_eff : float + Natural-log wild-type association constants. + protein_total, dna_total : float + Total protein and DNA (same concentration unit). + """ + + def __init__(self, ln_K_conf, ln_K_dna, ln_K_eff, + protein_total, dna_total): + self.wt_energies = _energies_from_ks(ln_K_conf, ln_K_dna, ln_K_eff) + self.protein_total = float(protein_total) + self.dna_total = float(dna_total) + + def genotype_ks(self, genotype="wt", effects=None, ddg=None): + """ + Association constants (K_conf, K_dna, K_eff) for a genotype. + + Mutations are supplied the same way as in ``observable``: either a + direct ``ddg`` dict or a ``MutationEffects`` catalog plus a ``genotype`` + string. Useful for feeding ``core.solve_species`` to inspect the full + occupancy breakdown. + """ + g = dict(self.wt_energies) + if ddg is not None and effects is not None: + raise ValueError("pass either ddg= or effects=, not both") + if ddg is not None: + delta = MutationEffects._as_state_dict(ddg) + elif effects is not None: + delta = effects.ddg_for(genotype) + else: + delta = {s: 0.0 for s in STATES} + for s in STATES: + g[s] += delta[s] + return _ks_from_energies(g) + + def observable(self, effector_total, genotype="wt", effects=None, ddg=None): + """ + Observable HD/(HD+D) over effector concentration(s). + + Provide mutations either as a direct ``ddg={'HD': ..., ...}`` dict (one + genotype, manual) or as a ``MutationEffects`` catalog plus a + ``genotype`` string. + """ + Kc, Kd, Ke = self.genotype_ks(genotype, effects, ddg) + return fraction_bound(Kc, Kd, Ke, self.protein_total, + self.dna_total, effector_total) + + +def enumerate_genotypes(mutations, order=2): + """['wt', singles..., pairs...] up to the requested combination order.""" + genos = ["wt"] + for k in range(1, order + 1): + genos += ["/".join(c) for c in combinations(mutations, k)] + return genos + + +def build_titration_df(model, effector_conc, genotypes=None, effects=None, + titrant_name="effector", observable_std=None, + noise_sd=None, rng=None): + """ + Long-form titration table for tfs-cat-response / tfs-extract-epistasis. + + Columns: genotype, titrant_name, titrant_conc, observable + (+ observable_std if requested). ``noise_sd`` adds Gaussian scatter to the + observable (drawn from ``rng``); ``observable_std`` is the *reported* error + written to every row (independent of the noise actually applied). + + Notes + ----- + ``cat_response`` accepts any genotype labels, but ``extract_epistasis`` + parses them through ``standardize_genotypes`` and requires the ``XsiteY`` + mutation convention (e.g. ``A1V``, ``A2V`` -> double ``A1V/A2V``). Name + mutations accordingly if you plan to run the epistasis extractor. + """ + if genotypes is None: + genotypes = (["wt"] if effects is None + else enumerate_genotypes(effects.mutations, order=2)) + conc = np.asarray(effector_conc, dtype=float) + if noise_sd is not None and rng is None: + rng = np.random.default_rng() + + rows = [] + for geno in genotypes: + y = np.atleast_1d(model.observable(conc, genotype=geno, effects=effects)) + if noise_sd: + y = np.clip(y + rng.normal(0.0, noise_sd, size=y.shape), 0.0, 1.0) + block = pd.DataFrame({"genotype": geno, "titrant_name": titrant_name, + "titrant_conc": conc, "observable": y}) + if observable_std is not None: + block["observable_std"] = observable_std + rows.append(block) + return pd.concat(rows, ignore_index=True) diff --git a/src/tfscreen/simulate/toy_thermo/sampling.py b/src/tfscreen/simulate/toy_thermo/sampling.py new file mode 100644 index 0000000..bd92c36 --- /dev/null +++ b/src/tfscreen/simulate/toy_thermo/sampling.py @@ -0,0 +1,57 @@ +""" +Draw random mutation catalogs from per-state Normal distributions. + +Instead of specifying every ddG by hand, give a mean/sd per state for main +effects (and optionally epistasis); each mutation's per-state effect is an +independent Normal draw. Produces a MutationEffects for the genotype/DataFrame +machinery in genotypes.py. +""" + +from itertools import combinations + +import numpy as np + +from .genotypes import MutationEffects, STATES + + +def _draw(rng, spec): + """spec: {state: sd} or {state: (mean, sd)} -> {state: value}.""" + out = {} + for s in STATES: + v = spec.get(s, 0.0) if spec else 0.0 + mean, sd = (0.0, float(v)) if np.isscalar(v) else (float(v[0]), float(v[1])) + out[s] = rng.normal(mean, sd) if sd > 0 else mean + return out + + +def sample_effects(mutations, effect_sd, epistasis_sd=None, + pairs=None, rng=None): + """ + Build a random MutationEffects. + + Parameters + ---------- + mutations : sequence of str + Mutation names to draw. + effect_sd : dict + Per-state main-effect spec. Each value is an sd (mean 0) or a + (mean, sd) tuple, e.g. {'HD': 1.0, 'LE': (0.0, 2.0)}. + epistasis_sd : dict, optional + Same form, for per-pair in-state epistasis. Omit for no epistasis. + pairs : iterable of (str, str), optional + Which pairs get epistasis terms (default: all pairs of ``mutations``). + rng : numpy.random.Generator, optional + + Returns + ------- + MutationEffects + """ + rng = rng if rng is not None else np.random.default_rng() + eff = MutationEffects() + for m in mutations: + eff.add_mutation(m, **_draw(rng, effect_sd)) + if epistasis_sd: + pairs = combinations(mutations, 2) if pairs is None else pairs + for a, b in pairs: + eff.add_epistasis(a, b, **_draw(rng, epistasis_sd)) + return eff diff --git a/tests/tfscreen/simulate/toy_thermo/test_core.py b/tests/tfscreen/simulate/toy_thermo/test_core.py new file mode 100644 index 0000000..65b2e9c --- /dev/null +++ b/tests/tfscreen/simulate/toy_thermo/test_core.py @@ -0,0 +1,71 @@ +"""Tests for tfscreen.simulate.toy_thermo.core.""" + +import numpy as np +import pytest + +from tfscreen.simulate.toy_thermo.core import ( + fraction_bound, solve_species, _solve_free_L, +) + +# A representative wild-type system: strong DNA binding, moderate effector. +KC, KD, KE = np.exp(0.0), np.exp(18.0), np.exp(14.0) +P_TOT, D_TOT = 1e-6, 1e-9 + + +def test_mass_balance_closes(): + """Solved species satisfy all three conservation laws exactly.""" + for E in np.logspace(-8, -2, 7): + s = solve_species(KC, KD, KE, P_TOT, D_TOT, E) + assert s["L"] + s["H"] + s["HD"] + s["LE"] == pytest.approx(P_TOT, rel=1e-9) + assert s["D"] + s["HD"] == pytest.approx(D_TOT, rel=1e-9) + assert s["E"] + s["LE"] == pytest.approx(E, rel=1e-9) + + +def test_observable_matches_species(): + """fraction_bound equals HD/(HD+D) from the explicit species solve.""" + for E in np.logspace(-8, -2, 7): + s = solve_species(KC, KD, KE, P_TOT, D_TOT, E) + expected = s["HD"] / (s["HD"] + s["D"]) + assert fraction_bound(KC, KD, KE, P_TOT, D_TOT, E) == pytest.approx(expected, rel=1e-9) + + +def test_observable_bounds(): + """Observable always lies in [0, 1].""" + y = fraction_bound(KC, KD, KE, P_TOT, D_TOT, np.logspace(-9, -1, 25)) + assert np.all(y >= 0.0) and np.all(y <= 1.0) + + +def test_effector_induces_derepression(): + """More effector sequesters protein as LE -> observable decreases.""" + grid = np.logspace(-8, -2, 13) + y = fraction_bound(KC, KD, KE, P_TOT, D_TOT, grid) + assert np.all(np.diff(y) <= 1e-12) # monotonically non-increasing + assert y[0] > y[-1] # and actually drops + + +def test_scalar_in_scalar_out(): + """Scalar effector returns a Python float; array returns an ndarray.""" + scalar = fraction_bound(KC, KD, KE, P_TOT, D_TOT, 1e-5) + assert isinstance(scalar, float) + arr = fraction_bound(KC, KD, KE, P_TOT, D_TOT, np.array([1e-6, 1e-5])) + assert isinstance(arr, np.ndarray) and arr.shape == (2,) + + +def test_zero_effector_is_valid(): + """Zero effector -> no LE, fully DNA-limited repression, no NaN.""" + s = solve_species(KC, KD, KE, P_TOT, D_TOT, 0.0) + assert s["LE"] == 0.0 + y = fraction_bound(KC, KD, KE, P_TOT, D_TOT, 0.0) + assert np.isfinite(y) and 0.0 <= y <= 1.0 + + +def test_free_L_within_protein_total(): + """Free [L] is bracketed by (0, protein_total].""" + L = _solve_free_L(KC, KD, KE, P_TOT, D_TOT, 1e-5) + assert 0.0 < L <= P_TOT + + +def test_weak_dna_binding_gives_low_occupancy(): + """Dropping K_dna far below the DNA scale leaves DNA mostly unbound.""" + y = fraction_bound(KC, np.exp(4.0), KE, P_TOT, D_TOT, 0.0) + assert y < 0.5 diff --git a/tests/tfscreen/simulate/toy_thermo/test_genotypes.py b/tests/tfscreen/simulate/toy_thermo/test_genotypes.py new file mode 100644 index 0000000..9c3f6f0 --- /dev/null +++ b/tests/tfscreen/simulate/toy_thermo/test_genotypes.py @@ -0,0 +1,177 @@ +"""Tests for tfscreen.simulate.toy_thermo.genotypes.""" + +import numpy as np +import pytest + +from tfscreen.simulate.toy_thermo.genotypes import ( + ThermoModel, MutationEffects, STATES, + enumerate_genotypes, build_titration_df, parse_genotype, + _energies_from_ks, _ks_from_energies, +) + +LN_KS = dict(ln_K_conf=0.5, ln_K_dna=18.0, ln_K_eff=14.0) +GRID = np.logspace(-8, -2, 9) + + +def _model(): + return ThermoModel(protein_total=1e-6, dna_total=1e-9, **LN_KS) + + +# --- energy <-> K bookkeeping ------------------------------------------------ + +def test_energy_k_roundtrip(): + """Energies derived from Ks reproduce those Ks.""" + g = _energies_from_ks(0.5, 18.0, 14.0) + Kc, Kd, Ke = _ks_from_energies(g) + assert np.log(Kc) == pytest.approx(0.5) + assert np.log(Kd) == pytest.approx(18.0) + assert np.log(Ke) == pytest.approx(14.0) + + +def test_genotype_ks_wt_matches_inputs(): + """genotype_ks('wt') returns exactly the configured wild-type constants.""" + Kc, Kd, Ke = _model().genotype_ks("wt") + assert np.log(Kc) == pytest.approx(LN_KS["ln_K_conf"]) + assert np.log(Kd) == pytest.approx(LN_KS["ln_K_dna"]) + assert np.log(Ke) == pytest.approx(LN_KS["ln_K_eff"]) + + +def test_genotype_ks_reflects_mutation(): + """Destabilizing HD lowers K_dna; other constants unchanged.""" + model = _model() + Kc0, Kd0, Ke0 = model.genotype_ks("wt") + Kc1, Kd1, Ke1 = model.genotype_ks(ddg={"HD": 2.0}) + assert np.log(Kd1) == pytest.approx(np.log(Kd0) - 2.0) + assert Kc1 == pytest.approx(Kc0) + assert Ke1 == pytest.approx(Ke0) + + +def test_global_energy_shift_is_invariant(): + """Shifting all four state energies equally leaves the observable unchanged.""" + model = _model() + wt = model.observable(GRID, genotype="wt") + shift = {s: 3.0 for s in STATES} # same ddG on every state + shifted = model.observable(GRID, ddg=shift) + assert np.allclose(wt, shifted, rtol=1e-9, atol=1e-12) + + +# --- MutationEffects catalog ------------------------------------------------- + +def test_parse_genotype(): + assert parse_genotype("wt") == [] + assert parse_genotype("A") == ["A"] + assert parse_genotype("A/B") == ["A", "B"] + + +def test_ddg_sums_singles_and_epistasis(): + eff = (MutationEffects() + .add_mutation("A", HD=2.0, H=1.0) + .add_mutation("B", HD=-1.0, LE=3.0) + .add_epistasis("A", "B", HD=0.5)) + d = eff.ddg_for("A/B") + assert d["HD"] == pytest.approx(2.0 - 1.0 + 0.5) + assert d["H"] == pytest.approx(1.0) + assert d["LE"] == pytest.approx(3.0) + assert d["L"] == pytest.approx(0.0) + + +def test_single_mutant_has_no_epistasis_term(): + eff = (MutationEffects() + .add_mutation("A", HD=2.0) + .add_mutation("B", HD=-1.0) + .add_epistasis("A", "B", HD=5.0)) + assert eff.ddg_for("A")["HD"] == pytest.approx(2.0) + + +def test_unknown_state_rejected(): + with pytest.raises(ValueError, match="unknown state"): + MutationEffects().add_mutation("A", ZZ=1.0) + + +def test_unknown_mutation_rejected(): + eff = MutationEffects().add_mutation("A", HD=1.0) + with pytest.raises(KeyError): + eff.ddg_for("A/B") + + +def test_self_epistasis_rejected(): + with pytest.raises(ValueError, match="distinct"): + MutationEffects().add_epistasis("A", "A", HD=1.0) + + +def test_ddg_and_effects_mutually_exclusive(): + model = _model() + eff = MutationEffects().add_mutation("A", HD=1.0) + with pytest.raises(ValueError, match="either"): + model.observable(GRID, genotype="A", effects=eff, ddg={"HD": 1.0}) + + +# --- observable epistasis emerges from a nonlinear map ----------------------- + +def test_observable_epistasis_without_thermo_epistasis(): + """Zero in-state epistasis still yields nonzero observable epistasis.""" + model = _model() + eff = (MutationEffects() + .add_mutation("A", HD=3.0) + .add_mutation("B", LE=-3.0) + .add_epistasis("A", "B")) # all-zero thermodynamic epistasis + E = 1e-5 + wt = model.observable(E, genotype="wt", effects=eff) + a = model.observable(E, genotype="A", effects=eff) + b = model.observable(E, genotype="B", effects=eff) + ab = model.observable(E, genotype="A/B", effects=eff) + obs_epistasis = (ab - a) - (b - wt) + assert abs(obs_epistasis) > 1e-3 + + +# --- enumeration + DataFrame builder ----------------------------------------- + +def test_enumerate_genotypes(): + genos = enumerate_genotypes(["A", "B", "C"], order=2) + assert genos == ["wt", "A", "B", "C", "A/B", "A/C", "B/C"] + + +def test_build_titration_df_shape_and_columns(): + model = _model() + eff = (MutationEffects() + .add_mutation("A", HD=2.0) + .add_mutation("B", LE=-2.0)) + df = build_titration_df(model, GRID, effects=eff, observable_std=0.02) + # wt + 2 singles + 1 double = 4 genotypes, each over the grid + assert set(df["genotype"]) == {"wt", "A", "B", "A/B"} + assert len(df) == 4 * len(GRID) + assert list(df.columns) == ["genotype", "titrant_name", "titrant_conc", + "observable", "observable_std"] + assert (df["observable_std"] == 0.02).all() + assert (df["titrant_name"] == "effector").all() + + +def test_build_titration_df_no_std_column_by_default(): + model = _model() + df = build_titration_df(model, GRID, genotypes=["wt"]) + assert "observable_std" not in df.columns + assert np.all((df["observable"] >= 0) & (df["observable"] <= 1)) + + +def test_build_titration_df_noise_is_reproducible(): + model = _model() + eff = MutationEffects().add_mutation("A", HD=2.0) + kw = dict(genotypes=["wt", "A"], noise_sd=0.03) + a = build_titration_df(model, GRID, effects=eff, + rng=np.random.default_rng(0), **kw) + b = build_titration_df(model, GRID, effects=eff, + rng=np.random.default_rng(0), **kw) + c = build_titration_df(model, GRID, effects=eff, + rng=np.random.default_rng(1), **kw) + assert np.allclose(a["observable"], b["observable"]) + assert not np.allclose(a["observable"], c["observable"]) + assert np.all((a["observable"] >= 0) & (a["observable"] <= 1)) + + +def test_manual_ddg_matches_effects_catalog(): + """Passing ddg= directly equals routing the same deltas through a catalog.""" + model = _model() + eff = MutationEffects().add_mutation("A", HD=2.5, LE=-1.0) + via_catalog = model.observable(GRID, genotype="A", effects=eff) + via_ddg = model.observable(GRID, ddg={"HD": 2.5, "LE": -1.0}) + assert np.allclose(via_catalog, via_ddg) diff --git a/tests/tfscreen/simulate/toy_thermo/test_integration.py b/tests/tfscreen/simulate/toy_thermo/test_integration.py new file mode 100644 index 0000000..1108adb --- /dev/null +++ b/tests/tfscreen/simulate/toy_thermo/test_integration.py @@ -0,0 +1,42 @@ +"""End-to-end: toy_thermo output feeds cat_response and extract_epistasis.""" + +import numpy as np + +from tfscreen.simulate.toy_thermo import ( + ThermoModel, sample_effects, build_titration_df, +) +from tfscreen.analysis.cat_response.cat_response import cat_response +from tfscreen.analysis.extract_epistasis import extract_epistasis + + +def _df(): + model = ThermoModel(ln_K_conf=0.0, ln_K_dna=18.0, ln_K_eff=14.0, + protein_total=1e-6, dna_total=1e-9) + grid = np.logspace(-8, -2, 13) + # XsiteY names so extract_epistasis can parse the genotypes + eff = sample_effects(["A1V", "A2V", "A3V"], + effect_sd={"HD": 1.5, "H": 0.5, "L": 0.5, "LE": 1.5}, + epistasis_sd={"HD": 0.3, "LE": 0.3}, + rng=np.random.default_rng(1)) + return build_titration_df(model, grid, effects=eff, observable_std=0.02) + + +def test_cat_response_consumes_titration_df(): + df = _df() + res, pred, assess, delta = cat_response( + df, x_obs="titrant_conc", y_obs="observable", + y_std="observable_std", progress=False) + # one row per genotype (wt + 3 singles + 3 doubles) + assert len(res) == 7 + assert "best_model" in res.columns + assert "response_class" in res.columns + + +def test_extract_epistasis_consumes_titration_df(): + df = _df() + ep = extract_epistasis(df, y_obs="observable", y_std="observable_std", + group_by="titrant_conc") + assert {"genotype", "titrant_conc", "ep_obs", "ep_std"} <= set(ep.columns) + # 3 double mutants x 13 concentrations + assert len(ep) == 3 * 13 + assert np.all(np.isfinite(ep["ep_obs"])) diff --git a/tests/tfscreen/simulate/toy_thermo/test_sampling.py b/tests/tfscreen/simulate/toy_thermo/test_sampling.py new file mode 100644 index 0000000..9a22779 --- /dev/null +++ b/tests/tfscreen/simulate/toy_thermo/test_sampling.py @@ -0,0 +1,73 @@ +"""Tests for tfscreen.simulate.toy_thermo.sampling.""" + +import numpy as np +import pytest + +from tfscreen.simulate.toy_thermo.sampling import sample_effects, _draw +from tfscreen.simulate.toy_thermo.genotypes import MutationEffects, STATES + + +def test_draw_sd_only_and_mean_sd(): + rng = np.random.default_rng(0) + out = _draw(rng, {"HD": 1.0, "LE": (5.0, 0.0)}) + assert set(out) == set(STATES) + assert out["LE"] == 5.0 # sd == 0 -> exactly the mean + assert out["H"] == 0.0 # unspecified -> 0 + assert out["L"] == 0.0 + + +def test_draw_zero_sd_is_deterministic(): + rng = np.random.default_rng(0) + out = _draw(rng, {"HD": 0.0}) + assert out["HD"] == 0.0 + + +def test_sample_effects_returns_catalog_with_all_mutations(): + eff = sample_effects(["A", "B", "C"], + effect_sd={"HD": 1.0, "LE": 1.0}, + rng=np.random.default_rng(0)) + assert isinstance(eff, MutationEffects) + assert set(eff.mutations) == {"A", "B", "C"} + + +def test_sample_effects_reproducible(): + kw = dict(mutations=["A", "B"], + effect_sd={"HD": 1.0, "H": 0.5, "L": 0.5, "LE": 1.0}, + epistasis_sd={"HD": 0.3}) + a = sample_effects(rng=np.random.default_rng(42), **kw) + b = sample_effects(rng=np.random.default_rng(42), **kw) + c = sample_effects(rng=np.random.default_rng(7), **kw) + assert a.ddg_for("A/B") == b.ddg_for("A/B") + assert a.ddg_for("A/B") != c.ddg_for("A/B") + + +def test_no_epistasis_by_default(): + eff = sample_effects(["A", "B"], effect_sd={"HD": 1.0}, + rng=np.random.default_rng(0)) + # With no epistasis_sd, A/B ddG is exactly the sum of the two singles. + d = eff.ddg_for("A/B") + da, db = eff.ddg_for("A"), eff.ddg_for("B") + for s in STATES: + assert d[s] == pytest.approx(da[s] + db[s]) + + +def test_epistasis_only_on_requested_pairs(): + eff = sample_effects(["A", "B", "C"], effect_sd={"HD": 1.0}, + epistasis_sd={"HD": 0.5}, pairs=[("A", "B")], + rng=np.random.default_rng(0)) + # A/B has an epistasis term; A/C does not (sum of singles only). + ab = eff.ddg_for("A/B")["HD"] + a, b = eff.ddg_for("A")["HD"], eff.ddg_for("B")["HD"] + assert ab != pytest.approx(a + b) + ac = eff.ddg_for("A/C")["HD"] + a, c = eff.ddg_for("A")["HD"], eff.ddg_for("C")["HD"] + assert ac == pytest.approx(a + c) + + +def test_mean_shift_biases_draws(): + """A large positive mean pushes draws positive.""" + eff = sample_effects([f"m{i}" for i in range(200)], + effect_sd={"HD": (5.0, 0.1)}, + rng=np.random.default_rng(0)) + vals = [eff.ddg_for(m)["HD"] for m in eff.mutations] + assert np.mean(vals) == pytest.approx(5.0, abs=0.1) From 18eedd092c7f983be036686663355930c1b23187 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Sun, 19 Jul 2026 23:26:29 -0400 Subject: [PATCH 15/26] added logit scale to epistasis --- CLAUDE.md | 2 +- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- examples/toy_thermo/toy_thermo.ipynb | 250 +++++++++++++----- reports/junit/junit.xml | 2 +- src/tfscreen/analysis/extract_epistasis.py | 55 +++- .../analysis/scripts/extract_epistasis_cli.py | 14 +- .../analysis/test_extract_epistasis.py | 88 +++++- 8 files changed, 324 insertions(+), 91 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 72e2c91..f57ffbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ tfs-extract-params # Extract parameters from checkpoint tfs-predict-growth # Predict growth from fitted model tfs-predict-theta # Predict operator occupancy tfs-cat-response # Fit categorical response curves -tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table +tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table (--scale add|mult|logit) tfs-compare-theta # Grade per-genotype theta stability across N estimate runs (seeds / k-fold dropouts) tfs-diagnose-nan # Diagnose NaN issues in inference tfs-simulate # Simulate a full experiment diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 3d2b8fa..6af4af5 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.19%coverage96.19% \ No newline at end of file +coverage: 95.94%coverage95.94% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index c644be3..6222380 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4186tests4186 \ No newline at end of file +tests: 4220tests4220 \ No newline at end of file diff --git a/examples/toy_thermo/toy_thermo.ipynb b/examples/toy_thermo/toy_thermo.ipynb index 1bdda25..2022b45 100644 --- a/examples/toy_thermo/toy_thermo.ipynb +++ b/examples/toy_thermo/toy_thermo.ipynb @@ -34,14 +34,7 @@ "cell_type": "code", "execution_count": 1, "id": "3b8b8dab", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:48.109508Z", - "iopub.status.busy": "2026-07-20T02:46:48.109402Z", - "iopub.status.idle": "2026-07-20T02:46:50.721588Z", - "shell.execute_reply": "2026-07-20T02:46:50.721040Z" - } - }, + "metadata": {}, "outputs": [ { "name": "stderr", @@ -85,14 +78,7 @@ "cell_type": "code", "execution_count": 2, "id": "53d83c46", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:50.724000Z", - "iopub.status.busy": "2026-07-20T02:46:50.723712Z", - "iopub.status.idle": "2026-07-20T02:46:50.982826Z", - "shell.execute_reply": "2026-07-20T02:46:50.982193Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -133,14 +119,7 @@ "cell_type": "code", "execution_count": 3, "id": "91398efc", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:50.984509Z", - "iopub.status.busy": "2026-07-20T02:46:50.984396Z", - "iopub.status.idle": "2026-07-20T02:46:51.177712Z", - "shell.execute_reply": "2026-07-20T02:46:51.177083Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -193,14 +172,7 @@ "cell_type": "code", "execution_count": 4, "id": "287d1795", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:51.179507Z", - "iopub.status.busy": "2026-07-20T02:46:51.179370Z", - "iopub.status.idle": "2026-07-20T02:46:51.373684Z", - "shell.execute_reply": "2026-07-20T02:46:51.373031Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -252,14 +224,7 @@ "cell_type": "code", "execution_count": 5, "id": "fcd3e37b", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:51.376026Z", - "iopub.status.busy": "2026-07-20T02:46:51.375881Z", - "iopub.status.idle": "2026-07-20T02:46:51.389079Z", - "shell.execute_reply": "2026-07-20T02:46:51.388571Z" - } - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -371,14 +336,7 @@ "cell_type": "code", "execution_count": 6, "id": "a61fc694", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:51.391032Z", - "iopub.status.busy": "2026-07-20T02:46:51.390885Z", - "iopub.status.idle": "2026-07-20T02:46:51.578349Z", - "shell.execute_reply": "2026-07-20T02:46:51.577823Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -419,14 +377,7 @@ "cell_type": "code", "execution_count": 7, "id": "3e0d9f14", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:51.580030Z", - "iopub.status.busy": "2026-07-20T02:46:51.579894Z", - "iopub.status.idle": "2026-07-20T02:46:51.664816Z", - "shell.execute_reply": "2026-07-20T02:46:51.664125Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -530,14 +481,7 @@ "cell_type": "code", "execution_count": 8, "id": "f7b22c30", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:51.667210Z", - "iopub.status.busy": "2026-07-20T02:46:51.667051Z", - "iopub.status.idle": "2026-07-20T02:46:51.879271Z", - "shell.execute_reply": "2026-07-20T02:46:51.878756Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -595,14 +539,7 @@ "cell_type": "code", "execution_count": 9, "id": "04fe6860", - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-20T02:46:51.881040Z", - "iopub.status.busy": "2026-07-20T02:46:51.880895Z", - "iopub.status.idle": "2026-07-20T02:46:52.186706Z", - "shell.execute_reply": "2026-07-20T02:46:52.186109Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -651,11 +588,178 @@ "above, or tune `ln_K_*`, the concentrations, and the effect distributions to\n", "explore how the observable curves — and their apparent epistasis — respond." ] + }, + { + "cell_type": "markdown", + "id": "0fb608ca", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 8. Ensemble vs. within-state epistasis, on the logit scale\n", + "\n", + "The observable stacks two nonlinearities, and separating them is the whole game\n", + "when you try to read epistasis curves:\n", + "\n", + "1. **DNA-binding saturation**, `theta = W/(1+W)` — a pure monotone squashing\n", + " shared by *every* mutation. It is what produced the sign-flipping,\n", + " diminishing-returns epistasis on the raw scale, and the `logit` link removes it\n", + " completely.\n", + "2. **Free-protein reweighting**, the `ln(exp(-g_L) + exp(-g_H) + [E]*exp(-g_LE))`\n", + " log-sum-exp — the genuinely allosteric part, which `logit` does *not* remove.\n", + "\n", + "In the protein-excess / trace-operator limit (the defaults here),\n", + "\n", + "$$\\operatorname{logit}\\theta \\;\\approx\\; c \\;-\\; g_{HD} \\;-\\; \\ln\\!\\big(e^{-g_L} + e^{-g_H} + [E]\\,e^{-g_{LE}}\\big).$$\n", + "\n", + "`logit theta` is **linear in `g_HD`** and convex through the log-sum-exp in the\n", + "other states. To second order the additive (no-interaction) epistasis is a\n", + "**population covariance**, `eps_logit ≈ -Cov_p(delta_A, delta_B)`, so it is nonzero\n", + "only where the free-protein ensemble is *mixed*. A genuine within-state\n", + "interaction `e_AB` instead adds a term of a *different form*: a flat `-e` if it\n", + "sits on `g_HD`, or an occupancy `p_k([E])*e` if it sits on a free state. The two\n", + "figures below make that contrast concrete." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "21596f1e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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nAdMTEw0ain6+PigefPm+Y5jZ2eHbdu24Z9//sGAAQNQo0YNpKenIyUlBe7u7ujSpQtmzZol1wpSK8r1lqF8fHwQEhKC3r17o1KlSoiJiZH3GXVtIirfJKE+oxARUZmUlJQEZ2dnZGRk4MCBA3neOCAiKg3e3t64desWli5dKr+aUl689tpr+PXXXzFs2DC5wUoiIkPwiTQRURn3zTffICMjA05OThqt1BIRlbY7d+7g1q1b8PX1xdChQ40djl6uX78uN/qnfg+diMhQTKSJiIwsMTERr776Knbs2KHRCEpERAQ+/vhjufGdsWPHwsLCwjhBEhEB8qenpk2bVqTXgUrKF198gUWLFuHOnTtyA1jJycn47bff0L59e6SlpaF27dro06ePcQMlonKPVbuJiIwsLi5OoyVZ9bt/uVu+79evH9avX29wC/FERM+DPn36YOvWrQBy2gaxtbVFXFycnFS7u7tjx44dcovKRESGYiJNRGRkWVlZWLJkCXbt2oWLFy/i0aNHSE1NhYuLC5o0aYLBgwejX79+kCTJ2KESEZVp+/fvx2+//YYjR47g/v37iI2NhbW1NXx9fdGrVy+MGTMGTk5Oxg6TiCoAJtJEREREREREeuA70kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREREREREpAcm0kRERERERER6YCJNREUydOhQSJKE8PDwQg2/b98+SJKEKVOmaHT39PSEp6enXvNu164dJEnSaxxjSUlJgbu7O0aMGGHsUMqt4tjeu3fvhiRJ+Oeff4opKiKi8oHl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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from scipy.special import logit\n", + "\n", + "\n", + "def epistasis_curve(model, E, ddg_A, ddg_B, epi=None, scale=\"logit\"):\n", + " \"\"\"Additive epistasis (11-10)-(01-00) vs. effector, on the raw or logit scale.\n", + "\n", + " ddg_A, ddg_B : per-state ddG dicts for the two single mutants (additive).\n", + " epi : per-state within-state interaction applied ONLY to the double.\n", + " \"\"\"\n", + " epi = epi or {}\n", + " states = set(ddg_A) | set(ddg_B) | set(epi)\n", + " ddg_AB = {s: ddg_A.get(s, 0.0) + ddg_B.get(s, 0.0) + epi.get(s, 0.0) for s in states}\n", + " y = {g: model.observable(E, ddg=d)\n", + " for g, d in [(\"00\", {}), (\"10\", ddg_A), (\"01\", ddg_B), (\"11\", ddg_AB)]}\n", + " link = ((lambda v: logit(np.clip(v, 1e-12, 1 - 1e-12))) if scale == \"logit\"\n", + " else (lambda v: v))\n", + " return (link(y[\"11\"]) - link(y[\"10\"])) - (link(y[\"01\"]) - link(y[\"00\"]))\n", + "\n", + "\n", + "# Same additive g_HD mutations, viewed on each scale.\n", + "ddg = {\"HD\": 2.0}\n", + "fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n", + "for ax, scale, color in zip(axes, [\"raw\", \"logit\"], [\"#2b6cb0\", \"#2f855a\"]):\n", + " ax.axhline(0, color=\"0.6\", lw=1)\n", + " ax.semilogx(effector, epistasis_curve(model, effector, ddg, ddg, scale=scale),\n", + " \"o-\", ms=4, color=color)\n", + " ax.set_xlabel(\"total effector\")\n", + " ax.set_title(f\"g_HD x g_HD, additive ({scale} scale)\")\n", + "axes[0].set_ylabel(\"epistasis\")\n", + "fig.suptitle(\"The raw-scale swing is a saturation artifact; logit removes it\")\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "77e62a5d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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OHGiyACuOC+99BJr167lnnvuoVevXvz222+89dZb5Ofn85///KdMZXz55Zc89dRTBAcHc++99+Ln58eFCxfYtm0bv/76K4MGDaJ58+aMGzeO//3vfzRr1oyBAwfaHDcUfEBOmzaNO++8k3bt2qHX69m9ezdffvklf/31F7t27bK+H55//nlmz57N3r17GTdunLU71JX3EGzdupVevXqRlZXFPffcQ506dYiNjeWHH35gxYoVbN68mZo1a5Z6fMnJyXTp0oUDBw7QsWNHxo4dS3p6OkuWLKFbt2788ssvNsdTqDzvl7i4ONq2bUtWVhZ9+/ZlyJAhZGVlcezYMb744gs++ugjdDodkydPZvbs2Zw6dYrJkydbt7/y9fL000/TrFkz7rrrLgICAjh37hy//fYbd911F4sXL2bAgAHX9FwWR1VVevXqxdatW+nYsSO9e/dGo9Fw6tQpli5dyrBhwwgPDy/1fD/33HNMnz6d6tWr89hjj1nP3ciRI9m9ezf/+9//7LbZvn07H3zwAd26deOJJ55g9+7d/Pbbb/z777/s378fZ2fnEvdZlvMIBf3u27dvT6NGjRg1ahQnTpywvg4OHTpEtWrVrHkr4npXko8//pjx48fj6+vLQw89hJubG0uXLmX8+PGsX7+exYsX2/TBfvPNN3nnnXeoVq0aY8aMQa/X8/PPP3P48OEy7a9r167ExsYyZ84coqKibILr0rojTpw4kQ8++IBatWrx0EMP4eHhwblz59iwYQN///23TVn/+9//eOGFF3BxceHee+8lLCzMmnfhwoV06tQJuLbrRUmWLl3K4MGD0Wg0DBgwgNDQUA4ePMj//d//8ddff7F161Z8fHzKdI5ueqoQDoiJiVEBtVatWurkyZOL/FuxYoXNNoAKqE899ZRqNput67/99lsVUJ944gnruqVLl6qA+vzzz9vtOyMjQ83NzbUuT5o0SQXUN954Q7VYLNb16enpauvWrVUnJyf13Llz1vXDhw9XAbVu3bpqQkKCdf3WrVtVQPX29lY7deqkZmZmWtMWLFigAuqzzz5rU5eoqCgVUOvVq6empqZa16empqr16tVTFUVRt2/fbl0fHR2tAurkyZNtyunQoYOq1WrVP//802b9kSNHVA8PD7VJkyZ25+FqFotFDQsLU318fGzOj6qq6r///qsC6v33329dd99996mAumfPHruyLl68WOr+StKyZUvVw8NDNRqNqqqqqtlsVr29vdU777xTBdQZM2ZY877xxhsqoK5bt866btasWSqgzpo1y6ZcQI2Kiipyn4Xb6HQ6dcOGDdb1JpNJ7dq1qwqomzdvLrW8wtdHZGSkGhcXZ12fmJioent7qx4eHmpeXl6Zz4OTk5MaHx9vl3blOS58Pw0fPrzIcuLj49WMjAy79XPmzFEBderUqUUeQ0xMjN02+fn5akREhOrh4aHu2rXLJm39+vWqVqtV77nnnjIcnao+9NBDKqB+8803dvUNDQ1VAwIC1JycHOv6ini/fPbZZyqgfvrpp3b1SUpKslku3F9xTp48abcuLi5ODQkJUevUqWOzvqzPparav6727dunAurAgQPtts3NzS3yub3a2rVrVUBt0KCBzblLTk5W69ata/ceKjx3gPrTTz/ZlDVs2DAVUOfPn1/qflW15PNY+NoF1Pfee88m7fXXX1cB9d1337VZXxHXO1VV1cmTJ6uAGh0dbV13/PhxVafTqYGBgerp06et63Nzc9VOnTqpgPr999/b7FOr1arVq1e3eW7T09PVhg0blniNuPL9Vdy1vTS+vr5qSEiImpWVZZd25et5z549qkajUUNCQuze1xaLxeYz7lqvF0Wdx4sXL6qenp5q9erV1djYWJv88+fPVwH1mWeeuZZDvalJNwxRLidOnGDKlClF/v355592+d3c3Hj//ffRaC6/9IYPH45Op2P79u12+V1cXOzWubu7YzAYgIKfsL788ktq1apl/fmrkIeHB2+++Sb5+fksXrzYrpzXXnuNgIAA63Lbtm2pWbMmqamp/Oc//8HNzc2aNmjQIPR6fbE/Kb7xxhs239S9vLx4/fXXUVXV+pN4cXbv3s2mTZsYPnw4vXr1skmrW7cuY8aMsbYClURRFB5++GFSUlJYtmyZTdrcuXMBirwZs6hz7OfnV+K+StOtWzcyMjLYsWMHUHCMqamp1p/9V69ebc0bHR2Ni4tLuVqSrvTQQw/RsWNH67JWq2X48OEARb7GivPGG2/Y9Ev09/dnwIABZGRkcOTIkTKXo9fr0ev1duuv5RwHBgbi7u5ut37YsGF4enry999/l7msP/74g9jYWF566SVatGhhk9apUycGDBjA8uXLSU9PL7GcixcvsmDBArp3787o0aPt6vvSSy+RmJhYZN3K834pVNTr1tfXt0zbFirs/nKl4OBgBg0axLFjx+y6R5T3uSyqzgaDocjn9mqF5+Wtt96yOXc+Pj7WFt+iumN06dKFIUOG2KwbNWoUcG3vh9JERkby0ksv2awrbP2+cj8Vdb0rzo8//ojJZGL8+PGEhoZa1xsMBt5//33A9jzNnz8fs9nM+PHjCQwMtK738PDg9ddfd6gO18rJyQmtVmu3/srX84wZM7BYLEydOtVupClFUQgJCbEuV8T14vvvvyc9PZ13333X7lePoUOH0rJlS2u3xduBdMMQ5dKrV68ig+Li1K1b1+5NrNPpqFatGqmpqdZ1Xbp0ITg4mPfee4+9e/dyzz33EBUVRYMGDWwC4iNHjpCSkkJISAhTpkyx219iYiJAkT+nXf0zIhR8UJ48edIuTavVEhgYSFxcXJHH1blz52LX7d69u8htCm3ZsgWA+Pj4Im+WLKz74cOHrd1UijNs2DDeffdd5s6dy3333QcUfKH48ccf8fPzo0+fPta8Q4cOZfHixbRv356HHnqIO++8k86dO1fIyAPdunVj2rRpREdH0759e2sXjO7du9OtWzfrayY7O5tt27bRuXNnnJycyr1fgFatWtmtK+wnfeVrrCLKSU1N5dNPP7XLV/g8Dh06lJdffpnGjRvz0EMP0a1bNzp16oSnp2eZ61Fo8eLFzJgxg127dpGSkoLZbLamFfe6LErh6+3IkSNFvt4uXLiAxWLh6NGjtG7duthytm/fjtlsJi8vr8hyjh07BhS8bu+55x6btPK8X/r168fEiRN5+umn+eeff+jduzdRUVFl6jZytZMnT/Luu++yevVqzp07R15enk16XFycNVAoz3PZoEEDmjZtyvz58zl79iwDBw6ka9euNG/e3KbhoCSF56WooRMLuzDt2bPHLq2i3g+lKepYitpPRV7vilLSebrjjjtwdna2OU+FDSCFXRiudOWX7soydOhQvvjiCxo3bszQoUPp1q0bd9xxh90Xq23btgHQs2fPMpVb3utF4fO0detWTpw4YZeem5vLxYsXuXjx4k03Wo0jJFgW11VxHyw6nc7mzezl5cWWLVt48803+f3331m+fDkAoaGhvPrqqzz11FNAQZ9JgAMHDnDgwIFi95uVlVWmuuh0uhLTjEZjkeVf2R/v6nVX34x4tcJjWLZsmV2L8JWKOoarNWjQgFatWrF8+XJSUlLw8fFhzZo1nD17lqeeesqmVeyBBx7gt99+4+OPP+arr77i888/R1EUa6Bb1JeJsurcuTNarZbo6GgmTpxIdHQ0jRo1IjAwkG7dujFnzhwOHjzIuXPnyM/PL7G/8rUq6Xm98jVWEeWkpqYW+SWtMAiYMGECfn5+fPnll0ybNs3an7Zv37588sknRbZsFmXatGlMmDCBgIAAevbsSY0aNawfpp9++qldkFeSwtfbDz/8UGK+0l5vheVs3LiRjRs3XlM55Xm/REREsGXLFt566y2WL1/Ozz//DBSMQPH2229b++SX5vjx47Rt25b09HS6detGv3798PT0RKPRsGbNGtauXWtzXsvzXOp0OlavXs1bb73FokWLGD9+PAABAQE888wzvPbaa0W2LF4pPT0djUZj82tYoWrVqqEoSpG/BlTU+6E0Zd1PRV7vilJ4Dop6jSmKQrVq1Th37pxd/itblQsVVUZF+9///kdkZCSzZs1i6tSpTJ06FWdnZwYPHsy0adOsgWhaWhqKopRpFI6KuF4UPk+ff/55ifmysrIkWBaiKoWFhTF79mwsFgv79u1j5cqVfPbZZzz99NP4+Pjw4IMPWi/QgwYNYuHChVVW1/j4eMLCwuzWAaXeSFF4DMXdiHithg0bxvPPP8/PP//ME088Ye2CMWzYMLu8AwYMsHYt2LhxI4sXL+a7776jd+/eHD582OGxsj09PWnVqhUbN24kJyeHDRs28OijjwKXW8Gio6OtLRwVGSxfTxERESWOeKIoCqNGjWLUqFEkJSWxfv165s+fz88//8yxY8fYt29fqUGSyWTinXfeITg4mD179th8qKuqygcffHBNdS58vf3+++92Lb6OlDN+/Hi7kU9KU573C0Djxo1ZuHAhRqORnTt3smLFCj777DOGDBlCSEhImVoEP/nkE1JSUpg7d65d96SxY8daR4AoVN7n0s/Pj+nTp/PZZ59x+PBhVq9ezfTp05k8eTJ6vZ6JEyeWWF9PT08sFguJiYl2gV1CQgKqqjr0i8X1VtHXu+LKj4+Pt+s+oKoq8fHxNuep8HFCQoJd/sLXZGXS6XRMmDCBCRMmEBcXx9q1a5k1axbff/89Fy5c4K+//gIKbhRUVZXz58+XOL5zRV0vCs/Lv//+61AL/61G+iyLG55Go6F58+a8/PLLzJ8/H8A6JFyDBg3w9PRkx44dxbb6Xg/r168vdt3V/UKvVthXd/PmzRVSlwcffBCdTse8efPIyclh8eLF1K5dm/bt2xe7jYeHB7179+brr79mxIgRxMfHs3Xr1nLVo1u3bmRnZ/PFF1+Qnp5O9+7dgYIvQbVq1WL16tVER0fj5uZGmzZtylSmRqOp0Naw68nPz4+BAwda+/kePHiQ48ePA1iDrKKO7eLFi6SlpXHHHXfYBUk7duwgJyfHbpuSyquo11ubNm1QFMWhcsrzfrmSXq+nffv2TJkyhc8++wxVVfnjjz+s6SWdh8Kflq8c8QIKAoqSWsqh5OeyNIqi0KBBA55++mlWrVoFUOQQl1crPC9FDSdWuK48vwaVpKTzeK0q+np3tZLO09atW8nNzbU5T82aNQMo8jnftGlTmfdbEecoJCSEBx98kD///JPatWvz999/W9/fbdu2BQqGxSyJI9eLolT283SzkWBZ3JAOHDhQ5Lf6wnWFwx3pdDqefPJJTp06xYQJE4oMmPfv33/NY0teq3feecfm5+O0tDSmTp2KoijWm8uK07ZtW9q1a8f8+fNZsGCBXbrFYrFr5SpJYGAgPXv2ZOPGjXz66aekp6cXeWPfunXrirywF56rK4eUOn/+PIcPHy71J/IrFbYWF97QeWUfwm7durF69Wq2b99Ox44di7xpqii+vr6cPXu2zHWoamvWrLFreTYajdafOAvPsY+PD4qicObMGbsyAgMDcXFxYdeuXWRnZ1vXp6Sk8Oyzzxa538Ibg4oqb8CAAYSFhfHxxx8XOSa30Wgs0xB7QUFBDB48mE2bNvHhhx8W2cK+detWmzoXKs/7ZefOnUV2N7j62gAln4fCVsSrj/W9994r8uaysj6XRYmNjS1yvN2i6lycwvMyZcoUm+NPS0uzdgUq7dw5qqTzeK0q+np3tYceegidTsfHH39s0zc3Pz+fV155BcBm6MmhQ4ei0WiYNm0aFy9etK7Pysoq8xCR4Ng5ysvLKzIgz8rKIjMzE71eb+0HPnbsWLRaLa+//rrdjaeqqlqP1ZHrRVFGjhyJh4cHr732WpFdHLOzs639mm8H0g1DlMvx48dLnMHv1VdfLdMHwdVWrVrFSy+9RMeOHalbty5+fn6cPHmSpUuX4uzszNNPP23NO2XKFHbt2sVnn33GsmXL6NKlC4GBgZw7d45///2XvXv3snnz5iL7pFWUunXr0rhxY5txY8+ePcuLL75Y4k1ShebPn0+3bt0YOnQon376KS1btsTFxYXTp0+zefNmEhMTyc3NLXN9hg0bxvLly613yRcVLD/33HPExcXRqVMnIiIiUBSFDRs2sG3bNtq3b29zw8vEiROZM2cOs2bNKnIygqJ06tQJvV5PYmIiLVq0sBmPs1u3bnz77bfWx2XVvXt3fv75ZwYOHEiLFi3QarX079+fpk2blrmM62ngwIF4enrSvn17wsPDMRqNrFq1ioMHD3L//fdbAzZ3d3fatGnDunXrGDZsGHXq1EGj0VjH333qqaeYNm0azZo1o1+/fqSnp7NixQrCw8Nt7oIv1L17dz766CMef/xxBg0ahJubG+Hh4QwbNgyDwcDChQu5++67iYqKonv37jRp0gRFUTh16hTr16/Hz8+vTGPMfvHFFxw5coSXX36ZuXPncscdd+Dt7c2ZM2fYsWMHx44d4/z583ZTP5fn/TJ37lxmzJhBly5dqFWrFp6enhw8eJDly5fj6+vLyJEjbc7DwoULGTRoEHfffTfOzs7Wczh27FhmzZrFoEGDGDx4MH5+fmzZsoVdu3bRt29fu/60ZX0ui7Jnzx7uu+8+2rZtS8OGDQkKCrKO6azRaHjhhRdKPdddunTh2WefZfr06dZzp6qq9dw999xzRY6vXhFKOo+OqOjr3ZVq1arF+++/z/jx42natCmDBw/Gzc2N33//nSNHjjBgwACb62G9evV49dVX+e9//0uTJk0YPHgwOp2OxYsX06RJE/bv31+mmzDr169PSEgIP/30EwaDgRo1aqAoCs8++2yxXYtycnKsn3GtWrUiLCyMzMxM/vjjDy5cuMCECROsIz81adKETz/9lOeee45GjRoxcOBAwsPDuXDhAuvWraNv3758+umnaDSaa75eFCUgIID58+fzwAMP0KxZM3r37k39+vXJy8sjNjaWtWvX0qFDh2u6wf+mVgXD1YlbwJVja5b0l5KSYt2GEsbIDQ8PV8PDw63LBw8eVMeNG6e2aNFC9fPzUw0Gg1qzZk11+PDh6oEDB+y2N5lM6owZM9SOHTuqnp6eqsFgUMPCwtTevXurX375pc2YySWNQVvSeKJX1/HK/Dk5OerLL7+shoaGqk5OTmq9evXUzz77zGbcZ1UteSzO5ORk9fXXX1cbN26suri4qO7u7mqdOnXUhx56SF28eHGRdSpOdna26unpqQLqHXfcUWSen376SR08eLBaq1Yt1dXVVfXy8lKbNWumvv/++3ZjdBaes6vHPS5Nhw4dVEAdP368zfq4uDjra+TqsY9Vtfhxls+fP68OHjxY9ff3VzUajU2e4rZR1eLPe1GvyZJeH0WNR1qSL774Qu3fv78aHh6uOjs7q35+fmrbtm3VL7/8Us3Pz7fJe+TIEbVPnz6qt7e3qiiKzX7y8/PV//znP2qdOnWsr+3x48erGRkZRb4uVVVVP/jgA7VOnTqqXq8v8jjPnj2rjhs3zlqmp6en2qBBA3X06NHqP//8U6bjU9WC19oHH3ygtmrVSnVzc1NdXFzUyMhIdeDAger3339vHWtbVSvm/bJlyxb1iSeeUBs3bqx6e3urLi4uap06ddRnnnlGPXXqlM32RqNRffnll9WwsDBVp9PZjWUdHR2tduzYUfXw8FC9vb3VPn36qDt37izyeb6W5/Lq833mzBn11VdfVdu3b68GBgaqTk5OalhYmHrfffcV+fovycyZM9U2bdqorq6uqqurq9qmTRt15syZdvlKutaUNq731Uo6j6WVVdx1vyKudyW9H5csWaJGRUWpHh4eqsFgUJs0aaJOmzbN5vV4pS+++EJt0KCB6uTkpNaoUUOdMGGCeubMGRVQBwwYYJO3uGvEli1brPssvL4VdR0plJ+fr77//vtqz5491Ro1aqhOTk5qtWrV1C5duqg//vij3ftBVQue13vuuUf19fW11nXQoEHqxo0bbcq9lutFSefx8OHD6mOPPaaGh4erTk5Oqo+Pj9qkSRP1ueeeU7dt21bssd1qFFW9hvl4hRBCCAd17dqVtWvXXtM08EJUlb///psePXrw8ssvW8doFrcn6bMshBBCiNtWYmKi3f0bqamp1hFKipqyXdxepM+yEEIIIW5bP/zwAx999BHdu3cnJCSE8+fP8+eff5KQkMCIESO44447qrqKoopJsCyEEEKI21aHDh1o1aoVf//9N8nJyWi1Who0aMAbb7xhnQBL3N6kz7IQQgghhBDFkD7LQgghhBBCFEO6YVxnFouFuLg4PDw8UBSlqqsjhBBCCHFbUlWVjIwMQkJCShxPW4Ll6ywuLo7Q0NCqroYQQgghhKBg5sUaNWoUmy7B8nXm4eEBFDwxnp6eVVwbIYQQQhTFaDSSlJRU1dW4Lfn5+aHX6yt9P+np6YSGhlpjs+JIsHydFXa98PT0lGBZCCGEuEEZjUby8/Oruhq3JU9Pz+sSLBcqrVus3OAnhBBCCCFEMSRYFkIIIYQQohgSLAshhBBCCFEMCZaFEEIIIYQohgTLQgghhBBCFEOCZSGEEEIIIYpxSwfL27dvp0+fPnh7e+Pm5kb79u35+eefy7z97NmzURSl2L81a9ZUXuWFEEIIIUSVu2XHWY6OjqZXr144OzszdOhQPDw8WLRoEUOGDOHMmTOMHz++zGUNGDCA5s2b262PiIiouAoLIYQQQogbzi0ZLJtMJsaMGYNGo2HdunXWQPfNN9+kbdu2TJo0ifvvv5/w8PAylTdw4EBGjBhReRUWQgghhBA3pFuyG8bq1as5ceIEDz30kE2LsJeXF5MmTSI/P585c+ZUXQWFEEIIIcRN4ZZsWS7sS9yzZ0+7tF69egGwdu3aMpe3e/dukpKSMJlMREREcNddd+Hn51embfPy8sjLy7Mup6enl3m/QgghhBCiat2SwfKxY8cAqFOnjl1aUFAQ7u7u1jxl8dlnn9ksu7i4MHnyZF555ZVSt3333XeZMmVKmfdV0WIOZLHpj4soGoXWd/pQt6WHTfov/zsDioJfsBPdHwi0Sdu5OoWzx7JBUbhrSCBuXpdfLoln89j+TzKKolC7qRt1mtuWu2nZRYz5Kq7uWtr08LVJO30km/gzuWg0CvVaeeB+Rbm5WWZO7M9CowGfACeCIpxttk04m4vFBFq9QkB1g02ayWjBYgaNFrQ6pdS53oUQQgghSnNLBstpaWlAQbeLonh6elrzlCQyMpLp06fTq1cvatSoQXJyMqtXr2bixIm8+uqruLq68uyzz5ZYxsSJE3nxxRety+np6YSGhl7D0ZRPerKR4/uyAKjTwt0mTVVVDm7LAKBGHRd4wHbbU4ey+HdTQUt45wH+uF1xOlMv5rN9ZQoALm4au2B5w9IkcjLN+FTT2wXLB7els/XPZACqhRpsguWUhHwWfnYWgFZ3+nDPqGCbbRd+do7Ec3noDQqTZjawSduyIpl/FiQAMOSFGtRv7WlNy0gxMmPSSTRahbotPezKjV6YwMVzeeicNPR7LBid0+UeSoln84g5mIXeSSGsnit+wZeDdFVVC+rjpMHgosHV45Z8SwkhhBC3LflkL0FUVBRRUVHW5erVqzNs2DBatmxJ69ateeutt3jyySfR6Yo/jQaDAYPBUGx6ZVMtlx9f3c6qqsWn2aWX0EiraOwTVUvBxpoi04rf1nJFmlZrv63ZrJaaBqC5Kt1sUslKNwMFrddXiz2YxekjOQD0HxNik3b6aDYr5lwAoN+YYJtg2WxS+fKVkwCE13dlxBsRNtv+/VM8pw5n4+Kmpd/oYDx89Na0jBQjZ47l4OyqwT/EgKevHiGEEELcWG7JYLmwRbm41uP09HR8fHwcLr9Ro0Z06tSJv//+m0OHDtGkSROHy6psTTp6Ua+1B6igd7INIBUFnv+sDqgF3Rau1uOhanS5NwBVVe0CudC6rox+O7Igzcc+yHtgXA1MRhW9k/09pE07eREc6YxqUfEOsN3W01dHj4eqoaoqwVd1wQBo0sGLzFQTOif7+voE6KnZ2A2zScXN0/6l7R2gx2xScfXQ2qUZ8wsCbUUp6MZhm3Y5gtfrNVelXQ7Qi6pT4rk8zh4rCMKv/uJw9ngOv/yvoBX9rqGBdOznb5P+xSsn0GggKNyZgWOr26QlnM0lL9uCl58edx9dkV9KhBBCCFF+t2SwXNhX+dixY7Rq1com7cKFC2RmZtK2bdty7cPfvyCwycrKKlc5lU2rU3DR2QeHAIqi4OVXfGtmSS2dzq5aqtdyKTa9ZmP3YtOq13IpdlsPHz0d+hZ/82TXQQHFpjXt5E3TTt5FpnkHODHuU/s+7IWGTQwnP9eCyWix6+tcu6k7LmO1mIyqXb01CrTo6o0xz0K1MPvg3my8HEw7u9k+DzlXtHBfnWYxqySeLbgxtKgvHFtWJLN7TSoAj/8nkuCIy/VKTzJyZFcGXn56qoU54+UvLdZCCCGEo27JYDkqKop3332XlStXMnToUJu0v/76y5rHUWazmR07dgCUeaxmcWNzcdPi4lb0lwr/EAP+IUV3pTG4au26bVzpkVfDsZhVcrPNdq331Wu6cNfQQHKzzQRH2gba+XkW3Dy15GSZ7QJpKAiIC139hScuJoflswu6jXS7P4Au99p+wfhr3gWcXbUEVDfQsJ0nQgghhCjeLRks33nnndSsWZMff/yR5557zjrWclpaGv/9739xcnLi0UcfteY/f/48aWlpBAcH29wUuHPnTruWabPZzKuvvsrx48fp1q0bwcG2N4oJcTWNVinyxr9qYc5FtkZDQcv9hC/roaoqFvsu1jS6wwvfYCeyUk24uNsG02kXLwfSnlcF0iajhS0rCm6urF7LxS5Y3r02lYQzuYREulC3hTsG16K/QAghhBC3i1syWNbpdHz77bf06tWLLl262Ex3ferUKT766CObqaonTpzInDlzmDVrls1Mfa1bt6Zp06Y0bdqU6tWrk5yczNq1azl69Cg1atTg22+/vf4HJ24riqKgLeJd2iLKmxZR3kVuU7uZO/2dNaQlGe26jWSkmKyPi+qecXBLmnX0lHGf1rYJlnOyzBjzLHIjohBCiNvKLRksA3Tr1o0NGzYwefJkFixYgNFopEmTJrz//vsMGTKkTGWMHz+eLVu2sGrVKpKTk3FycqJ27dq8/vrrvPjii+W6SVCIyuIXbLAZseNKHj46Hp8aSVqSye5GR1VViYvJBcDVQ2sXTB/YnMayWRdw99Zxz6hg6rWyHS5QCCGEuBUpqnrlAGGisqWnp+Pl5UVaWhqentJfVNxYsjNMnDuRQ06WhaYdbccpX/J1HHvWpgIwanIEoXVdrWmZqSb+mneB4EgXajZ2Iyi86O4lQghxszAajVy8eLGqq3Fb8vf3R6+v/F8xyxqT3bIty0KIa+fqobObYKZQcIQzaRfduHAq1y4YPncih/2b09m/OZ0Off3s0hPP5eFbzanIIQqFEEKIG5kEy0KIMmnb05e2PX1RVdVueL24mBzr46v7SZtN6qXZE6FRey/6jwmWqciFEELcNCRYFkJck6IC3ah7A2jQxpO4kzmE1Xe1SYs/k4vZpGI2FcyeKIGyEEKIm4kEy0KIctNoFYLCnYvsq6zVKjTp4MmxPZk07eRll77gkzMEhhpo0sGr2PGshRBCiKoiwbIQolJVC3PmvqdrFDk7YsKZXA7vyODwjgxO7Mti9NuRVVRLIYQQomj28+gKIUQl0Ok1djf4nTmWA5dWNelo3+p8Yl8medlFzMoihBBCXCfSsiyEqDKtuvtQt4U7+7ek07i97bA9WWkmfvjwNFqtQqs7feg9LKiKaimEEOJ2JsGyEKJKefjoueNuP7v1+7eko1rAZFHR6eWmQCGEEFVDumEIIW5IEQ1cadPTB1dPrd0EKaZ8C1++eoK/f4on4UxuFdVQCCHE7UBaloUQN6RqYc70GR5M70eC0GhtW5aP7skk4UweCWfyyEw1MXBs9SqqpRBCiFudtCwLIW5oVwfKAEnn81AuXb2KujFQCCGEqCjSsiyEuOl0HhBAy24+HNqeQWQjN5u0pPN5/PHdebo9EEhYPddiShBCCCHKRlqWhRA3JTdPHa3v9EGjsW15jv4lkdhD2cx6O5Z/N6ZVUe2EEELcKiRYFkLcMvJzLcRfuuHPzVNLvVYeVVwjIYQQNzvphiGEuGU4OWt48r1a7Fmfilar4ORs2x5w5lg2voFOuHnJpU8IIUTZyCeGEOKWotEqtOzqY7femG9h4Wdnyc22cEcfX6LuDUDRyPjNQgghSibBshDitrB9VQrpySYAzsfkSqAshBCiTKTPshDittCkgyet7/JBq1PoPjjQLl1V1SqolRBCiBudtCwLIW4LHj56+o4MpuugANw8bS99F2Jz+W3GObo9EEjdFu4oirQ6CyGEKCAty0KI28rVgTLA6l8SiD+dx0/TzvDvpvQqqJUQQogblQTLQojbWn6uhewMMwCefjoatpHh5oQQQlwm3TCEELc1J2cNj02J4MjODEBB52TbhnB8Xyb+wU54BzhVTQWFEEJUKQmWhRC3PUVRqN/a0259Xo6ZX784R36ehah7A+h4j5+MoiGEELcZ6YYhhBDF2PpXMtkZZkz5KmePZUugLIQQtyFpWRZCiGK07elLdoaZ3WtS6f1oUFVXRwghRBWQYFkIIYrh7Kql97Agou4NwMVda5MWfzqXs8dzaNnVW1qchRDiFibBshBClOLqQFm1qCybeZ4zx3LYsy6VB56rgaevvopqJ4QQBaKjo8nNzeXuu+++prTKduTIEVauXImqqgQGBjJw4ECcnZ2vez0cJX2WhRDiGp06nM2ZYzkA5GSacfXQlrKFEELcGrKzs68pf15eHkuXLmXo0KE899xzeHh4sG7dukqqXeWQlmUhhLhGEQ3deHRSOMtnn+fu4UHo9NLuIMTt5ty5c6xatYq8vDxUVaVz5840atSIlJQUvvrqK9q3b8/Ro0etrbl169bFaDTy22+/ER8fj1arxc3NjUcffRSAvXv3sm3bNsxmM05OTvTp04egoCB2797Nvn37cHNz48KFCzg7O9O/f39Wr17NxYsX8fT0ZMiQIRgMBgDS09OZPXs2GRkZ+Pn5MXDgQFxdXe3qv3HjRg4cOIDFYsHNzY1+/frh7e1d6nEvWLAAgCZNmtCwYcMiy77S8ePHCQoKIiAgAIA2bdowd+5cevbseS2nu0pJsCyEEA6IbOTGk+/VQqO17a+ck2VmxZwLdLs/AJ9AGZtZiFtRTk4Ov//+Ow8//DAeHh5kZWUxY8YMQkNDgYLW1GrVqtGtWzeOHTvGn3/+Sd26dTl+/Di5ubk888wzwOVW2tOnT/Pvv/8ycuRIdDodp06dYtGiRTz99NMAxMXF8eSTT+Lt7c3ixYuZP38+jz32GO7u7vzwww/s3buXtm3bAnDq1CmefPJJPDw8+OOPP/j777/p37+/Tf337dtHUlISo0ePRqPRsHfvXpYtW8bDDz9c6rGPHDmSuLg49u/fz7fffou/vz9NmjShXr16ODnZX/PS0tJsgnBvb28yMzMxm81otTfHr3ISLAshhIOuDpQB/vkpgX83pnFoWzr3PV2dBm3sx28WQtzczpw5Q0pKCvPmzbNZf/HiRXx8fNDpdDRo0ACA0NBQkpOTAahWrRqJiYn88ccfREREUKdOHQAOHz5MfHw833zzjbWsnJwcjEYjADVq1LAGnCEhIZjNZtzd3QGoXr06SUlJ1u3q1q2Lh0fBTKStWrWytgRf6fDhw8TFxTFjxgwAVFW9puMPCQkhJCSEHj16cOrUKZYtW8bvv//O+PHjrS3ctxIJloUQooLk5Zg5tjcDKAikq9d0qeIaCSEqS0BAAKNHj7Zbn5KSglarRVEKvkwrimINRn19fXn66aeJiYnh5MmTrFq1irFjxwLQrFkz7rrrriL3pdNdDtcURbFbtlgsxdazsB5X69SpE61bty7xGPfs2cPmzZsBaN++PS1atADAYrFw6tQp9u/fz4kTJwgPD6dXr15Ftix7eXlx4sQJ63Jqairu7u43TasySLAshBAVxuCi5an3a7FmUSLeAU54+tmOkKGqarEfXEKIm0doaCipqamcOHGCWrVqAXD+/Hlrv9zipKWl4eLiQv369alduzaHDx8mLS2NevXqsWjRIlq3bo23tzcWi4Xz589TvXr1a67bsWPHyMzMxN3dnV27dlGzZk27PPXr12fTpk3WPsdms5mEhASCg4Nt8jVv3pzmzZvbrFu9ejX79u0jKCiIJk2a0Lt3b/T64kcDql27NsuWLSMxMZGAgAC2b99O48aNr/m4qpIEy0IIUYEMLlp6PWI/gYnFovLjB6ep19qDVt190MjYzELctFxcXHjooYdYuXIlK1euxGw24+XlxdChQ0vcLiEhgb///hsoaJ1t1qwZQUEF14sePXqwYMECLBYLZrOZOnXqOBQsh4WFsWjRItLT0603+F2tadOmZGdnM2fOHGtdWrRoYRcsFyU4OJgOHTqUeeg3g8FA//79+emnn7BYLAQGBnLvvfde0zFVNUW91o4qolzS09Px8vIiLS0NT0/pyyjE7WL738ksn3UBgKadvLj3yWv/EBRCXD9Go5GLFy9WdTVuS/7+/iW2VleUssZkMt6REEJcB0nn862PW3b1rrqKCCGEuCbSDUMIIa6D3sOCaNDag5iD2YQ3cLNJy0ozYXDRoHOS9gshhLjRSLAshBDXSXgDN7tAGWD5nAucOZpN54H+tOzqg1Yn/ZmFEOJGIc0YQghRhS6cyuXg1nQyUkysXZSI2SS3kQhxo/n55585c+ZMqflmzZrFoUOHAPj111+tw65d7ZNPPuH8+fN263/99VemTZvGl19+af3bvXt3+SpfQQpHwagoR44cYdasWQBkZmby9ddfYzabK6z8iuRQy/Lp06fZs2cPLVu2pEaNGtb1Bw4c4JlnnmHPnj1ERETwwQcf0KNHjwqrrBBC3Gr0Tgr1W3tweEcGnfr74+QsbRhC3EjOnj1LTk6OdXa+ytahQwfuuOOO67Kva9G9e/dKK9vd3Z3Q0FD27t1Ly5YtK20/jnIoWP7oo4/4/PPPOXjwoHVdVlYWd911F/Hx8UDBHOf9+/dn37591hlqhBBC2PILNjDkhVDiTuYQWMN25itjvoWfpp2h1Z0+NGjtgSLDzQlx3e3cuZMmTZpYl/ft28fWrVsxm82oqkr37t2pV69eFdawoEW7ffv21lkDFyxYQN26dWnRogW//vorOp2O5ORk0tLSCAwM5P7770en0xEdHU1iYiImk4mLFy/i5+fHXXfdxcqVK0lJSSEkJIT77rsPjUbDr7/+SlBQEHfccQdLly5Fq9XSt29fsrOz+eabbxgwYAARERHs3buXbdu2YTabcXJyok+fPgQFBWE2m1mxYgUnT57E2dmZ8PBwm2No0qQJK1asuCGDZYeaMNatW0edOnVsXhw//vgj8fHxDBw4kD179vD222+Tl5fH//3f/1VYZYUQ4lYVUtPF7ga/Hf+kcHJ/Fr/87ywrf4yvopoJcXuLjY21Ge+4du3ajB49mrFjxzJ06FCWLl2KyWSqsP1t2rTJphvGqVOnyl3mhQsXePDBB3nmmWfIysqydhUBiIuL49577+XZZ58lPz+fpUuXMnjwYJ5++mkSExM5fvy4XXl33303p0+f5sCBA/z222+0bNmSiIgITp8+zb///svIkSMZO3Ysd955J4sWLQIKvnQkJSXx1FNPMWrUKLtuKMHBwcTHx5Obm1vu461oDrUsnz9/nlatWtms+/PPP1EUhenTp1O9enWaNm3KDz/8wOrVqyukokIIcbs5dSi74IECzbt4V2ldhLhdpaen4+7ubl1OSUmxTvqh0WjIyckhJSWl1Nn7yqoyumHUr1/fOhV19erVSU5OtqbVqlULFxcXoCBg1Wq1GAwG63JSUpJdeXq9nsGDB/P1119To0YNOnXqBMDhw4eJj4/nm2++sebNycnBaDRy8uRJmjVrZp2qu0WLFuzatcuaT6vV4uzsTEZGRoUee0VwKFhOSUnB19fXZt2WLVto2LChzbevJk2asGrVqvLVUAghblNDXqjB8b2ZnD2eQ7Uw29myEs7mkpNhLnJ0DSFExdHr9TYtxwsXLuSuu+6iUaNGALz33nsV2rLsCI1Gw5VzzF1dn8IAFUBRFCwWS7FpJeW90sWLF9Hr9WRlZWE2m63bNWvWjLvuusuh4zCZTNdlMpJr5VA3DDc3NxITE63LsbGxnD9/no4dO9rk0+l0Vf4CEkKIm5WiKNRp7kG3+wPt0v6en8DsqaeY++4pMlKMVVA7IW4P1apVs5nJLzc3Fx8fH6Dg/qzr1W1g69at1qmyr+br68vZs2eBggbN06dPV2pdUlNTWbZsGY8++ig1atTgzz//BKBevXrs27eP1NRUoGAa7XPnzgFQs2ZN9u3bh9lsxmQysWfPHpsyMzMzURTlhpzd2KGW5YYNG7JhwwYSExMJCAjgxx9/RFEUOnfubJPvzJkzVKtWrUIqKoQQosC5Ezkc25MJFMwM6OKureIaCXHratiwISdOnKBWrVoA9O7dm59//hlnZ2ciIyPx8vJyqNx58+ah0Vxusxw9ejRQ0Gf5ykCyadOmdOzYkcTERGuQfrWOHTuycOFCvvjiCwICAmxGKqtoZrOZhQsX0r17dwIDA+nduzffffcd+/fvp3HjxvTo0YMFCxZgsVgwm83UqVOH6tWr06pVKxISEvj888+tN/jFxcVZyz1+/Dj169e3OSc3CkW9st2+jL7++mvGjh1LaGgoLVu2ZPny5bi4uHDmzBk8PDyAgm9efn5+dO/end9//73CK36zKus85EIIURyLWWXfxjTWLk6k8wB/Wnaz/QDNyzZjcJUAWojyMBqNXLx4kby8PL777jtGjx5t7fdbFb777jseeeQRa3/iW83MmTPp168fAQEB+Pv7X5fuGGWNyRxqWR4zZgxbtmxh9uzZ1gB55syZ1kAZYOnSpeTk5NClSxdHdiGEEKIYGq1C8y7eNOnghXLVaHJ5OWY+G3+cWk3cibrPH7+gW/ODVYjrxWAw0Lt3b1JSUqr01/LHHnusyvZd2TIzM2ndunWF3SRZ0RxqWS505swZ4uPjqV+/vs2dogB79uzh1KlTtG/fXrpiXEFaloUQlWndr4lELyy4p6TLvf5F9ncWQpSusGVZXH+3RMtyodDQ0GJntGnevDnNmzcvT/FCCCGukYuHFhd3LTmZZhq1t7345+da2LUmhYZtPfH0vfHuOBdCiBtRuYJlIYQQN5Y2d/nStKMX+zenE1jDdri5o7sz+GtuPH/Ni+euoYF0vMe/imophBA3jzIFy99//z0A9957Lx4eHtblsnr00UevvWZCCCEcYnDR0qq7/V3zB7emFzxQISTSxSbNYlHJyTDj5iVtKEIIcaUy9VnWaDQoisKhQ4eoW7eudbmszGZzuSp5K5E+y0KIqpJ4Lo8DW9KJPZjFo5PC0WgvX8dPHcpizn9OEdHQjY79/KjVxL2EkoS49Umf5apzU/ZZfvTRR1EUxTqWYOGyEEKIm0dAdQNdBwXAIPs7zvdvSUdVIeZAFi2ivK9/5YQQ4gZVpmB59uzZJS4LIYS4uXkH6PEJ1JORYqJuS9tW5aTzefw1L56G7Typ38oDZzcZw1kIcfuQzmlCCCHoeI8/Hfr6kZpoxOBiGwwf2JLOsT2ZHNuTSfZD1ejQ16+KaimEENdfpQTLhX0/pKuGEELcPBRFwSfQfoay00eyrY+vHo4u7mQOf82LJzjSmUbtPAmt61rp9RRCiOvJoQm49+/fz2effcbRo0dt1kdHRxMZGYmvry+BgYHSXUMIIW4BD78cxqjJEfR8uBpefrY33Zw7kcPpI9ls/TOZ87G5NmmqqrJ3fSoXTuViNjk8/5UQQlQph4Llzz77jBdffBEXl8tDDyUlJTFw4EBOnTqFqqokJSUxevRodu/eXWGVFUIIcf0pGoXQuq7c0ce++0VKQr71cXCk7bjOGckmfvsqjhmTTvLTx2fstlUtEkALIW58DgXLGzdupFGjRjaz982dO5eMjAyeeOIJUlNT+f7777FYLEyfPr3CKiuEEOLG0vPhIF76qi6PvBpGULhtsHxlS3O1MIPdtt+9FcuM107y+3dxlGEUUyGEqBIO9VmOj4/njjvusFm3atUqtFotU6dOxdPTk0ceeYRPPvmEzZs3V0hFhRBC3JhcPXRFjsscUMNAj4cCOR+TS0RDN5s0k9HC+dgcLGYwG1W7e1y2rUwm4Uwenr462vTwxcVdRuAQQlQNh4LlwkGcr7R161aaN2+On9/ln+nq1KnD8uXLy1dDIYQQNyXfak506Fv0lNqZqSb8gg1cPJdHcISzXfqxPZkc35sJQKs7bWcjPL4vk1U/xuPpq6fNXT7Ubelhk56fa8HJ2aEfToUQwo5DVxNPT0/OnTtnXT506BDJycl06NDBLm9Vj4ixfft2+vTpg7e3N25ubrRv356ff/75msrIy8vj7bffpk6dOjg7OxMSEsLjjz9OQkJCJdVaCCFubd4BTjz1fi1e/bY+PR6qZpeenmwEQKtTcPWwbVVOic8n4Uwex/dmkpVuskkzm1TeHX2Y90YfZuH0s3blJpzJ5dyJHJLO52GRPtNCiDJwqGW5efPmrF+/nuPHj1O7dm2+++47FEUhKirKJl9MTAzBwcEVUlFHREdH06tXL5ydnRk6dCgeHh4sWrSIIUOGcObMGcaPH19qGRaLhQEDBvDXX3/Rvn17Bg0axLFjx/j222/5559/2LJlCwEB9rNhCSGEKJ2Ts6bIVuBhE8NJTzKSnWm2a3TJz7Wg1SmYTSqeV43OkZFiBBXycixYzPbB8Kof4zm+LwuASTProzFcLvvwjgw2LE3E2VVL+z5+1G56uWuJalH5d1Mazq5aPH31BBXRGi6EuDU5FCw/8cQTrF69mlatWlGzZk327dtHYGAgffv2tebJyMhgz5499OvXr8Iqey1MJhNjxoxBo9Gwbt06mjdvDsCbb75J27ZtmTRpEvfffz/h4eElljNnzhz++usvHnzwQX744QfrRfurr77iySef5PXXX2fGjBmVfThCCHFbcffS4e5V9EdUx37+dLjHj+wMM4arAm2zWSWioSvpySZ8qtmPGZ2eXNASrdGCzsk2CE+7mM+5EwU3JTbt7G2Tlp9r4dcv4wCo2diNYRNtPzt+++ocpw5nozdoGDU5AmfXy63hZ45ms3N1CjonDY3v8CSigW3/7d1rUtDqNXh464hsZJuWmWrCZLSgc9Lg6qFFo5H5C4S43hwKlh944AEOHTrE+++/z969e4mIiOD777/HYLh8t/PPP/+M0Wi0a22+XlavXs2JEycYOXKkNVAG8PLyYtKkSYwYMYI5c+bw5ptvlljON998A8C7775r07rxxBNP8OGHH/LDDz/w6aef2gyjd6MwJZ8jNyud3JycghVF3W1euE4BTw8P67KKSk5ODkZjQSvNpcxXlVPwv16nw8XZGfWK9MyMjEt3t6u2+720rvDOd2eDAb1eZy3abDaRnZ11Oa9NGeqlfCqqasHNxZWCp6RgfX5eHvn5eUVvo1oK6qeqaDQKLs7O1vJV1UJuTjYWs9m6bN1eVUG1WMvSaTSX6luQT7VYCs6vzTaWy2VzeVmv16FRFGses9mMKT/v0qFabPd31f6dnPSXhtlSAQWzxYJFBRQFFA1oNKBoUJSC/wv+FDQ6PU4Gw+U0jYa8vHzUK/IoihY0BeUoTq4obr5o3H1x8QvG1S8ExdkdRVEwm82kp6eX6bXn6emJVns5WMjNzSWn8HVYAo1GY3c/RGZmZsHrsBQGgwFXV9sJMVJSUspUX3d3d/T6yy2URqORrKysMm3r5eVlc23IyckhLy/PLt/VraNarRZ3d9ub4jIzM7FYLKXu02Aw2FxvLRZLqfUt3L+Li4vNc2M0GsnPzy9uM5vtrz6/+fn5mEymIvNeSavV4uRkG7jm5tqOyVwcvV5vU1+LxWJ9PegMYFbNmK843e4+MHRCwS+aV+/TZDLRsL0bNVIMWMzYHXdO9uXXmbOrbRCekX55J3pn7J7jlMQ8UhMLtr8yoFVVlQuns9i7Pg0AvxAtQZFXHo/K0m/OA1CjjguPNYq0ppnNZlZ8H8fBrQV9t8e+H4qX/+XX6Yl/s/n1/+LRaCHqvgA63nO5f7jRaGT226cx5lnwDtRz79O2XV32rE3n5L85aLUKPR8OxveKLxYXzmSweXkKGo1CWH1n6rWyDeC3r0wjL9uCs6uOLgMvl2uxWDhxIJXzJ/NQFIXaLVzx8rscZuTlWDi6MwtFgcAabkQ0uPz6NxqNHN2TRm6WBUWBeq1t95l60UhSnBFFUQiv64Wn7+XzkJWZw6nDBa9/F3ctgaG2z/vFuHxysyxotVoiG3ih1SnW+iaeTyM10YQKePgouHlqARXVAvnGfE4dKriGGFx1+FQrOBbVAhazmdTkfPKzCz68PHxBo7v8WWfMVckqeMrx9HHCxb3g9aSqKiajkdSLJiyWgku3mzfWNIC8bDDmKSiKgpevHt0VP55kZ+WSnVGQT+ekYnDlis9oyMlQLpWrwdvf9nWWnZFPfm7BsTu5WGzKVS2Qk1mQZnDW4eJx+fVvNpnIyrBgvvRWd3Yzc+Vb3GQE46VyPTwN6K44/flGI1lpBdc0jRYMrrZxSH6Ogtl06XropUNzabdenp421+SqpqjlGK8nPz+f9PR0/P3tb+A4ffo0KSkp1KpVy+4D4XqYNGkS7777LvPnz2fo0KE2aRcuXCA4OJju3bvzzz//FFtGbm4ubm5u1KlTh8OHD9uljx07lhkzZrBu3To6d+5cZBl5eXk2F9X09HRCQ0M5efIkHh4eRW5TUbJmPo75tIxzLcpJq0dx80F19uJCRh75WhfytK7kaVzIK3ysvfRY44JFo+O+++6zeX0fOnSIbdu2lborDw8P7rvvPpt1//zzD2fP2vc9vVq9evVo3769zbq5c+eWKfjs3r27zVCY58+fZ+XKlaVuB/DII4/YBHM7d+5k//79pW4XFBREr169bNYtXbq0TAF+y5YtadKkiXU5JyenzPdi9OvXD19fX+vyiRMn2LBhQ6nbOTs7M2TIEJt169atIyYmptRta9asaXeN/Omnn4r8UnG1zp07U7NmTetyUlISf/zxR6nbAQwZMgRn58vdJfbt21fq2P+qCj6eAdzT/25rUAWw7PdVxB3Wg1mH4pyF3ueCzXY5J1pizvJBg44nPqxmDZjNZjOzPllL3pnGABjC96H3O3fpy68F1aSQvb/gV1nv4FyiBmVhys/HbMwnMf4Ce9cEYcouaMX2qj4PrTYLhYIv1/nZEWQk9QGgRth+goIPgcUMFjNpqckcP/k4qmpAr02khv9XgIpGtaBgITGjL2k57QBoFPwNbk7nUC59QU/KCOR46jMABDpHE+r2GwpqwZ+qsjf1LfIs/uiVDDoETLyUVlCnkxl9OJXbH4Bm7h8RoP/Xum22uRob0j8CIMRpAy3cvwRAudQ4sCH9HVLNtQEY4DMYUFEupZ/M7c3enNEAtHX9lHDD2oLtgHyLG7+l/QBAsG4nUR5TLu2zwJrMN4gztgVgsNdQDJoM6/MWm9+Z9VmvAtDa5WsaOC+xeV7npSxBRYev9hh9PZ+3SduU9Twn8nsA0N/zCby0l69T541N+TvzXQAaOy+ghcv3NtsuTP2eHNUPN00C93mNtEnblT2SA3n3A9DT/WWq6Q9Y01LM4fyR/gUAtZ3+5A432+F5/0ifToq5JlryeMjH9lp6IPc+duU8BkAXt/8Q7rTJmpZj8WFh2jwAaug30819qs22qzKmcsHUAoCh3oPQK5e/7J7Iu5NN2S8C0Nb1c+oZLg/soKoK81IL3q8B2oP09nzJptz1mS8Ra+wKwEDPUXho4wFIHjyLiIaNqWwZGRnUrFnTOvN0cco13bWTk1ORgTJAWFgYYWFh5Sm+XI4dOwYUjMhxtaCgINzd3a15inPixAksFkuRZVxZ9rFjx4oNlt99912mTJlit37p0qWV3hrdKukiPqVnE6JkZiNqegKkJxBUhuxGRU/W9Hmk6NzI17mSr3MjVzEQrrgR7xJGrq74L8+ZmZksXrzYZl12dnYxuW2dPHmSuLg4m3VlCZQBNm/ezPbt263LRbWWFue3336zaUktSwAIkJiYaHesZW3N3r9/v831q6zHCQVfPq5uWS6LvLw8u/qW5dcCKGg8uXrbsrRmQ8FN2nv27LEum83mMm0H8Mcff6DRXG4hK9Nzo6qkp11g8YJ5qCYTmPLBbMSUm42bNh+NxoQu34jmghGdxYhONaJXjeid1qDXGdFjYvdUMzrM6FQTWsx0tOgxe3qgqBrcU+NxT09FT8FxmFUdJ12PY1YNuKQn4fnD5QAmAEC9l4v6uphxomPmUpw0l18j543N2amtj0XVEHpxFbUz/rY5lDM8ghEVD1LplL/FJm2zuQOXGj6JzNqNT+4pa1q8qRHHLz32V5NpaDlus+0BLOQBOoyEGW2/LCVy+f0aQArBXLQup3P5i4szufiqqTbbai+dEwUznti+F/Rcfk86Kfk4c+Xr53Kagmo9t9Z1avFdVy6H1MVxtNuLdJcpj63btrLr8NHSM5ZTWa9h5QqWb2RpaQWXgat/0i3k6elpzVOeMq7MV5SJEyfy4osvWpcLW5b79+9f6S3LeevSyDp35KqfzgvewDY/JygKGo2GwMBAmzypaWnWLhyqNRi4+gKg4OLqgre3D4W/yygonI+/gMVsuZT98raqYluGr69fwU+7l7bNz88nPiHhch6lsF3h8uPC+oSGhqLV6qzrU9PSSElNvZTnqn1dUY7B4FzQinipGwKKhrPnzpGdk2vNq6Jc3l/hMgp+AQEF50kp+AC2qHDo8GHbfEXVWVGoVbsObu7uBd0dUEhLT+dkTIxN+bZlFBynRtHSqnXry8ehqpw6FUtCQry1JaiwVUe51F1EudSVw9PDg8jwsCu6h1g4cuQwxrw8wGKbX1XRmnLRGTPR5Wfg7aTgbMlBzU5FzUpGzUoFtfRARa8a0een4pqfWmR6lmcYaQFNSA9oQp5boE2ai4sLrQuP9ZIDBw6QnJxc6n6Dg4OpXbu2zboNGzaUabKLhg0b2gx7mZqayr///lvqdgAdO3a0CchiYmI4c8Z+trqreXl50axZM5t1O3fuJDMzs9RtIyMjbRoj8vPz2bRpU7H5rzwHrVu3trn2nD9/nkOHDpW6T71eb9cosH//fuLj40vdNigoiEaNGtmsW7t2bZkC9UaNGtncKJ6ens7WrVuL30BVUY15mHKyqBMRTH5GGvnpSZgyU8hJvoCalYzekofeko9ezcNJzcdJzcOg5mPg6iCsgihAMcNEaxUTdQzF/4rR0PnXYtOC9Xu4R/9ssekPeD9SbFpr129o7jIXC1qcFdvPMF/dcfp6PIOKBmdNqt22ndw+wKwaUAoDfjRYLrXlVnfahJv2DCpanLQXyMQVy6XrcL5ioqnrN6iAmzaeFMXLGqxaUKju/DcB6g5UIEHxK2zLLuh8pk+gJosADSZ9JnG6ELiUbjTrCHNZBii4aOI5raluEwS7Gg5RXZeCoijEOQWjVfwvXWshQ7UQ7LwaUMjRZ3NCG3nF56NCoPNGQMGgTeWkU11rigUV1RhPgGYjAKd1/jhpnK3b5age+Bu2gAL5zjkcNzSwbmtWVTyc9+NqcUGnyeaQrp7N+c1xysaP7aAoxLsEkKa73MKak++Or2EnAEZ9Ggd19YHLn9FOhlh8zKloFAtHXJpeUapKmgV8LAW/rMQ7eZChb2hNNVmc8TbsKVgwpHDE9fK1yWKxoOTH460tSD+kr4tGufxZkKG64m3eC0CaiytHnJtb04wmC96GgjSN9iL79bbXAaMhDW/NPgBOudZGry14r3fp1p3g8EgqW0ZGBs8//3yp+crVDeP8+fMsWbKEI0cKArKiilIUhe+++87RXTisZ8+erFq1imPHjtl9gAJUr16dzMzMEgPdTZs20bFjRx5++GHmzZtnl/7NN9/w+OOP8/HHH/PCCy+UqV6FY1SX1uQvxI1GtViwZKdhzkzCnHERc0ZSwV9mkvWxJfPyektO6f2b9dVq4da0J67NemIIa4qisR8VQdzeVFUlPyOVlITzpCUmkJmcQE5qIsb0JEwZySjZKWhzU3DKT8PZlI67JdOmFbKqWFAwosOEDpOiw1z4v0aPRdFhVnRYNHpUjR6LRo9FqweNHlWrB60TqlaPotWDzgml8E+rQ9HqC76caXUoWh0ajQ5Fq0XRatFcWqfVaAvy6nRotTo0Wi0anR5tYbpWi1avQ6PVoS1cr9Oi1WrRaDQomoL/NRoNGq0OjUZB0WjRarRotErBvq7Ic6syGo1cvHix9Iyiwvn7+1+XPstljckcblmePn06L730kk3LQGGwXPiTpKqqVRYsF7YGFxcMp6en4+NTcieFspRxZT4hbmWKRoPW3Qetuw8E2X8BvZpqysecmXwpeL5I7smdZO9bSX7cEWseY/wJUld9SeqqL9F6VcOtaQ9cm/bEpU47FJ39SAbi1mPOzSQh9hjxJ4+Rfu44xsRYtOnnccpPw8WUjpslAx0F3Uz0gM+lv4qSixN5OJGvccaoMWDUumDWOmPWuWDRu4DeFZxcUZxc0Rhc0Tq7oXNxQ+/ijpOrOwY3dwxuHri4eeDs7oGLiwtOzi7onfRVPs+AEKJiOBQs//PPP4wbNw5PT0/Gjx/P2rVr2bx5MzNmzODo0aMsXryY2NhYnn/+ebufGa+XK/sTt2rVyibtwoULZGZm0rZt2xLLqFmzJhqNpti+zSX1ixbidqfonNB5B6HzLujp7NowCt97XsSYeIqsfSvJ3reS3JM7raOlmNPiSV8/j/T189C4eODaqDuuzXri2jAKjcGtpF2JG5wlN5PUMye4EHOEtDPHyU+IRZt2FvfsODwtBY0Rnpf+HGVCQ4biQbbWg1y9FyaDNxYXHzTuPug8/HD2CsDNxx93/2q4e3nj6uGBq7sHTk43zh33Qogbk0PB8v/+9z8UReGvv/6iXbt2jBw5ks2bNzNmzBgApk6dypNPPsnMmTPZtWtXhVa4rKKionj33XdZuXKl3WgYf/31lzVPSVxcXGjbti1btmzh1KlTNmMyq6rKqlWrcHNzs+tjKYQonj4gHO87x+B95xhM6Ylk7/+H7L2ryDmyAdVU0F/UkpNB5o4lZO5YgqJzwqVeJ1yb9cStyZ1oPYq+qVhULUtOOlnnY4iPOULq6ePkJcSiST2DW3Yc7uaCX+GcL/2VRT46MhR3srWe5Ok9MRq8UV18UNx90Xv6YfDyx80nAHe/QHwDg/D298NJV0zHYCGEKAeH+iwHBQURGhpqvXt85MiRfP/99zZ3Kefl5REeHk7v3r2ZPXt2hVW4rEwmE/Xq1ePcuXNs2bLFOtZyWloabdu2JTY2liNHjhAREQEU9L9OS0sjODjYplvFrFmzGDVqVLGTkjz++OPXNCmJ9FkWomiW3EyyD60je+9Ksg+sxpKTYZ9JUXCu2QrXpj1xa9oTfUDJkwqJimdKT+TiwW3EH91HbnwMSsppXLLO42Yu2xjcV0rBg2R9NbLdqoNPKC5BkfiF1Sa4Zl38AvwxON2y96CLm4D0Wa46t0Sf5ZSUFLp27WpdLjygnJwc63BoBoOBzp07lziOcWXS6XR8++239OrViy5duthMd33q1Ck++ugja6AMBaNWzJkzh1mzZjFixAjr+uHDh7NgwQLmz59PTEwMUVFRHD9+nMWLFxMZGcnUqVPtdy6EuGYaZ3fcW/TBvUUfVFM+Oce2kr1vJVn7VmFOuzTigqqSe2IHuSd2kPzrf3EKqVcQODfrhVONhtJHtIKpZiP5Zw+RdHgbiQe2opzbh3teAgAel/5Kk4QnSdpAst1CUH3CMFSLwCe0NsG16tE0JBC97ta9QUwIcWtwKFj29fW1GQ+08Ea506dPU6/e5SFQzGYzSUlJ5ayi47p168aGDRuYPHkyCxYswGg00qRJE95//327wfWLo9FoWLJkCe+99x5z587lk08+wdfXl8cee4ypU6cSEBBQyUchxO1H0Tnh2qAzrg064/fAFPJO7yN73yqy9v6FMf6ENV9+3BHy446Q+ud0nGo0xLPzI7i3HoDG4FpC6aI4pvRE8mJ2kXJkO2lHtqNPPIzOUtA1pqTAOAkv4jUBZLmGYPEOxRAYiVdoLYJr1qVh9QDcnKVfsBDi5uVQN4x27dqRl5dnHSh+7ty5DB8+nA8//JDx48cDBZMLREZG4uPjw9GjlT+w9M1CumEIUT75F06Q/W9B4JwXu8cuXePigXu7+/Hs/AhO1WraFyCAglbjvLMHyYvZTdrRHeSc3IU+83yJ2+Sh5zhhJHvVQx/aFO+wugTVrEN49QB83Z2kZV/cUqQbRtW5JbphREVF8cknnxAfH0+1atXo27cvbm5uTJo0iQsXLhAWFsacOXNITk62u7lOCCHKwymoFk5BtfDuMRZTajzZ/64iY8tC8k4VDHxvyckgfc0s0tfMwqV+Jzw7D8O1cXcU7e3d/9WUlkBe7G5yY3aReWwnxrP/ojFfnoCjqI+lC/hxVAkn3achHnVaU7dFG7rXlpZiIcTtxaGW5e3btzNp0iQmTJhAr169gIIJOsaOHWvNo6oqERER7NixA19f34qr8U1OWpaFqBx5p/aRtn4eWTuXohptpzXW+oTg2fFBPDoMQed563edUs0m8s8eJDdmJ7kxu8k+sRM1Na7EbfLQc4wwjioR5AY0wrdBG5o1rkvzSB8JjsVtSVqWq86N1rJcrhn8rrZr1y5++eUXkpOTadCgASNHjpQJO64iwbIQlcucmULGll9I3/ADpounbRO1etya341Xl2EYara6ZboNqGYTeWf2k3tsCznHt5F7fBtqXlaJ25zHjyNEcFSJwBzchOoNW9CmXpAEx0JcIsFy1bmlg2VROgmWhbg+VIuFnEPrSF8/l+wD0dbJTwo5VW9QcENgm4E33Q2BqtlI3ql/yT2+lZzjW8k9saPE4DgXJ44RyhEiOKaJRKnRlAb1a9G2jr8Ex0IUQ4LlqnNLBMtvv/02zZs3p3///iXm+/3339m9ezdvvvnmte7iliXBshDXn/HiGdI3/kjGpgVYslJs0i7fEPgwTtVqVVENS6aa8sk7ta8gMD62hdyTu1Dzs4vNn4IH+6nFAWpxTBOJe1gDWtYNom0df1rU9MXVcHv33xaiLCRYrjq3RLCs0WgYMWIEM2fOLDHfmDFjmDlzps1kJbc7CZaFqDoWYx5Zu5eRvm5ukSNpuNTriGeXYbg2vrNKbwhUjXnkntpbEBgf30buyZ2oxtxi8yfhyQFq8S+12U9tkp2C6dGiOr1bVqd1bT8JjoVwgATLVedGC5Yr9QpqsVhumT6BQoibn0ZvwKPtfXi0vY+80/+Svn4emTuWWG8IzDmykZwjG9F6B+PZ6UE8Ogy9LjcEWox5BSNVHCvoVpEXs8vuJsUrJeFlDYz/pTbn8UfRKLSvF8DTbUK5q1mwBMhCCFFBKvVqeubMGdzd3StzF0II4RBDWBMCHn4f34ETydiykPT18zBdPAWAOfU8KX98TMqK6bg164U+MMJ246t/kCvrD3RX5VPN+eSd/pe82D2opvxiNoIsgz97zDXZZarJv9TmAn5wqSGidrAHL7YNpW/rGlTzdilbPYQQQpRZmYPl77//3mb5+PHjdusKmUwmDhw4QHR0NHfccUf5aiiEEJVI6+aN952j8eo2ipzD60lfN5fsA6sLAluzkaxdf1z3OineIZxzb8Sa9OpEp4cSn+dbEBxf+qHO18PAPa1r0K9NDerX8JJf8IQQohKVuc+yRqOxXpBVVS314qyqKhqNhsWLF5d6I+DtRPosC3HjMyadJWPDj6RvXoAlM7nS96fzD0MX2Yaj+rosOV+NVbH2eQx6Dd2bBNO/bSh31A9Ap9VUer2EuJ1Jn+Wqc9P2WX700UetAfKcOXOoVasWHTt2LDKvk5MTNWrUYODAgTRp0uQaqy6EEFVL71cD3wEv491nHHmn9hbZRcK+waCIBoQiGxUurzNbLOxLc+e3Q/ms3nuBXKP9zdCta/vRv20oPZqH4OEiQ7wJIcT1VqmjYQh70rIshDifksPCjbEs2nyKi+n2N/JFBLrRr20o97QOpbrfzTUGtBC3CmlZrjo3bcvylWJiYuTGPSGEuAYWi8rmI4ksWB/Dmv0XsFzVTOHt5sTdLavTv20ojcO9pR+yEELcIBwKlsPDw0tML4zQ5WIvhLjdpWXl8+uW0/y8MZbTibaz7Ok0ClGNgxjQLpTODauh10k/ZCGEuNE4FCzv37+f1atX07t3b+rWrWtdHx0dzahRozh9+jS+vr58+OGHjBgxoqLqKoQQN41/T6WwYH0MK3adI89osUmr5u3MAx0jGHRHOAFezlVUQyGEEGXhUJ/lxx9/nJkzZxITE0NoaCgASUlJ1KxZk4yMDGs+jUbD9u3badGiRcXV+CYnfZaFuHXl5JtYsfMcCzbEcuB0ql16+3oBDO0cQdfGQTKahRA3OOmzXHVuiT7LGzdupFGjRtZAGWDu3LlkZGTwxBNP8P7777N06VIeffRRpk+fLjcCCiFuabEJmfy8IZbftpwmPcdok+bpomdg+zAGd4ogIlDu9RBCiJuNQ8FyfHy83WQjq1atQqvVMnXqVDw9PXnkkUf45JNP2Lx5c4VUVAghbiQms4U1+y+wYEMsmw8n2qU3DPViaOdI7m5VHRcnmXpaCCFuVg5dwQubra+0detWmjdvjp+fn3VdnTp1WL58eflqKIQQN5DEtFwWbT7FLxtjiU/NtUkz6DXc3bI6QzpH0iTcp4pqKIQQoiI5FCx7enpy7tw56/KhQ4dITk7m4YcftssrI2IIIW52qqqy/XgSC9bH8M/e85iuGvct1N+NIZ0iGNg+DG83pyqqpRBCiMrgULDcvHlz1q9fz/Hjx6lduzbfffcdiqIQFRVlky8mJobg4OAKqagQQlxvaVn5LNtxlp82xHDyQqZNmkaBro2DGNI5kjvqBaDRSMOAEELcihwKlp944glWr15Nq1atqFmzJvv27SMwMJC+ffta82RkZLBnzx769etXYZUVQojKlpljZPW/F1ix8xybDyfYtSL7eRgY1CGcBzqEE+wrs+sJIcStzqFg+YEHHuDQoUO8//777N27l4iICL7//nsMBoM1z88//4zRaLRrbRZCiBtNTr6JdQfiWbHzHOsOxJNvstjlaV3bjyGdIrmrWbBMHiKEELcRh8ZZLpSfn096ejr+/v52aadPnyYlJYVatWrJ1NhXkHGWhbgx5BvNbDqcyIpd51i97zw5+Wa7PEE+LvRuWZ0BbUOpEyLvVyFuJzLOctW5JcZZLuTk5FRkoAwQFhZGWFhYeYoXQogKZTJb2Hr0Iit2neOfvXFk5Jjs8vh6GOjdIoTerarTPMJX+iILIcRtTgb/FELc0iwWlV0nk1ix8xyr9sSRnJlvl8fTVU/P5iH0blmd1rX9ZHY9IYQQVmUKlt9++20AnnnmGXx9fa3LZaEoCm+88YZjtRNCCAeoqsr+U6ms2HWOv3afsxsPGcDVoOXOpsHc3aoGd9QLkH7IQgghilSmPssajQZFUTh06BB169a1Lpe0aWG6oiiYzfZ9AW9X0mdZiMqhqipHzqXz565z/LnrHGeTsu3yGPQaujYOonfL6nRuWA1nJ20V1FQIcTOQPstV56bss/zmm2+iKIq1f3LhshBCVCWj2cLhs2msPxDPil3niInPtMuj0yp0alCNu1tVp2vjarg5V/4FWAghxK2jXKNhiGsnLctCOC4tK589McnsPpnMnphk9p9KJddo/8uVRoF29QK4u2V17mwWjJerzKonhLg20rJcdW7KlmUhhLjeVFUlNiHTGhjvPplcZMvxlVrV8uPultXp0SIEPw9DiXmFEEKIsqiQYFlVVZKSklBVFT8/PzQauVFGCHFtcvJNHDidyp6TyeyOSWZvTAqpWfYjV1wpxNeFFjX9aBHpS9cmQQT5uFyn2gohhLhdlCtYXrVqFdOmTWPDhg3k5OQA4OzsTOfOnXnhhRfo1atXhVRSCHHrSUjLKWg1vtRyfOhMmt3U0lfSaRQahHrRPNKXFjX9aBbpQzVvCY6FEEJULoeD5ZdeeomPP/7YbkSMnJwcVq5cyapVq3j++eeZNm1auSsphLi5mcwWjsWlsycmmT0xKew+mURcck6J23i7OdE80pfmkT40r+lL4zAfGb1CCCHEdedQsDxv3jymTZuGi4sLTz31FMOHDycyMhKA2NhY5syZwxdffMGnn35KixYteOSRRyq00kKIG09WrpEzF7M5czHr0l82Z5MKHp9PzsFcQqsxQM0g90utxr40j/QlItBdRt0RQghR5RwaDaNdu3bs2rWL1atX07lz5yLzbNiwga5du9K6dWu2bNlS7oreKmQ0DHGzslhUEtNzOXMxi7OXguKzSZeD45QiZsYrjrNeS+Nwb2tg3CzSF283GbFCCHHjkNEwqs4tMRrG/v376dSpU7GBMmBN3759uyO7EEJUgTyjmbNJ2Zy9FABfGQyfS8omz2i55jLdnHWE+rsREehOs0gfWtT0pV51L/QypbQQQoibgEPBsrOzMyEhIaXmCwkJwclJWouEqAqqqpKZayIlM5+0rHxSsvJJycwjLctISlYeqVn5pGblk5J5+f/kzDwcGXm9mrczof5u1PBzJdTfreCxf8Fjbzcn6U4hhBDipuVQsNyqVSv27dtXar59+/bRunVrR3YhhLjEZLaQk28mJ99ETp6ZtOzLAW5hkFvkuqz8EkeXuBZOOg01/F2p4ed2KRh2pcaloLi6r6vceCeEEOKW5VCw/Nprr3HnnXfywQcf8PLLLxeZ58MPP+TQoUNMnz69XBUU4kZlsagYzRaMJgtGs4V8U8HjfJNtcHvl4+x806VlMzl5pjLlM5quvevDtTLoNXi7ORHo5WzXMlzDz41AL2c0GmkdFkIIcftxKFhWFIVnnnmGiRMn8ssvvzBs2DDraBgxMTHMmzePnTt38txzz6HRaFi3bp3N9l26dCl/zUWZqKqKqoJFVVEB1EvrAFUFFdX6s7uqqljUy+sv/bOWUfiYS9taCtdf2q5gubAM2/8tFrXIfBZVRbUUse7SNma7Pwtmi4pFVTGbVcxX/W9RVUxX/V+4rcWiYrJYrOUWBLkq+SYzRpNt4FsY9BrNlivWq5iuWF/a6A5VxVmvxdvdCR83J7zcCv73cb/82NvdCe/Cx5eWXZxkMk8hhBCiKA6NhqHRaFAUxRo4Xd0fsbj1hetMJpMjdb0lXM/RMB79ZD27TiZX6j5ExdJqFFyctAV/Bt2lxzpcDFrrYy9XfUGw635lMGzAy80Jbze9BL5CCFEBZDSMqnNLjIbRpUsXuWHnJiDPkeMUpaCfrl6rQa/TXH6s1aDTadBrFfSX1jnpNNbHhfltg10trtaA99I6m8c6XC/l0+s08rwJIYQQNxCHguU1a9ZUcDVEZagV7IHJbAEFFBQ0SkEAXRiLKYqCQkFgqBSssD6+Mg0urb+iHGuZGtAoCopyuXyNcmmd5orHNusvl3NlPps8lx5rtQo6jQaNRkGrKGi1l//XaBR0moL/tZf+NIqCTlv4v+ZSGRqbbQvz2wS5VwW+WumfK4QQQgjKMd21uPG9OaRZVVdBCCGEEOKmViGzAuTn53P+/HmSk6V/rBBCCCGEuHWUK1ieN28ebdu2xc3NjRo1ajBhwgRr2q+//spDDz1ETExMuSsphBBCCCFEVXA4WB49ejTDhw9nx44duLi4cPWgGnXr1uWnn35i0aJF5a6kEEIIIYQQVcGhYPmHH35g5syZNG7cmO3bt5OWlmaXp1GjRtSoUYMVK1aUu5JCCCGEEEJUBYdu8Pv6669xd3fnjz/+IDQ0tNh8TZo04dChQw5XTgghhBBCiKrkUMvy3r17adeuXYmBMoCvry/x8fEOVUwIIYQQQoiq5lCwnJeXh5eXV6n5EhMT0Wq1juxCCCGEEEKIKudQsFy9evVSu1eoqsrBgweJjIx0qGJCCCGEEEJUNYeC5TvvvJPDhw+zZMmSYvPMnTuXs2fP0qNHD4crJ4QQQgghRFVyKFieMGECBoOBhx56iE8//ZS4uDhrWnJyMl999RVPPfUUbm5uPPfccxVWWSGEEEIIIa4nRb16gOQy+uWXX3j00UfJz88vMl2v1/PDDz8waNCgclXwVpOeno6XlxdpaWl4enpWdXWEEEIIUQSj0cjFixeruhq3JX9/f/R6faXvp6wxmcOTkjzwwANs376dBx54AA8PD1RVRVVVnJ2d6devH5s3b5ZAWQghhBBC3NQcGme5UOPGjfnpp59QVZWkpCQsFgv+/v5oNOWaRVsIIYQQQogbQrmC5UKKouDv718RRQkhhBBCCHHDkCZgIYQQQgghiiHBshBCCCGEEMWQYFkIIYQQQohi3LLBcnp6Oi+++CLh4eEYDAYiIiJ46aWXyMzMvKZyFEUp9m/EiBGVU3khhBBCCHFDqJAb/G40WVlZREVFsWfPHnr27MmDDz7I7t27+eijj1i7di3r1q3D2dm5zOWFh4cXGRg3b9684iothBBCCCFuOLdksPzBBx+wZ88eXnnlFd577z3r+ldffZX333+fTz75hIkTJ5a5vIiICN56661KqKkQQgghhLiROTyD341KVVVq1KhBeno6Fy5cwM3NzZqWlZVFUFAQgYGBnDhxokzlKYpCVFQUa9asqZD6yQx+QgghxI1PZvCrOjfaDH7lbllOS0tj+/btJCYmEh4eTocOHcpbZLkcO3aMuLg4evXqZRMoA7i5udGxY0f++usvzpw5Q2hoaJnKTE1N5euvv+bixYv4+vrSsWNHmjRpUqZt8/LyyMvLsy6np6eX/WCEEEIIIUSVcvgGv4yMDEaPHk1gYCC9evXikUce4dtvv7Wmf/vtt4SEhLB169YKqWhZHTt2DIA6deoUmV64vjBfWezdu5cnnniC1157jSeffJKmTZty9913k5CQUOq27777Ll5eXta/sgboQgghhBCi6jkULOfk5NC1a1dmzpyJj48Pd999N1f35rjnnnuIj4/nt99+q4h6lllaWhoAXl5eRaYXNrMX5ivN+PHj2bRpExcvXiQ9PZ1NmzZx99138+eff3LPPfdgNptL3H7ixImkpaVZ/86cOXMNRyOEEEIIIaqSQ90wPv74Y3bv3s2DDz7I119/jZubGxqNbdwdFBREgwYNiI6Odqhi48ePt+m+UJpx48YV25pcHh999JHN8h133MEff/xB9+7dWbt2LUuWLOG+++4rdnuDwYDBYKjwegkhhBBCiMrnULC8YMECgoKC+O6770ocgq1u3bps27bNoYrNmDGDrKysMue///77qVOnjrVFubiW48I+w8W1PJeFRqNhzJgxrF27lo0bN5YYLAshhBBCiJuXQ8HyiRMn6NGjR6ljFbu6ujp8J+m1Th5SqLQ+yaX1aS4rf39/gGsK6IUQQgghxM3FoT7LWq0Wo9FYar6zZ8/ajUhR2erUqUNISAgbN260C2SzsrLYuHEjkZGR5b7RrvDGxYiIiHKVI4QQQgghblwOBcu1atVi7969mEymYvNkZmayb98+GjRo4HDlHKEoCqNHjyYzM5N33nnHJu2dd94hMzOTMWPG2KzPzs7m8OHDnD592mb9v//+W+SXgk2bNvH++++j1+t54IEHKv4ghBBCCCHEDcGhbhj9+/dn6tSpTJ06tdiZ7aZOnUpaWhr33ntveernkJdffpklS5bw/vvvs3v3blq2bMmuXbtYuXIlbdq04fnnn7fJv23bNrp162Y3+ci0adNYtmwZnTp1IjQ0FL1ez4EDB1i5ciWKovD5559Tq1at63twQgghhBDiunEoWH7hhReYNWsW77zzDnv27GHw4MEAxMfHs3jxYn7++Wd++eUXIiIiGDt2bIVWuCzc3NxYu3Ytb731FosWLSI6Oprg4GDGjx/P5MmTcXFxKVM5AwYMIDU1lb1797Jq1Sry8/MJCgpi6NChPP/887Rt27aSj0QIIYQQQlQlh6e7PnjwIP379+fkyZMoimKTpqoq4eHhLFu2jIYNG1ZIRW8VMt21EEIIceOT6a6rzi0z3XXDhg3Zv38/s2fPZvny5Zw8eRKLxUJoaCh33303jz/+OK6uro4WL4QQQgghRJVzuGVZOEZaloUQQogbn7QsV50brWXZodEwhBBCCCGEuB1IsCyEEEIIIUQxytRnuXv37g7vQFEU/vnnH4e3F0IIIYQQoqqUKVi+cuzha3X1SBlCCCGEEELcLMoULEdHR1d2PYQQQgghhLjhlClYjoqKqux6CCGEEEIIccORG/yEEEIIIYQohgTLQgghhBBCFMPhGfwAzp8/z5IlSzhy5Ajp6ekUNb+Joih899135dmNEEIIIYQQVcLhYHn69Om89NJLGI1G67rCYLlwBAxVVSVYFkIIIYQQNy2HumH8888/jBs3DmdnZ1599VXuuOMOAGbMmMH48eOJiIgA4Pnnn2fmzJkVVlkhhBBCCCGuJ4eC5f/9738oisJff/3Ff/7zH+rUqQPAmDFj+PDDDzl48CDDhw9n5syZdO7cuUIrLIQQQgghxPXiULC8bds2WrZsSbt27YpMNxgMfPnllzg7O/P222+Xq4JCCCGEEEJUFYeC5ZSUFGrVqmVd1uv1AOTk5FjXGQwGOnfuLFNdCyGEEEKIm5ZDwbKvry9ZWVnWZR8fHwBOnz5tk89sNpOUlFSO6gkhhBBCCFF1HAqWw8LCOHPmjHW5cePGqKrKH3/8YV2XmZnJ+vXrqVGjRvlrKYQQQgghRBVwaOi4qKgoPvnkE+Lj46lWrRp9+/bFzc2NSZMmceHCBcLCwpgzZw7JyckMHTq0ousshBBCCCHEdeFQsPzAAw+we/du9uzZQ69evfD19eXjjz9m7NixfPzxx0DBGMsRERFMmTKlQisshBBCCCHE9aKoRU2756Bdu3bxyy+/kJycTIMGDRg5ciReXl4VVfwtIT09HS8vL9LS0vD09Kzq6gghhBCiCEajkYsXL1Z1NW5L/v7+1sEjKlNZY7JyTXd9tZYtW9KyZcuKLFIIIYQQQogq49ANfkIIIYQQQtwOHAqWN2/ezKhRo9i0aVOxeTZu3MioUaPYtm2bw5UTQgghhBCiKjkULH/99df8+OOP1K1bt9g8devW5YcffuDbb791uHJCCCGEEEJUJYeC5U2bNtG8eXP8/f2LzRMQEECLFi1Yv369w5UTQgghhBCiKjkULJ87d46IiIhS84WHhxMXF+fILoQQQgghhKhyDgXLGo2G/Pz8UvMZjUZMJpMjuxBCCCGEEKLKORQsR0REsHnz5hIDYZPJxObNmwkPD3e4ckIIIYQQQlQlh4LlXr16kZCQwFtvvVVsnilTppCQkEDv3r0drZsQQgghhBBVyqEZ/OLi4mjcuDFpaWn079+fMWPGUL9+fQAOHz7MN998w9KlS/Hw8GD//v3UqFGjwit+s5IZ/IQQQogbn8zgV3VuiRn8QkJC+OWXXxg0aBBLlixh6dKlNumqquLh4cEvv/wigbIQQgghhLhpOTyD35133sn+/fsZN24c9erVw9nZGWdnZ+rWrcu4cePYv38/PXr0qMi6CiGEEEIIcV051LJcqEaNGnzyyScVVRchhBBCCCFuKA63LAshhBBCCHGrK1fL8tVUVeX7779nz549hIeHM2bMGNzc3CpyF0IIIYQQQlw3DrUsT5s2DV9fX6Kjo23W33vvvYwaNYrPPvuM8ePH07FjR3JyciqkokIIIYQQQlxvDgXLK1asQKvV0qVLF+u66Oholi5dSkBAAOPGjaNp06b8+++/zJ49u6LqKoQQQgghxHXlULB89OhRGjVqhFarta5buHAhiqIwf/58Pv74Y9atW4enpyc//PBDhVVWCCGEEEKI68mhYDkpKYmQkBCbdRs2bMDf359u3boB4OHhQceOHYmJiSl/LYUQQgghhKgCDgXLFouF3Nxc63JWVhYHDx6kY8eONvl8fHxITk4uXw2FEEIIIYSoIg4Fy2FhYezevdu6vHLlSsxms12wnJKSgq+vb/lqKIQQQgghRBVxKFju3bs3p0+f5qmnnmLJkiVMnDgRRVHo27evTb49e/YQFhZWIRUVQgghhBDienMoWJ44cSJBQUF89dVX3HfffRw9epSHH36Y+vXrW/Ps2rWLuLg4OnToUGGVFUIIIYQQ4npyaFKSoKAgdu3axddff018fDxt27Zl2LBhNnkOHDjAgAEDuO+++yqkokIIIYQQQlxviqqqalVX4naSnp6Ol5cXaWlpeHp6VnV1hBBCCFEEo9HIxYsXq7oatyV/f3/0en2l76esMZlD3TCEEEIIIYS4HVRYsDxy5Eh0Ood6dQghhBBCCHFDqtCWZenRIYQQQgghbiXSDUMIIYQQQohiSLAshBBCCCFEMSRYFkIIIYQQohgVFiz7+fnJbH1CCCGEEOKWUmHB8kcffURMTExFFSeEEEIIIUSVcyhYfvvtt1m6dGmp+X7//XfefvttR3YhhBBCCCFElXMoWH7rrbf47bffSs23dOlSpkyZ4sguhBBCCCGEqHKVeoOfxWJBUZTK3IUQQgghhBCVplKD5TNnzuDu7l6ZuxBCCCGEEKLSlHl+6u+//95m+fjx43brCplMJg4cOEB0dDR33HFH+WoohBBCCCFEFSlzsDxixAibLhUbN25k48aNxeZXVRWNRsOECRPKV0MhhBBCCCGqSJmD5UcffdQaLM+ZM4datWrRsWPHIvM6OTlRo0YNBg4cSJMmTSqmpkIIIYQQQlxnZQ6WZ8+ebX08Z84cOnXqxMyZMyujTkIIIYQQQtwQHLrBLyYmhg8//LCi61Jh9uzZw6RJk+jVqxcBAQEoikLXrl0dLm/79u306dMHb29v3NzcaN++PT///HPFVVgIIYQQQtyQytyyfKXw8PCKrkeF+u2333j33XdxcnKibt26XLx40eGyoqOj6dWrF87OzgwdOhQPDw8WLVrEkCFDOHPmDOPHj6/AmgshhBBCiBuJoqqqWlqm06dPA1C9enW0Wq11uazCwsIcq52DDhw4QF5eHk2aNCEpKYng4GCioqJYs2bNNZVjMpmoX78+Z8+eZcuWLTRv3hyAtLQ02rZtS2xsLEePHr2mLw/p6el4eXmRlpaGp6fnNdVHCCGEENeH0WgsV2ObcJy/vz96vb7S91PWmKxMLcsRERFoNBoOHjxI3bp1iYiIKPNkI4qiYDKZylbrCtKoUaMKKWf16tWcOHGCkSNHWgNlAC8vLyZNmsSIESOYM2cOb775ZoXsTwghhBBC3FjKFCyHhYWhKIo1yi9cvtUVtkT37NnTLq1Xr14ArF27tsQy8vLyyMvLsy6np6dXXAWFEEIIIUSlKlOwHBsbW+LyrerYsWMA1KlTxy4tKCgId3d3a57ivPvuu0yZMqVS6ieEEEIIISpXpU53fbNLS0sDCrpdFMXT09OapzgTJ04kLS3N+nfmzJkKr6cQQgghhKgcDo2GcT2MHz/epvtCacaNG1dkC3BVMxgMGAyGqq6GEEIIUeEsFgv5+flVXY1KYTQaMRqNVV2N21Jubi5ms7nCynNyckKjcbx9+IYNlmfMmEFWVlaZ899///0VHiwXtigX13qcnp6Oj49Phe5TCCGEuBnk5+cTExODxWKp6qpUClVVb9lju9FlZmZW6L1xGo2GyMhInJycHNreoWBZq9WWKZ9er8ff3582bdowcuRI+vfvX+Z9ZGZmOlK1ClUYfB87doxWrVrZpF24cIHMzEzatm1bFVUTQgghqoyqqpw/fx6tVktoaGi5Wu1uVBaL5bqP5iUK6HS6CntNWSwW4uLiOH/+vMMDVDgULJdhaGag4FtnXFwcS5YsYenSpYwaNYpvvvnGkV1WiaioKN59911WrlzJ0KFDbdL++usvax4hhBDidmIymcjOziYkJARXV9eqrk6lkGC56lRksAwQEBBAXFwcJpPJofGbHaqJxWLhlVdewc3NjQkTJrB7925SUlJITU1lz549vPTSS7i7u/PSSy9x+vRpZs+ejZ+fHzNnzrwhp4k2Go0cPnyYEydO2Ky/8847qVmzJj/++CN79uyxrk9LS+O///0vTk5OPProo9e5tkIIIUTVKuxP6ujP2kJcT4WvU0f7QTvUsjx37lw++ugjoqOj6dSpk01a06ZNadq0Kf3796dr1640bNiQ4cOHU6tWLTp37szMmTMZPHiwQ5Utq8OHD/Pee+8BkJOTY103YsQIa57Zs2dbH587d44GDRoQHh5uMyyeTqfj22+/pVevXnTp0sVmuutTp07x0UcfERERUanHIoQQQtyoboc5F8TNr7yv0zJNd321tm3b4ubmRnR0dIn5unXrRmZmJtu3bwegRYsWxMXFER8f71hty2jNmjV069atxDxXHnZsbCyRkZF2wXKhbdu2MXnyZDZt2oTRaKRJkya8+OKLDBky5JrrJtNdCyGEuNnl5uYSExNDZGQkzs7OVV2dSiHdMKpORXfDKO71WqHTXV/t0KFDDBgwoNR8ISEhLF261Lpcq1YtDh486Mgur0nXrl3L3K8aCqbzLil/27ZtWbFiRUVUTQghhBBC3EQcCtt1Oh379+8vNd+BAwfQ6S7H42azGTc3N0d2KYQQQghRYWJjY/H29rZbrygKqampAPTo0YO6devSpk0bGjRowN13383y5cuvb0UvqVu3Lnv37rVZ16NHD5YsWQLAO++8Q/Xq1Wnbti2NGjWiQ4cOTJ8+vcR+usuWLePpp5++5rosW7aMHj16FJl24MAB6tatC0BcXBzdu3e3pr3zzjvk5uZal6dMmcL8+fOvef9X6ty5MzExMeUqozQOtSy3a9eOVatW8fnnnxd7kr/44gv27dtHz549retOnz5NtWrVHKupEEIIIW5YO3bsYOfOnaXmCwwM5N5777VZ9+uvv5KQkFBk/latWtG6desKqaMjPvzwQ+uv6WvXrmXYsGF89tlnDBw4sMrqVJyhQ4cybdo0AE6ePMnIkSM5efIkn3zySZH533jjDX799ddKq09ISAirV6+2Lk+dOpVnn33W2hVi8uTJ5d7H+PHjmTx5Mt9//325yyqOQy3Lr7/+Ooqi8Nxzz9G5c2e+/PJLVqxYwZ9//slXX31FVFQUzz77LFqtltdeew2A+Ph49u7dS+fOnSv0AIQQQghR9fLz88nMzCz1r/DG+yvl5OQUm7+sMwQuWbKEBg0a0KxZM1555RX8/f2LvA+pPKKionj99df54IMP7NISExOpW7cuW7duBWDx4sW0bt26yOM9fPgwXbp0oXnz5gwePJg+ffpUeLBXs2ZNZsyYwddff13k5GobNmzA29ub8PBwoGD+iJ49e9K+fXuaN2/OuHHjrJOyGI1Gnn32WRo2bEjHjh1Zu3atTVnvvPMODRs2pH379jajnsXGxhIYGAhgbVzt3r07bdq0ISEhgdGjR/PZZ5+RnZ1NcHAwFy5csG771ltv8cILLwAF81307duXNm3a0LRpU/7v//7Pmq9v376sWLGi2AnkKoJDLcudOnVi3rx5PP7442zcuJFNmzbZpKuqiqurKzNmzLAGx/n5+Xz33Xe0b9++/LUWQgghxA3FyckJd3f3UvO5uLgUua64bcsyPF1CQgKjRo1i48aN1K9fn1mzZpGUlFTqdhkZGTRv3rzUfFdq27YtL7/8st36gIAAZs6cyfDhw5k7dy4TJkzgzz//LPJ4R40axRNPPMHw4cM5dOgQ7dq1s5vPoSgPP/ywTXlXD3l7tfr16+Pq6srRo0dp06aNTdq6dets1nl7e7N48WLc3d0xm80MGjSIhQsXMnjwYL799luOHj1qHUb3nnvusW63fPlyFi1axJYtW/Dw8GDkyJFF1uXzzz/n22+/ZfXq1XbdX1xdXRk4cCA//vgjL774IqqqMmfOHJYuXYrZbObBBx9k3rx51K9fn+zsbNq3b0+7du1o06YNer2eJk2asH79ept6VSSHp7seOnQoUVFRfPfdd6xdu5azZ88CUL16dbp06cJjjz1G9erVrflDQ0MZPnx4+WsshBBCiBtO69atHe4ucXW3jGu1ZcsWmjZtSv369QEYPnw4Y8eOLXU7Dw8Pm3kUoPRhxkoaEKBTp06MHDmSqKgoZs6cae27e6X09HT27t3LI488AkCDBg3o0KFDqXUF+OGHH2jWrJl1ubh+w2Wp79mzZ6lVq5Z12WKxMGnSJDZt2oSqqiQmJtKoUSMGDx5MdHQ0jzzyiPWLy/Dhw61D8EZHR3P//fdbR5MYPXq0XSNqWRQ+Zy+++CJr1qzBz8+PJk2acPDgQQ4cOGDzZSIjI4ODBw9ag/2goCBrHFoZHA6WAYKDg3n99dd5/fXXK6o+QgghhBA3rB07dtCoUaNi0/fs2UNAQMA1BW+VNV71kSNHyMnJoV69enZprq6u5OXlWZf/97//kZiYyIYNG3B2duall16yuRmvrPV19Fjat2+PxWJh+/btzJkzx9pCraoqvr6+dl9qrpSbm1tkC35FufUmcxdCCCHEbaV9+/bs27ePI0eOADBv3rwy93W+Fhs2bGDq1KlMmDChyPQvvviClJQUtm/fzjfffFNkC6unpydNmzblxx9/BAoCWkdaYksTGxvL2LFjefzxx4scQ7hJkyYcPXrUupySkkK1atVwdnbmwoULLF682JrWvXt3fvzxR4xGI/n5+cyZM8cmbdGiRWRkZKCqKt99912xdfLw8Cixb/Gjjz7KF198wfLly3nooYcAqFevHp6ensyaNcua7/jx4yQnJ1uXDx06ZNPiXtHK1bIMBT99REdHc+7cOaCgG0a3bt2kb7IQQgghrovAwEC+/fZbBg4ciMFgoEePHri7uxc5NNy1eumll5g6dSpZWVmEhYXxxRdf0LdvX7t8u3fv5tNPP2X9+vUEBgYye/ZsRowYwaZNm/Dz87PJO3PmTB5//HE+/vhjatWqRevWrfHy8ip3XX/66SfWr19PdnY2np6eDB06tNhRy/r06cN//vMfzGYzWq2WZ555hgcffJDmzZsTHBxsM+TbY489xoEDB2jWrBk+Pj507NiR3bt3A3D33XezY8cO2rVrh6enJ7169Sq2fs8//zx9+vTB1dWVZcuW2aU//PDD1K5dm/vuuw8fHx+gYLjiP/74g+eff55PPvkEs9mMv7+/9ctGbGwsZrO5UoNlh2bwg4Jh4B5++GHrt6HCYgqb3zt27Mi8efMICwuroKreGmQGPyGEEDe7G3EGv4yMDDw8PAD47bffmDhxIocOHXK4vMqcwS8zMxM3NzcURSEmJoaoqCg2btxIaGhopeyvOM899xxRUVEMGjTouu63NNcyg9+rr75K7dq1GT16dLF5qmQGv9TUVLp160ZMTAzOzs706tXL2kn85MmT/Pnnn2zYsIE777yTHTt2VMi3JSGEEEKI4kyfPp0FCxZgNpvx9PTkhx9+qOoqFWvz5s1MnDgRKJiw7cMPP7zugTIUjHP8119/Xff9VqSQkBBGjRpVqftwqGX5jTfe4D//+Q99+vTh66+/JiQkxCb9woULjBkzhuXLl/Paa6/x9ttvV1iFb3bSsiyEEOJmdyO2LBendevWdi3EjRo1KjWYrsyW5eI8/fTTbNu2zW79unXrKvUGthvNtbQsl0V5W5YdCpYbN25MYmIiMTExuLq6FpknOzubyMhIAgICyjQ19u1CgmUhhBA3u5spWHZUVQTLosCNFiw7VJPC/jXFBcpQMCRJVFRUpc/XLYQQQgghRGVxKFjWarUYjcZS85lMpgr9ZiCEEEIIIcT15FAkW6dOHdasWUNqamqxeZKTk4mOji5y9hohhBBCCCFuBg4Fyw888ABpaWn07duXAwcO2KX/+++/3HPPPaSnpzNkyJByV1IIIYQQQoiq4FCwPG7cOJo1a8bmzZtp1qwZbdq0YfDgwQwePJjWrVvTokULtmzZQrNmzXjuuecqus5CCCGEEOUSGxtb5KQliqKU+Mt5Zfj++++5//77bdatXbuWNm3aFLvNsmXLip1w5EazZMkStm7dWqa8e/fu5aeffipT3tzcXFq1alXirIAVwaFxll1cXFi9ejVPPvkkCxcuZOfOnezcudOartFoGDJkCJ9//vkte5esEEIIIQoM/mANSRl5lVa+n4eBn1/uWmnl34zeeOMNfv3116quRpn8/vvvNG3alHbt2pWad+/evfz+++/W6a5L4uzszLBhw5g2bVqlDlPs8HTXPj4+/PTTT5w5c4Z169bZTHfdpUuXKhlcWwghhBDXX1JGHvGpuVVahyVLlvDqq6/i5ORE7969+e6779ixYwcREREVUv57773Htm3bWLRoEfn5+URFRTFu3DgefPBBu7xvv/028+fPx8fHhx49ejB//nyOHj1aIfUA2LBhA97e3oSHhwNw7NgxxowZQ2ZmJhaLhX79+jFlyhRWr17NW2+9RW5uLvn5+YwbN46RI0cCMHr0aPR6PTExMZw8eZKuXbsyZswYJk2axOnTp+nfvz8ffvghUDB/xosvvsipU6fIycmxln+1rVu3Mm7cOMxmMyaTibFjxxIWFsYff/zBP//8w9y5c3nyySfp06cPjz76KOnp6eTm5hIVFcUnn3zCxYsXefvtt0lLS6N58+a0b9+er776iu3bt/PKK6+Qnp6O2Wxm0qRJPPDAAwAMHTqUFi1aMGXKFOss0hXN4WC5UGhoKA8//HBF1EUIIYQQNyE/D0OVlp+QkMCoUaPYuHEj9evXZ9asWSQlJZVabkZGBs2bNy9THV555RX69+/PJ598QmxsLC1btiwyUF6+fDm//vor27Ztw93dnccff7xM5V/d7SIzMxN3d/ci865bt84m75dffkmfPn14+eWXgYJBFgBatGhBdHQ0Wq2W5ORk2rVrR48ePahRowYABw4cYOXKlWg0Gpo1a0ZKSgrLly8nPz+f+vXrM3LkSBo2bMhjjz3GK6+8QpcuXTCZTAwcOJBFixbZTZP9wQcf8MILL1jvV0tJScHHx4d77rmHpk2bWrvm5ubmsnjxYtzd3TGbzQwaNIiFCxcyePBg3nzzTX7//XeWLFkCFMwa/fjjj7N8+XKCg4O5ePEiLVu2pEOHDlSvXp2goCBcXFw4cOAAjRs3LtO5vlblDpaFEEIIcXur6i4SW7ZsoWnTptSvXx+A4cOHM3bs2FK38/DwYM+ePTbrimudVBSF2bNn065dO7y9vdmwYUOR+aKjoxk0aBAeHh4AjBgxgrVr15Zal6ioKBYuXGhdXrt2LRMmTCgy79mzZ6lVq5Z1uXPnzrz66qtkZmbSuXNn7rzzTgCSkpJ44oknOHbsGDqdjqSkJA4cOGANlvv162ftLtu4cWN69OiBXq9Hr9fToEEDjh07Rnh4ONHR0SQkJFj3l5mZWWRLeVRUFP/97385fvw4Xbt2pWPHjkXW32KxMGnSJDZt2oSqqv/f3n2HRXWsfwD/LgssHWyo2EBFRUTKsoIUQcGI2Eu8toi9XEtMLCTmRmw/SwwmxpbYLvZ61YjBLmBBiijFKGqUEjSKBSlK3Z3fH8iRZXcRYVkWfD/Pw5Nw3pk5c95dYXaYcwbPnz+HtbU1RowYIVM2IiICjx49Qt++faWO37t3Dy1atAAANGvWDOnp6bU7WE5LS6vWSVq3bl2t+oQQQgghtS0tLQ0SiQQ5OTl48+ZNpe7LqomlAXp6eigoeL9GfMiQIXB2dsbFixexZcsWbNiwASdPnsSsWbPg4+ODQ4cOgcfjwcnJCfn575fLCATvZ+z5fL7U9WhoaEAsFqN0o+crV6588HrnzJmDAQMG4NKlS1i8eDE6d+6MDRs2yJRbv349nj9/jqtXr0JHRwcLFiyQ6ldZjDFYW1sjIiJC4Xnz8/NrdDvwSj0Nw9zcHBYWFlX6atu2bY11nhBCCCHE2dkZCQkJuHfvHgBg7969KCwsVOo5srOzMXbsWOzcuRNTpkzBxIkTuYFkWZ6enjh+/Dhyc3PBGMOuXbuU2g8AsLGxkZrZffDgAZo2bYqxY8di5cqViI6OBlCyhKFNmzbg8Xi4cuUKEhISPvpcBgYG8PDw4NYvA8CTJ0+Qnp4uU/bevXuwsLDApEmTsHDhQq4fhoaGyM7O5splZmaiadOm0NHRwdOnT3Hs2DEuZmRkJFXWxcUFycnJuHDhAncsLi6Oe33FYjEePnwIGxubj762yqrUzHLr1q1rbNE0IYQQQkh1mJqaYvv27Rg8eDAEAgF69+4NAwMDuY+Gq6pp06Zh1KhR8PT0hIeHB65evYrAwECZpRL9+vVDTEwMRCIRTExM4O7uDmNjY6X1AwB8fX3xf//3fxCLxeDz+Th+/Dj2798PbW1tSCQSbNy4EQCwYsUKzJkzBytXroStrS26detWpfPt2rULCxcuhL29PXg8HvT19bFp0yZuOUepLVu2ICwsDNra2uDz+VizZg0AYMyYMZg8eTJOnjyJ6dOnY9asWRg1ahTs7OzQvHlz9OrVi2ujZ8+e+Pnnn9G1a1e4uLjg119/xR9//IH58+dj3rx5KCoqQuvWrXHixAkAJTc7ikQiNGzYsErXVhk8Ju9jEakx2dnZMDY2RlZWFoyMjGq7O4QQQshHy8/PR3JyMiwsLNTmEbE5OTncOuETJ07g22+/xd27d6vcnkQiQXFxcbX6whjDwoULkZeXxw1glWXOnDnw8PCQucmuPtDU1ISGRuW2Ahk5ciQmTZqE3r17Kyyj6P1a2TEZ3eBHCCGEkDpvw4YNOHToEMRiMYyMjLBv375a68vEiRORmpqK/Px8dO7cWekDZQAICAjA2bNnld5uXVL62LmKBsrKQDPLKkYzy4QQQuo6dZxZVsTR0VFmhtja2vqDg+nqzCzLEx8fj8mTJ8scHzt2LL788kulnac++JiZ5cqgmWVCCCGEEAVu3LhR210AANja2iImJqa2u0GqQHnDdkIIIYQQQuoZGiwTQgghhBCiAA2WCSGEEEIIUYAGy4QQQgghhChAg2VCCCGEEEIUoMEyIYQQQj45KSkpcnf44/F4eP36tUr7snv3bgwfPlzmeEpKCnR1dSESibgvNzc3he1s3rxZaltqdbR7924kJSVx3wcHB8vsglgV7u7uSE5OrnY78tCj4wghhBBSLelrBkKc87zG2ucbNkFL/5M11r46MzQ0rNQj5/Ly8rB+/XrcvHlTBb2quj179sDY2BidOnUCAAwYMAADBgyodrvz5s1DQEAAdu/eXe22yqOZZUIIIYRUizjnOcSvn9bcVyUG4r///jusrKxga2sLf39/NG7cGCkpKUq7xtWrV2Po0KFgjKGgoADOzs44cOCA3LLLli2DlZUVXFxcEBAQgA4dOiitH4ocO3YMLi4u0NfX5479+OOPcHBwgKOjI9zd3fH27VsAQGBgIOzs7ODg4AA/Pz9kZWUBAJYvX47Ro0djyJAhsLa2xuDBg/Hnn3+iX79+sLa2xhdffAGJRAIAmDx5MqZOnQoPDw9YW1tj0qRJyMvLA1Cy3feMGTPg6uoKoVCIGTNmoLCwEDt37kRsbCwWLFgAkUiE06dPS82qh4eHw97eHjNnzoStrS2sra2lnpP922+/oUOHDnBwcMDy5cvB4/G4WL9+/XD69GnuWpSJBsuEEEIIqRa+YRPwTZrV3JdhkwrPn5GRgYkTJ+L48eOIj49Hp06d8PLlyw/2OycnB3Z2dlJfivj7+6OoqAg//fQTFixYAAcHB4waNUqmXEhICI4fP47o6Ghcu3YNT548+WA/PtTHsssw/Pz85Ja7fPkyRCIR9/2ePXtw/PhxhIaG4saNGzh58iQEAgHOnDmDXbt2ISwsDDdv3oSenh6+++47rl5sbCx27tyJ27dvIzc3F9OnT8fBgwcRHx+PpKQknDlzhisbHR2NU6dOISEhAa9evcIvv/wCAFi4cCFcXV1x7do13LhxA4wxbNiwARMnToRQKMTatWsRExODvn37ylzHvXv3MG7cOMTHx2P27Nlc327fvo0lS5bg8uXLuHnzpszuilpaWrCxscGVK1eqnmwFaBkGIYQQQqqltpdIREZGomvXrtyf9v38/DB9+vQP1jM0NERcXJzUsbKzleWPBwUFwcnJCSYmJrh69arccqGhoRg2bBgMDQ0BAOPHj0d4ePhHXI1sHyuzDCM9PR2fffYZ931ISAimTJkCY2NjAECDBg0AAJcuXcLnn3/OrdeeNm2a1KDf29ubK2tnZweBQMBdi52dHf766y+u7PDhw6Wuc/PmzfD390dwcDCioqKwfv16ACVLRCq7fXW7du3g5OQEAOjevTt+/PFHrt8+Pj5o1qwZAGDKlClYtmyZVN1mzZohPT29Uuf5GDRYJoQQQgiphLS0NEgkEuTk5ODNmzfQ0dH5YB1Fg29l09PTQ35+/kfXK9+/stfE5/Nlvi8/oysPYwwHDx6s0vKTyp5PXl7z8/Ohq6v70ef8EFqGQQghhJA6zdnZGQkJCbh37x4AYO/evSgsLFTqObKzszF27Fjs3LkTU6ZMwcSJE8EYkynn6emJ48ePIzc3F4wx7Nq1S6n9UMTGxgb379/nvu/fvz+2bdvGreF9/fo1xGIxevXqhaNHjyI7OxsAsG3bNnh7e1fpnMeOHUNubi7EYjF2794NLy8vAMDAgQMRGBjIDXQzMzO5GWlDQ0Pu3B+jZ8+eOHv2LDIyMgAAO3bskClz9+5d2NraVulaKkKDZUIIIYTUaaampti+fTsGDx4MOzs7JCYmwsDAQO6j4aqqdLmCp6cn5s2bBx6Ph8DAQJly/fr1w4ABAyASieDi4gJjY2NuKURFzp8/j7Zt23JfCxYsACC7ZlkkEiEnJ0em/tChQ3H+/Hnu+zFjxmDIkCHw8PCAo6MjBg0ahIKCAvj4+GDcuHHo0aMHHBwckJOTgxUrVlQpJ46Ojujfvz+6du0KExMTzJ49GwCwdu1a6OjoQCQSQSgUwsfHB6mpqQCASZMmYc2aNdwNfpVlY2OD//znP3B1dYWDgwPy8/Ol8pqSkgKxWFwjg2Uek/exiNSY7OxsGBsbIysrC0ZGRrXdHUIIIeSj5efnIzk5GRYWFpVaiqAKOTk53PrZEydO4Ntvv8Xdu3er3J5EIqnUkoOK+sIYw8KFC5GXl4eNGzdWuS+VNWjQIHz//fdwdHSs8XNNnjwZXbt2xZw5c5Tetqamptw1zmVf4/Xr1+PMmTPcgPubb75B+/btMXnyZJl6it6vlR2T0ZplQgghhNR5GzZswKFDhyAWi2FkZIR9+/bVWl8mTpyI1NRU5Ofno3PnzioZKAPAunXrpDb8qG+++eYbXLt2DUVFRTAzM8Nvv/3GxczMzDBx4sQaOS/NLKsYzSwTQgip69RxZlkRR0dHmRlia2vrDw6mqzOzLE98fLzcWc+xY8fiyy+/VNp56gNFM8tVRTPLhBBCCCEKlN3UojbZ2tpW6hFwRP3QDX6EEEIIIYQoQINlQgghhBBCFKDBMiGEEEIIIQrQYJkQQgghnwRfX19u45KgoCCpJ0cEBQVh8ODBcuvFxsZizJgxSutHSkoKtm7dWunyy5cvr9LufKq2e/duDB8+XOpYeHg4RCIR971AIICDgwOEQiGsra3xxRdfVOsRf6pAg2VCCCGEfBJCQkLQsWNHALKD5YoIhUKlPoouNTUV27Ztq3T5FStW1InBcmVdunQJsbGxSExMhLu7Ozw9PZGcnFzb3VKIBsuEEEIIqbYne/bgRp8+uNGnD7LKPYEi//FjLvZo9WqZune//JKLV8XWrVsxdepUAMCdO3fA4/Fw7tw5AMCyZcuwbNkyAIC5uTni4uKwfft23LhxA1999RXs7OwQEhICAMjNzcWoUaNgY2ODbt264dGjRwCkZ0dTUlJgamqKZcuWwdnZGVZWVgp3osvLy8OYMWNga2sLR0dH+Pr6AgBmzZqF+/fvQyQSYejQoQAAf39/uLi4QCQSwcvLi5sBnzlzJgCgV69eEIlEyMjIQE5ODmbMmAFXV1cIhULMmDFD4fbe+/btg5ubG5ycnODl5YWEhAS55WJjY+Hh4QGhUAhXV1dERER89PV+LA0NDUydOhW9e/eWemayuqHBMiGEEEKqrfjNGxRmZKAwIwOScgM3JhZzsWI5WzUXZWZy8arw9vbGhQsXAJRsG929e3ep7729vaXKT548GY6Ojvjpp58QFxfHDWJjYmKwcuVKJCYmwsvLCz/++KPc82VlZcHGxgaRkZH4+eefua2pyzt37hxev36N+Ph43LhxA3v37gUAbNy4ER06dEBMTAyOHTsGAJg/fz4iIiIQExODadOmYd68eQCATZs2ASiZjY2JiYGpqSkWLlwIV1dXXLt2DTdu3ABjDBs2bJA5f0REBA4dOoSLFy8iKioKS5cuxbhx42TKFRYW4l//+he+++47xMbG4ocffsDIkSORm5v7UdcLvP9gUfo1ffp0hWVLiUQi3Llz54Plags9Z5kQQggh1aaprw9tU1MAgIa2tlSMx+dzMc132xWXpdWgARevirZt2wIAHj16hAsXLmDVqlWYN28ecnNzcefOHXTr1q1S7XTv3h0WFhYAAGdnZ7kDUADQ0dHh1jc7OztzM9Dl2djY4N69e5g9ezZ69OgBHx8fhee+ePEiNm/ejJycHEgkEmRmZiosGxwcjKioKKxfvx5AyQy2vE08goODkZiYCDc3N+5YZmYm8vLyoKuryx27f/8+NDQ08NlnnwEAXF1dYWpqivj4eLRo0aLS1wsAHh4eOHr0KPd9eHg45s+fr7A8AKj7/ng0WCaEEEJItZl98QXMvvhCbkynRQs4nj2rsK7Vu0FfdXh7e+P06dN48OABPDw8wBjD//73P3Tv3h2ampUb7pTd3Y3P50MsFsstJxAIwOPxZMrt3buXG8DOmjULfn5+iIuLQ2hoKC5duoRFixYhOjpapr20tDTMnTsX165dQ7t27biZbUUYYzh48CA6dOhQ4fUwxjB27FgsX7684guXo/T6KrpeZYmNjYW1tbVS21QmWoZBCCGEkDrP29sba9eu5WaRe/XqhYCAAJklGKWMjIyQlZWl1D6MHTsWMTExiImJgZ+fH9LT08Hj8TBgwACsWbMGjDGkp6fLnDs7OxtaWlpo3rw5GGPYvHmzVLuGhoZS5QcOHIjAwEBuO+7MzEz89ddfMv3p378/Dhw4gLS0NAAlW3jHxsbKlOvQoQMkEgm3dOX69et49uwZbG1tq5+UCkgkEuzYsQPnzp3j1pyrIxosE0IIIaTO8/LyQlpaGjc47t27N1JTUxXO0E6dOhUrV66UusFP2W7fvg1PT084OjqiW7duGD16NGxsbGBjY4POnTvD3t4eQ4cORZcuXfD555/Dzs4OLi4uaN26tVQ7c+fOha+vL3eD39q1a6GjowORSAShUAgfHx+kpqbKnN/NzQ0rV67EiBEj4OjoCDs7Oxw+fFimnLa2Ng4dOoTly5dDKBRi/vz5OHDgAAwMDGokL7169YJQKISNjQ1CQ0MRFhbGLX9RRzym7gtF6pns7GwYGxsjKysLRkZGtd0dQggh5KPl5+cjOTkZFhYWUksX6hOJRMLN3BLV0tTUlLsGu6oUvV8rOyajmWVCCCGEEEIUoMEyIYQQQgghCtBgmRBCCCGEEAVosEwIIYQQQogCNFgmhBBCCCFEgXo5WI6Li8OiRYvQp08fNGnSBDweD56enlVqy9zcHDweT+5XVdskhBBCCCF1Q73cwe/EiRNYtWoVtLW10aFDB7x48aJa7RkbG2Pu3Lkyx83NzavVLiGEEEIIUW/1cmb5888/R2xsLHJzc3H+/Plqt2diYoIlS5bIfI0fP776nSWEEEKISvj6+uLevXsAgKCgICQlJXGxoKAgDB48WG692NhYjBkzRmn9SElJwdatWytdfvny5cjPz1fa+WvK7t27MXz4cKlj4eHhEIlEtdQj5aiXM8vqvL+4qjGxGOzdQ9V5fD54mu9fcsYYWGFhyTc8HjS0taXqSoqKAImkJKylBV6ZB4RX1C4ASAoKqtauRAJWVFTyjYYGNLS0pOsWFgLv9tHREAikr7W4GOzdfvU8TU3w+Hz51/qBdnna2uDxePLbrSiH8tote63l21XDHEq1Wz6HtfXafCiHpe1qaMjmsOz7u1y7rLgYpXsy8TQ1pduVSKTbLdPf0rqlyp+Tvetr6Xml2i23B1TZGCGk5pXdqS8oKAgmJibo1KnTB+sJhULs27dPaf1ITU3Ftm3bKr3F84oVKzB79ux6uwGMuquXM8vKVlBQgKCgIKxcuRIbN25EVFTUR9XNzs6W+lKl5yEhiHR2RqSzM54eOyYTL439OWWKTOyvgAAuXvDkiVTsdWQkF0vfsUOm7g0fH0Q6OyOu3CdMAEhdv56rm3v7tlTsTVISF0sJDJSpGz9yJCKdnRHzbjvTsh7v2sXVzbx6VSpW+PQpF3vw3Xcyde/MmMHFWbkdmzJOnuRiGadOScVYYSEXu/Pvf8u0e//bb7l44fPnUrHMy5e52OPdu2XqRvfqhUhnZ8SPHi0TS167lqv75v59qVhOQgIXS/3lF5m6t4YORaSzM2707SsTS9++naubFR0tFcv/+28u9nDpUpm6tydP5uLlPT16lIu9OHtWKibOzeViSXKWO92bP5+LF79+LRV7eeECIp2cEOnkhH8OHpSpG+XqikgnJyT6+cnEHq5Ygchu3RDZrRvykpOlYtmxsVwsbfNmmbqxvr64LhIhdsAAmVjqhg24LhTiulCI7NhYqdjbhw9x3cEB1x0c8HDZMpm68SNHIsLeXm4On+zZgwihEBFCIV6U+4tZ4atXuC4S4bpIhKR582Tq3pk9G9ednHDdyQnFublSseenTyOye3dEdu+Op//7n1SMSSSIdHVFpKsr/pw2Tabdv5YtQ5S7O6Lc3ZH/+LFU7HVkJKJ69EBUjx54HBQkU/fmwIGI9vREvJzZurRNmxDdsyeie/ZE7p9/SsXe3LuH6F69EN2rF1LXr5epmzhhAmK8vHBz4ECZ2JP9+xHj7Y0Yb2+8vn5dKlbw7BkX+0vOa5P09ddcXFL6ofGd53/8wcWenz4tFZMUFCDGywsxXl5yX5u/lizhrkfmZ8TVq1zsn/37ZerG+voiumdPJE6cKBNL+eknLodvHjyQiuXcvo1oT09Ee3oibdMmmbrxo0Yh2tMTt4YMkYml79yJaA8PJHzxxfsP9O9EBD9D4PQ7CJx+B/evSG+9nJlRiMAZdxE4/Q5+//mOTLv7Vz3k6hZlZUnFmFiM3KQk5CYlIe/vv2Xq5qen45dlyzBhxAhIiotx584d8Hg8nDt3DsXZ2fjP7Nn4z+zZKHz1Cubm5oiLi8P27dtx48YNzJ01C12trHD8v/8FAOTm5mLUqFGwsbGByN4e98LDUfjoEcIuXuRmR1NSUmDapAkC5s6Fk709rDp2xOlyr3tRaioKHz5E9r17GDNmDGxtbeHo6AhfX1+IX73CzGnTcP/ePYgcHTF06FAAgL+/P1y6d4dj167o6eKCu5GRAICZM2cCKNki2tHODo+jo/EyMREzZsyAq6srhEIhZsyYgbznz1H48CEKHz6E5M0bri/79u2Dm6srutnZoaeLC25euiSTw+J//kHk77/D3ckJQqEQrq6uiIiIAAAk//knTBs3RsDcuXAWiWBlZSV1vaXnLCr3MwAAijMyUPzkCVhBgczvVvb2LVdXnJkpU1ed1MuZZWV7+vQpJkyYIHVMJBLhwIEDaNeuXYV1V61ahaVyBhaEkE+cnFnl8jPPMrGys9bSwfe/iOSUYcXF3Ox9+dlsVlwMSemfd9/Nppclefu25L9y/gQsyc+HuHTwXa7vrLgY4pycknLlBlUAUJyTg+KsLGjK2WJW/PYt98GIlesTE4tR/O4XqzgvT7bdrCwUvXr1/q8KZfubl4eily9L/r98XCLhYuJyHyi4dt/FZdotKHjfrpxrLXr1qqQNOZMlxbm53PWUf/1ZUdH7a5WT/6KsLEjevpXbbtkcln9PsOJiFL8bkMprt/S1Kf8XFaDk+oqzs8HT05OJFbyVIDfnXRtF5a5FAuRmlxwrkH3Z8Cb3fV25Sq9B3vtbIkFPJyf8tG0bAOD8+fPo3r07Lly4gJ5OTrgUEYFlX38t9R6dPHky9u7di2nDhmGAlxc0dHRw+PJlxMTEIC4uDhYWFpg/cyZ+2rYNvyxZInPOrOxsdOnQAd/NmoWLcXFYsGAB+paZgGASCcAYzl2+jNevXyM+Ph4A8OrVK4AxrF+8GP6rVyP6yhXwdHUBAPPnz8fq5ctRlJaGoyEhmL94Mf44dw6bNm3C9u3bcenSJehnZ4MVFmL2kiVw9/HBli1bwBjDjBkzsGnrVsz517/edaDkWiMiInDo0CFcPH8evCdPcC02FhPnzEFcuYmqgoICjJkzBxuWLoXvuHG4dv06Ro4ciTt3Sj7YZOXkoEuHDghYuhTnIyMxb96899dbmtd3/y277IIVF+NNTg709fRkf0bIqauuaLD8ARMmTIC7uzu6dOkCAwMD3L9/H+vWrcOePXvg5eWFxMREGBoaKqz/7bff4uuvv+a+z87ORqtWrVTRdQCAVqNGMHJ0BABoN2kiEy+N6ckZ9OtaWHDx8ssANI2NuZigeXOZuoa2thC/eQNtU1OZmE6rVlxdvr6+VIyvp8fFdFq3lqlr0KULtBo1Av/dD5eyBM2acXU1jY2lYjxt7QqvVb9TJ0DBPvRajRu/z2GjRtJBDQ0upt+xo0xdvXbtuF9KGuX+XK/ZoEGFOTSyt4ekoAACMzOZmG6bNu9zWC4XfAOD9zmU814ztLGBoHlzaMp53wrMzN63W24Qo6Gjw8V0LSxk6up36iSz/KKUtqkpV1erYUOpGI/Pf//aWFrK1NVr1w7id7Mk5Zc8aDVoACOhsOQcTZvK1DVycAATi+W+l3Rat4aRgwN3bWXxDQxgZG8PAHLzb9ClC4pev4ZWgwYyMUHz5jC0teXakWpXIIBh164l52/RQqauvqUlNAQCmfcKAGg3bgyDd0vMyg8weXw+9K2sSs4vp12dli1RXPr+LPc+1zQ05PJe/t8NAOi++/cir11tU1PuvVD+tdHQ1YXuu5ugNU1M5PfJyEjue1/TxIR77/LK/ezhaWlB0LKlwna1TU0hKSwEX85gjm9gwL2eMu9VPp+LyXtdtRo3lvteAEp+bpXGZM7L43G5027cWLbdhg25OK/ca6Ohq/v+WuX8e9Vp0QKS/HwI5Lz3tRo0eJ/D8q+NQMDFtOS9Ni1agMfny30/aBkbQ6dNG7BGjWQ+8An0NGDw7q2pKZA+J08DMDDiAWDQ0ZP9WatvyIeBUdG7srLx0t9BvHLLqUqPlU5cJScn48KFC1i1ahXmzZuHN199hXuPHqGbUCh38K+hpQUNbW2u3e7du8Pi3XvauVs3bPrtN7nn1NHRwaB+/cDj8eDUrRsePXok3SdtbUAsRtcuXXBvzRrMnj0bPXr0gI+PT8mHUi2tkvyVudaLFy9i86ZNyM7MhEQiQaacD0GlfTl18SJi7tzB+nd/XcnLy4NG9+7v/728azc4OBiJiYlw8/DgPjC/yspCXl4edMv87niQmgoNPh+9e/UCeDy4urrC1NQU8fHxMGvYEDoCAQb16wdoaMDZ2VnqennlXhsPDw8cPXoUACB++RJhly7Bf8UKmQ/qPA2N93XlvDbqhMcqmsqoRfPmzUOBnE/oinz55ZewlPOL9unTp2jevDk8PDwQFhamtP6NGzcOe/bsQWBgoNRg+EOys7NhbGyMrKwsGMmZUSGEEELUXX5+PpKTk2FhYaE262inTp0KW1tbbNiwAUlJSRAKhZgzZw6OHDmCU++W0Jmbm+PEiROws7ODp6cn5s6dy93UFxQUhBMnTuDEiRMAgJMnTyIwMBDnz59HeHg45s+fj5iYGKSkpKBbt27IyMgAULJ0o1GjRigoKMDevXu5AeysWbPg5+eH3NxchIaG4tKlSzh16hSio6ORkJDAtQcAaWlp6NatG65du4Z27dohMTERXl5e3DkEAgGePXsGk3cfblq0aIHQ0FB06NChwpx888030NLSwvLlyyssd/v2bQwdOhT3yyzrE4lE+Pnnn9GiRQuF11ve7t27cfLkSW6wDEAqd5WlqakJDQUTWFWh6P1a2TGZ2s4s//bbb3hTZs3NhwwfPlzuYLmmTJs2DXv27MG1a9c+arBMCCGEEOXz9vbGwoUL0aNHDwAla3wDAgLkPvoVAIyMjJBVbn10dY0dOxZjx47lvk9PT0eDBg0wYMAA9OnTB8HBwUhPT5c5d3Z2NrS0tNC8eXMwxrC53L0ShoaGyMrK4gbLAwcORGBgIDZt2gRNTU1kZmbi5cuXaN++vVS9/v37Y/z48ZgyZQpat24NiUSCW7duQfjuL3KlOnToAIlEggsXLsDb2xvXr1/Hs2fPYGtrW+3H79YHanuDX25ubsld8pX8UvUGIY3f/TntYwb0hBBCCKkZXl5eSEtLg/e7G8B79+6N1NRUeHl5yS0/depUrFy5EnZ2dlJPyVCm27dvw9PTE46OjujWrRtGjx4NGxsb2NjYoHPnzrC3t8fQoUPRpUsXfP7557Czs4OLiwtal1s6NnfuXPj6+kIkEiEjIwNr166Fjo4ORCIRhEIhfHx8kJqaKnN+Nzc3rFy5EiNGjICjoyPs7Oxw+PBhmXLa2to4dOgQli9fDqFQiPnz5+PAgQMwKLeU7FOltsswlKWmlmHs3r0bfn5+mDZtGn799ddK16NlGIQQQuo6dVyGoWwSiQTF5Z7gQFRD3ZZhqO3MsioVFRUhKSkJDx8+lDqelJSEt+/uBC9/3N/fHwAwWs5jvQghhBBCSP2gtmuWqyMpKQmrV68GUHKHaOmxsjvuBZV57ufjx49hZWWFNm3aICUlhTt+8OBBrFu3Dj169ECbNm2gr6+P+/fvIyQkBEVFRfj222+5tVGEEEIIIaT+qZeD5adPn2LXrl1Sx549eyZ1LEjOQ/LL69mzJ+7evYtbt27hypUrePv2LRo3bgxfX1/8+9//xmeffabsrhNCCCF1Rj1fyUnqieq+T+v9mmV1Q2uWCSGE1HVisRgPHjyAnp4emjRpUi+3bqc1y7VHmWuWGWN4/vw53r59C0tLS/DLPNO5zj86jhBCCCHqic/no2XLlkhPT5davlifMMYgUbRrJqlRGhoaSv0AxuPx0LJlS6mB8segwTIhhBBCPpqBgQEsLS1RVFRU212pEUVFRch8t904Ua0GDRpAS87OiVWlpaVV5YEyQINlQgghhFQRn8+v1iBEnfH5fKUO2Ejl6ejoqFXu6dFxhBBCCCGEKECDZUIIIYQQQhSgZRgqVvrwkezs7FruCSGEEEIUKSoqQk5OTm1345Okra2tkmUYpWOxDz0YjgbLKlb6D69Vq1a13BNCCCGEEJKTkwNjY2OFcXrOsopJJBI8efIEhoaGch+LIhKJEBMTU+Xj5b/Pzs5Gq1at8Pfff6vsuc6K+lpT9StTvqIyH5PzDx2ri/n+2Daqm++K4pTzqpelnCu3jZr6uaIoVh9zri4/yxXFKOeq/f2p6Hht5pwxhpycHJiZmVX4XGeaWVYxDQ0NtGzZUmGcz+fLfYNU9riickZGRir7x66oDzVVvzLlKyrzMTmv7LG6lO+PbaO6+a4oTjmvelnKuXLbqKmfK4pi9THn6vKzXFGMcq7a35+Kjtd2ziuaUS5FN/ipmZkzZ1bruKJyqlTdPnxs/cqUr6jMx+S8ssdUSRnn/5g2qpvviuKU86qXpZwrt42a+rmiKFYfc64uP8sVxSjnqv39qeh4bee8MmgZRj1H22urFuVb9Sjnqkc5Vz3KuepRzlVPXXNOM8v1nEAgQEBAAAQCQW135ZNA+VY9yrnqUc5Vj3KuepRz1VPXnNPMMiGEEEIIIQrQzDIhhBBCCCEK0GCZEEIIIYQQBWiwTAghhBBCiAI0WCaEEEIIIUQBGiyTCuXk5GD69OkwMzODvr4+7O3tcfTo0druVr1mbm4OHo8n87V27dra7lq9lZaWhhEjRqBBgwbQ19eHSCTC48ePa7tb9daSJUtk3t+Ojo613a1PxowZM8Dj8bBx48ba7kq99tNPP8Ha2hoGBgYwMTFBr169EBUVVdvdqtdWrVoFR0dHGBoaomnTphgxYgRSUlKq3S4NlkmFvvrqK4SFheHw4cNITEzEiBEjMHLkSCQkJNR21+qtmJgY/PPPP9zX/v37AQBDhw6t5Z7VTy9fvoSbmxtMTExw4cIFJCQkYPHixWr36KL6xtbWVup9fvbs2dru0ifh1KlTuH79OszMzGq7K/VemzZtsG7dOsTHxyMiIgLt27dHnz598PLly9ruWr0VHh6O2bNnIyoqCmfOnMGrV6/Qt29fFBcXV6tdenQcqVCXLl0wduxYfPPNN9yxRo0aITAwEOPHj6+9jn1CxowZg/T0dISHh9d2V+qlhQsXIioqivKrQkuWLMGpU6dw48aN2u7KJ+XZs2dwdHRESEgIBgwYgPnz52PWrFm13a1PRumGG2FhYfDw8Kjt7nwSkpOT0bZtW8THx6Nr165VbodmltXQ3r17MW3aNDg6OkIgEIDH4yEoKKjCOjExMfD19YWJiQn09fXh7OyMw4cPV7svLi4u+P333/H06VMwxnDkyBEUFBTUu3/o6pTzsrKysnD8+HFMmDBBqe3WNnXKd3BwMBwcHDBs2DCYmppCJBLh2LFj1W5X3ahTzgHg7t27aN68Odq3b48JEybg6dOnSmlXnahbzidMmIA5c+bAxsZGKe2pI3XLeanCwkJs3boVDRo0qHf5V9ecAyW/QwGgYcOG1WuIEbXTpk0bBoA1btyY+////ve/CstfunSJaWlpMUNDQzZlyhT29ddfc/V+/PHHavUlLy+PjRo1igFgmpqazMjIiJ09e7Zabaojdcp5WVu2bGEGBgYsNzdXaW2qA3XKt0AgYAKBgC1ZsoTdvHmTrV27lmloaLDw8PBqtatu1CnnISEh7PDhwywhIYGdPn2aiUQiZmVlxfLz86vVrrpRp5xv2LCB9ejRg4nFYq5vGzZsqFab6kidcs4YY5cvX2b6+vpMQ0ODmZmZsRs3blS7TXWjbjkvVVxczLy8vJivr2+126LBsho6f/48S0lJYYwxtmrVqgrfeEVFRaxdu3ZMIBCwW7duccdfv37NOnTowLS1tbm2Svn7+zMAFX6VWr16NevcuTM7ffo0i4uLY8uXL2cmJibszz//VPp11yZ1ynlZIpGITZw4USnXqE7UKd9aWlrMzc1Nqv7AgQPZmDFjlHOxakKdcl5eRkYG09HRYUePHq32daoTdcn53bt3mampKUtOTubq1tfBsrrkvNTbt2/ZgwcPWGRkJJs0aRJr27Yte/78uVKvubapW84ZY0wikbDJkyczS0tLlpGRUe1rpMGymvvQG+/s2bMMAJswYYJMLCgoiAFgS5culTqekZHB7t69W+EXYyX/yLW0tNiZM2ek6nt7e7OZM2cq5wLVUG3mvKzbt28zAOzKlStKuS51Vdv5btWqFZs0aZJUfX9/f+bu7l79i1NTtZ1zeTp16sTWrl1b5WtSd7WZ8//+97+Mx+MxPp/PfQFgGhoazNbWVtmXqjbU8X3evn179sMPP1T5mtSdOuRcIpGw6dOnM3Nzc5aWlqaU69KUvziD1BVhYWEAgM8++0wm1qdPHwCQuXGpSZMmaNKkyQfbLioqQlFREfh8vtRxPp8PiURSxR7XfTWZ87J27twJS0tLuLm5Va2j9URN59vFxQV//fWX1LH79++jTZs2Veht/aCq93ipzMxMpKamwtzcvEr164OazPngwYNlHs3Xp08fjB8/vt7dD/ExVP0+BwDGGAoKCqpcv66r6ZwzxjBz5kz88ccfCA8PR6tWrarX4XdosFzHPXjwAABgaWkpE2vWrBkMDAy4Mh/LyMgI7u7umD9/PjZu3AgzMzOcPHkS58+fR0hISLX6XZfVZM5LFRcXY+/evZg7d2612qkPajrfX331FVxdXREYGIhBgwbhwoULCA4O/qSfjlHTOV+wYAEGDBiA1q1bIz09HYsWLUKLFi3g6+tb5TbruprMuYmJCUxMTKSOaWlpcTdYfqpq+n3u7++PgQMHomXLlnj16hU2b96M9PR0DBs2rMpt1nU1nfOZM2fiwIEDCA4Ohq6uLnfjcMOGDaGtrV3ldmmwXMeV3ulpbGwsN25kZMSVqYqDBw/C398fn3/+ObKystC+fXsEBQVxnwA/RTWdcwD4448/8OLFC4wbN65a7dQHNZ1vJycnHDlyBN999x3+85//oEOHDjhy5AhcXFyq3GZdV9M5//vvvzFy5Ei8ePECTZs2hYeHB/bs2QM9Pb0qt1nXqeLnCpFW0zl/8uQJRo4ciYyMDDRs2BAikQhXrlyBlZVVldus62o651u2bAEAuLu7Sx0PDQ2Fp6dnldulwTKpkJmZGfbs2VPb3fjkDBo0CGKxuLa78ckYMmQIhgwZUtvd+GQcPHiwtrvwyVPGrmakYvS7U/VYDW0dQs9ZruNKP50p+iRW+hB0ojyUc9WifKse5Vz1KOeqRzlXvbqacxos13Gl637krfF5+vQpcnNz5a4NIlVHOVctyrfqUc5Vj3KuepRz1aurOafBch1XupPeuXPnZGJnz56VKkOUg3KuWpRv1aOcqx7lXPUo56pXV3NOg+U6zsvLC23btsX+/fsRFxfHHc/KysLKlSuhra1NN4kpGeVctSjfqkc5Vz3KuepRzlWvruacx2pqNTSpsu3bt+Pq1asAgMTERNy8eROurq7cI37c3NwwefJkrnxoaCj69OkDHR0djBw5EoaGhvjf//6H1NRU/Pjjj5g3b16tXEddQjlXLcq36lHOVY9yrnqUc9X7JHKulK1NiFL5+flVuK2jn5+fTJ2oqCjm4+PDjIyMmK6uLuvWrRs7ePCg6jtfR1HOVYvyrXqUc9WjnKse5Vz1PoWc08wyIYQQQgghCtCaZUIIIYQQQhSgwTIhhBBCCCEK0GCZEEIIIYQQBWiwTAghhBBCiAI0WCaEEEIIIUQBGiwTQgghhBCiAA2WCSGEEEIIUYAGy4QQQgghhChAg2VCCCGEEEIUoMEyIYTUIykpKeDxeDA3N1fJ+RhjWLt2Lbp06QJdXV3weDzweDwuXlBQgEWLFsHS0hICgUClfSOEEGWgwTIhhFTA3NwcPB4PKSkpSm13yZIl4PF4WLJkiVLbVbUtW7Zg4cKFSE9PR9++feHn5wc/Pz8u/v3332PVqlXIycnBoEGD4Ofnh+HDh6u0j/Ul14SQ2qFZ2x0ghBBSdx0+fBgAcOTIEfTu3Vth/MqVK7C0tFRp3wghRBloZpkQQkiVpaWlAYDCgfCH4oQQou5osEwIIXIEBQWBx+MhNTUVAGBhYcGtx+XxeAgLC5MqHx0djREjRsDMzAza2towNTXFgAEDcP78eZm2eTweli5dCgBYunSpVLvjx4/nyt25cwcBAQFwdXVFixYtoK2tjUaNGsHb25ubsVWm4uJibN++HZ6enmjYsCEEAgEsLCwwY8YM/P3331JlPT09wePxkJycDEA6P0uWLOGWrzDGuGsu/QoKCpJqKzY2FmPGjEHr1q0hEAjQsGFD9OnTByEhIRX2defOnfD29kbjxo0hEAjQsmVLeHt7Y8OGDVy5yuYaAF69eoVFixbB2toaenp6MDQ0hFAoxA8//IC8vDyZPoSFhYHH48HT0xNv377F4sWLYWVlBT09PVqXTUg9QsswCCFEjvbt28PPzw9Hjx7FmzdvMGzYMBgYGHDxZs2acf+/bds2TJ8+HRKJBPb29vD09ERqaipOnTqFU6dOYcmSJQgICODK+/n5IS4uDvHx8bC1tYWdnR0Xc3Nz4/5/3bp12LFjBzp16gQbGxuYmJggLS0NoaGhuHjxIiIjI7Fu3TqlXG9OTg4GDhyIsLAwGBgYQCgUokmTJkhMTMSvv/6KI0eO4Pz587C3twcA+Pj4wNzcXG5+7OzsMHz4cLx48QK7du3irrlsbkutX78eX3/9NSQSCezs7ODk5ISnT58iLCwM586dw9KlS7F48WKpvmZlZaF///64evUqtLS04OLiAjMzMzx9+hQJCQm4ePEiZs+e/VG5fvToEXr16oXU1FQ0adIEvr6+KCoqQmhoKPz9/XHo0CFcuHABDRo0kMldfn4+PD09cefOHfTo0QO2trZ4+fJlNV8RQojaYIQQQhRq06YNA8CSk5PlxhMSEpimpibj8Xhs9+7dUrGQkBCmra3NALBz585JxQICAhgAFhAQoPDcYWFh7OHDhzLHk5KSWMuWLRkAFhUVJRVLTk5mAFibNm0qdX2lRo8ezQCw/v37s2fPnknFfvrpJwaAWVpasuLiYqnYh/IDgCn6VXPmzBnG4/FY48aNWXh4uFQsISGBu8awsDCp2NChQxkAZm9vL3PeoqIiduLECaljlcm1k5MTA8AGDhzIcnNzueMZGRnMwcGBAWCjR4+WqhMaGspdX9euXdk///yjsH1CSN1FyzAIIaQa1q9fj+LiYgwZMgRffPGFVKxv376YOnUqAGDt2rUf3baHhwfatm0rc7xjx474/vvvAQBHjx6tQq+l3b17FwcOHICZmRn2798PU1NTqfjcuXPh6+uLBw8e4PTp09U+X6mAgAAwxvDrr7+iR48eUjEbGxtu1rzssor4+HgcO3YMOjo6CA4OllnuoKmpiUGDBn1UP65evYqoqCjo6elh69at0NfX52JNmjTB1q1bAQAHDx5Eenq63DY2btwo9dcGQkj9QcswCCGkGkrXLpdf/1pq0qRJ2LhxI65cuQKxWAw+n/9R7efm5uL06dO4desWXrx4gcLCQgDAP//8AwC4d+9elfteKiQkBIwx9O3bF4aGhnLLeHp6IiQkBBEREejfv3+1z/nixQtER0dDV1cXAwYMUHhOAIiIiOCOnTlzBgDQr18/tGjRotr9AN6/hj4+PmjatKlMXCgUwtbWFvHx8QgPD8eYMWOk4qampnB3d1dKXwgh6ocGy4QQUg2PHz8GUHKDmzzt2rUDULKu9eXLlzKzthUJDg7GhAkTKlz/mp2d/RG9le/Ro0cAgB07dmDHjh0Vln3+/Hm1zwcAycnJYIwhLy8PAoGg0ucsveGyU6dOSukH8OHXECh5HePj47myZdHNfITUbzRYJoQQNfT48WP861//Ql5eHhYuXIgxY8bA3NwcBgYG0NDQwLlz59CnTx/uaRPVIZFIAJTcmGdra1thWScnp2qfr+w5DQwMMGzYMKW0WVt0dXVruwuEkBpEg2VCCKmGFi1a4OHDh3j06BG6dOkiEy+dtdXR0UHDhg0r3W5wcDDy8vIwZMgQrFmzRib+4MGDqne6nFatWgEAXF1dsXHjRqW1W5lz8ng87Ny5ExoalbuFpnXr1gCApKQkpfWldDlH6WslT2lMWUs/CCF1B93gRwghFdDW1gZQ8lxfeUrX1ZZ/dnCpnTt3AgDc3d2hqfl+fuJD7b569QoA0KZNG5kYYwz79+//cOcrqW/fvgCAkydPIj8/X2ntVsTMzAxdu3ZFTk4Otw65Mnx8fACUrLN+8uRJpepU9jU8c+YMnj17JhO/desW4uLioKGhIXMjIiGk/qPBMiGEVKBly5YAgD///FNu/Msvv4SmpiZOnDiBvXv3SsXOnTuH3377DQAwf/78j2rXysoKQMnTLkpv5gMAsViMxYsXS930Vl329vYYNmwY/v77bwwdOhQpKSkyZd68eYN9+/bJHUxW1YoVKwAAEyZMQHBwsEycMYaoqCicO3eOO2ZnZ4dBgwYhLy8PgwYN4nYILFVcXIyTJ09KHftQrt3c3ODk5IS8vDxMmzYNb9++5WIvXrzAtGnTAAAjR47kZsQJIZ8OHlPGgjdCCKmnNm3ahFmzZsHAwACfffYZtynFggUL0LFjRwDA1q1bMWPGDEgkEjg4OKBTp05ITU1FREQEGGMym5IAwLNnz9CuXTu8efMGrq6usLS0BJ/Ph6urKyZMmIDi4mI4OzsjNjYWBgYG8PDwgL6+PqKiovDkyRN8/fXXWLNmDTw8PKR2E0xJSYGFhQXatGkjd9CrSE5ODoYMGYKLFy9CW1sbtra2sLCwAGMMKSkpiI+PR2FhIe7evSt1c525uTlSU1ORnJws90Y3Ho8HAArXVv/yyy+YN28eiouL0b59e3Ts2BHGxsZ4/vw54uPjkZGRAX9/f6xevZqrk5mZCV9fX0RGRkJbW1tqU5LExEQ8f/5c6nwfyjUgvSmJqakpevTowW1Kkp2dDQcHB5lNScLCwtCzZ0+Z14AQUs/UzuOdCSGkbhCLxWzVqlXM2tqa6ejocJtQhIaGSpWLjIxkw4cPZ82aNWOampqsUaNGrF+/fjKbkZR1+fJl5u3tzRo0aMA0NDQYAObn58fFc3Jy2KJFi1jHjh2Zjo4OMzU1ZYMHD2Y3btzgNsTw8PCQarOqm5KUXuv+/fuZr68va9q0KdPS0mKNGjViXbp0YRMmTGDHjx9nhYWFUnWqsylJqcTERDZ16lRmaWnJdHR0mJ6eHmvbti3r06cP++WXX9jjx49l6hQUFLAtW7Ywd3d3ZmJiwrS1tVnLli1Z79692aZNm2TKfyjXjDH28uVL9u233zIrKyuuH/b29mz16tXs7du3Mm0qeg0IIfULzSwTQgghhBCiAK1ZJoQQQgghRAEaLBNCCCGEEKIADZYJIYQQQghRgAbLhBBCCCGEKECDZUIIIYQQQhSgwTIhhBBCCCEK0GCZEEIIIYQQBWiwTAghhBBCiAI0WCaEEEIIIUQBGiwTQgghhBCiAA2WCSGEEEIIUYAGy4QQQgghhCjw//UHToPN0bzeAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "channels = [\n", + " (\"g_HD x g_HD (additive)\", dict(ddg_A={\"HD\": 2.0}, ddg_B={\"HD\": 2.0}), \"0.55\", \"--\"),\n", + " (\"g_H x g_H (same state)\", dict(ddg_A={\"H\": 1.5}, ddg_B={\"H\": 1.5}), \"#2b6cb0\", \"-\"),\n", + " (\"g_H x g_LE (competing)\", dict(ddg_A={\"H\": 1.5}, ddg_B={\"LE\": -1.5}), \"#dd6b20\", \"-\"),\n", + " (\"within-state e on HD\", dict(ddg_A={\"HD\": 2.0}, ddg_B={\"HD\": 2.0}, epi={\"HD\": 1.5}), \"#c53030\", \":\"),\n", + " (\"within-state e on H\", dict(ddg_A={\"HD\": 2.0}, ddg_B={\"HD\": 2.0}, epi={\"H\": 1.5}), \"#805ad5\", \":\"),\n", + "]\n", + "\n", + "fig, ax = plt.subplots(figsize=(7.5, 4.6))\n", + "ax.axvspan(1e-3, effector[-1], color=\"0.9\", zorder=0) # unmixed (all-LE) readout zone\n", + "ax.axhline(0, color=\"0.6\", lw=1)\n", + "for label, kw, color, ls in channels:\n", + " e = epistasis_curve(model, effector, scale=\"logit\", **kw)\n", + " ax.semilogx(effector, e, ls, color=color, lw=2, label=label)\n", + "ax.text(3.2e-3, 0.15, \"ensemble -> 0\\n(all LE, unmixed)\", fontsize=8, color=\"0.4\", ha=\"center\")\n", + "ax.set_xlabel(\"total effector\")\n", + "ax.set_ylabel(\"logit-scale epistasis\")\n", + "ax.set_title(\"Ensemble vs. within-state epistasis on the logit scale\")\n", + "ax.legend(fontsize=8, loc=\"lower right\")\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "481e2bec", + "metadata": {}, + "source": [ + "### Reading the panel\n", + "\n", + "- **`g_HD × g_HD` (grey dashed) is flat at zero.** Two mutations that are additive\n", + " in energy and act through the operator-affinity channel have *no* epistasis on\n", + " the logit scale, at any concentration. The large raw-scale swing (left figure)\n", + " was entirely the DNA-saturation squashing — a scale artifact.\n", + "- **The two ensemble channels (`g_H×g_H`, `g_H×g_LE`) live only where the\n", + " free-protein ensemble is mixed** and vanish at the **high-effector plateau**\n", + " (everything is LE — unmixed). Note `g_H×g_H` is *large at low effector even\n", + " though the observable is plateaued there*: L and H are still 50/50, a hidden\n", + " conformational transition. Epistasis on a flat part of the observable is an\n", + " ensemble tell. `g_H×g_LE` is silent until LE starts to populate, then switches\n", + " on — its onset tracks the ligand transition, not the binding curve.\n", + "- **The within-state interactions do not follow the covariance shape.**\n", + " `e_AB` on `g_HD` is a **flat offset at every concentration**, including the\n", + " high-effector plateau where all ensemble channels have died — that residue is\n", + " the clean readout. `e_AB` on `g_H` tracks the H occupancy `p_H([E])`: a single\n", + " monotone step, no sign structure.\n", + "\n", + "**Practical upshot.** Do epistasis extraction on **logit θ**, and read the\n", + "**plateaus**: at the fully-liganded end, additive/ensemble terms go to zero and\n", + "only genuine within-state coupling survives. A single static monotone scale\n", + "removes the `g_HD` channel everywhere but *cannot* flatten the ensemble channels\n", + "at all concentrations at once — that irremovability is the signature.\n", + "\n", + "Things to try: set `ln_K_conf=4` so H dominates at low effector — the low-E\n", + "ensemble epistasis now vanishes too (ensemble unmixed at *both* ends), confirming\n", + "the low-E signal above was the hidden L/H mixing. Or separate the conformational\n", + "and ligand transitions in `[E]` to hunt for a two-lobed `ε(E)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19010b77-023f-4719-876e-efcf15800178", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index d9dd82b..e924dc8 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index 648a69a..e8f743f 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -3,6 +3,7 @@ import pandas as pd import numpy as np +import warnings from typing import List, Optional def mutant_cycle_pivot( @@ -132,7 +133,8 @@ def extract_epistasis( y_std: Optional[str] = None, group_by: List[str] | str | None=None, scale: str = "add", - keep_extra: bool = False + keep_extra: bool = False, + logit_eps: float = 1e-9, ) -> pd.DataFrame: """ Calculate epistasis between pairs of mutations for a given observable. @@ -158,14 +160,24 @@ def extract_epistasis( Column name(s) that define a unique experimental condition. Epistasis is calculated independently for each condition. If None, treat all conditions at once - scale : {"add", "mult"}, default "add" - The scale for calculating epistasis. + scale : {"add", "mult", "logit"}, default "add" + The scale for calculating epistasis. Each name selects a transform of + the observable; epistasis is the difference-of-differences of the + transformed values (reported in ratio form for "mult"). - "add": epsilon = (Y_{11} - Y_{10}) - (Y_{01} - Y_{00}) - "mult": epsilon = (Y_{11} / Y_{10}) / (Y_{01} / Y_{00}) + - "logit": epsilon = (L_{11} - L_{10}) - (L_{01} - L_{00}), where + L = logit(Y). Requires an observable in (0, 1) (e.g. theta/occupancy); + removes a saturating measurement scale so genuine (within-state) + interactions are not masked by the nonlinearity of a bounded readout. keep_extra : bool, default False If True, all columns from the original DataFrame are kept in the output. If False, only key identifiers and the calculated epistasis values are returned. + logit_eps : float, default 1e-9 + Only used when scale="logit". Observations are clamped to + [logit_eps, 1 - logit_eps] before the logit transform to keep values at + the 0/1 bounds finite. Inputs outside [0, 1] are clamped with a warning. Returns ------- @@ -180,9 +192,10 @@ def extract_epistasis( If `scale` is not one of "add" or "mult". """ - # Determine the epistatic scale - if scale not in ["add","mult"]: - err = "scale should be either 'add' (additive) or 'mult' (multiplicative)\n" + # Determine the epistatic scale + if scale not in ["add","mult","logit"]: + err = ("scale should be 'add' (additive), 'mult' (multiplicative), or " + "'logit' (additive on the logit scale)\n") raise ValueError(err) # Figure out what columns to extract @@ -233,11 +246,35 @@ def extract_epistasis( ep_obs = (obs_11 - obs_10) - (obs_01 - obs_00) if y_std is not None: ep_std = np.sqrt(std_11**2 + std_10**2 + std_01**2 + std_00**2) - + + # Logit scale: additive epistasis of logit(Y). Removes a saturating [0, 1] + # measurement scale so within-state interactions are not masked by it. + elif scale == "logit": + raw = [obs_00, obs_10, obs_01, obs_11] + + # logit is only defined on (0, 1); warn if the observable is not a + # fraction (a common sign of pointing y_obs at fitness/dG), then clamp. + if any(((o < 0.0) | (o > 1.0)).any() for o in raw): + warnings.warn( + "scale='logit' expects an observable in [0, 1]; values outside " + "this range were found and clamped. Check that y_obs is a " + "fraction/occupancy (e.g. theta), not fitness or dG.", + stacklevel=2, + ) + + clip = [o.clip(logit_eps, 1.0 - logit_eps) for o in raw] + t_00, t_10, t_01, t_11 = (np.log(c / (1.0 - c)) for c in clip) + ep_obs = (t_11 - t_10) - (t_01 - t_00) + if y_std is not None: + # delta method: d/dy logit(y) = 1 / (y (1 - y)) + raw_std = [std_00, std_10, std_01, std_11] + t_std = [s / (c * (1.0 - c)) for s, c in zip(raw_std, clip)] + ep_std = np.sqrt(sum(s**2 for s in t_std)) + # Multiplicative scale - else: + else: ep_obs = (obs_11 / obs_10) / (obs_01 / obs_00) - if y_std is not None: + if y_std is not None: rel_err_sq = ((std_11 / obs_11)**2 + (std_10 / obs_10)**2 + (std_01 / obs_01)**2 + (std_00 / obs_00)**2) ep_std = np.abs(ep_obs) * np.sqrt(rel_err_sq) diff --git a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py index 6dd0c37..4389e29 100644 --- a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -96,7 +96,8 @@ def extract_epistasis(data_file, y_std=None, group_by=None, scale="add", - keep_extra=False): + keep_extra=False, + logit_eps=1e-9): """ Calculate second-order epistasis for pairs of mutations. @@ -128,13 +129,17 @@ def extract_epistasis(data_file, One or more column names that define a unique experimental condition. Epistasis is calculated independently within each condition. If omitted, the whole table is treated as a single condition. - scale : {"add", "mult"}, optional + scale : {"add", "mult", "logit"}, optional Epistatic scale. "add" (default): (Y11 - Y10) - (Y01 - Y00). - "mult": (Y11 / Y10) / (Y01 / Y00). + "mult": (Y11 / Y10) / (Y01 / Y00). "logit": additive epistasis of + logit(Y) — requires an observable in (0, 1), such as theta/occupancy. keep_extra : bool, optional If True, retain all columns from the input CSV in the output. If False (default), keep only the identifier columns and the calculated epistasis values. + logit_eps : float, optional + Only used when scale="logit". Observations are clamped to + [logit_eps, 1 - logit_eps] before the transform. Default 1e-9. """ print(f"Reading {data_file}...", flush=True) df = pd.read_csv(data_file) @@ -162,7 +167,8 @@ def extract_epistasis(data_file, y_std=y_std, group_by=group_by, scale=scale, - keep_extra=keep_extra) + keep_extra=keep_extra, + logit_eps=logit_eps) out_file = f"{out_prefix}.csv" if result.empty: diff --git a/tests/tfscreen/analysis/test_extract_epistasis.py b/tests/tfscreen/analysis/test_extract_epistasis.py index 1184798..dca5594 100644 --- a/tests/tfscreen/analysis/test_extract_epistasis.py +++ b/tests/tfscreen/analysis/test_extract_epistasis.py @@ -245,4 +245,90 @@ def test_returns_empty_when_no_valid_cycles(self): def test_returns_empty_for_empty_input(self): """An empty input frame returns an empty frame (no KeyError).""" result = extract_epistasis(pd.DataFrame({"genotype": []}), "fitness") - assert result.empty \ No newline at end of file + assert result.empty + + +# --- logit-scale epistasis ---------------------------------------------------- + +def _logit(y): + return np.log(y / (1.0 - y)) + + +@pytest.fixture +def theta_cycle_df(): + """One complete mutant cycle with a fractional (in (0,1)) observable.""" + return pd.DataFrame({ + "genotype": ["wt", "A10G", "P25L", "A10G/P25L"], + "theta": [0.9, 0.8, 0.5, 0.3], + "theta_std": [0.05, 0.05, 0.05, 0.05], + }) + + +class TestLogitEpistasis: + def test_logit_ep_obs(self, theta_cycle_df): + """ep_obs is the difference-of-differences of logit(theta).""" + result = extract_epistasis(theta_cycle_df, "theta", scale="logit") + row = result.iloc[0] + expected = (_logit(0.3) - _logit(0.5)) - (_logit(0.8) - _logit(0.9)) + assert np.isclose(row["ep_obs"], expected) + + def test_logit_ep_std_delta_method(self, theta_cycle_df): + """ep_std propagates via the logit delta method, s / (y(1-y)).""" + result = extract_epistasis(theta_cycle_df, "theta", y_std="theta_std", + scale="logit") + row = result.iloc[0] + t = [0.05 / (y * (1.0 - y)) for y in (0.9, 0.8, 0.5, 0.3)] + expected = np.sqrt(sum(s**2 for s in t)) + assert np.isclose(row["ep_std"], expected) + + def test_logit_removes_pure_scale_epistasis(self): + """logit-additive thetas -> ~0 logit epistasis but nonzero on 'add'.""" + # logit values chosen additive: L11 = L01 + L10 - L00 -> logit ep == 0 + sig = lambda x: 1.0 / (1.0 + np.exp(-x)) + L = {"00": 0.0, "01": 0.8, "10": -1.2} + L["11"] = L["01"] + L["10"] - L["00"] + df = pd.DataFrame({ + "genotype": ["wt", "A10G", "P25L", "A10G/P25L"], + "theta": [sig(L["00"]), sig(L["01"]), sig(L["10"]), sig(L["11"])], + }) + logit_ep = extract_epistasis(df, "theta", scale="logit").iloc[0]["ep_obs"] + add_ep = extract_epistasis(df, "theta", scale="add").iloc[0]["ep_obs"] + assert np.isclose(logit_ep, 0.0, atol=1e-9) + assert abs(add_ep) > 1e-3 + + def test_logit_without_std(self, theta_cycle_df): + """No y_std -> no ep_std column, ep_obs still computed.""" + result = extract_epistasis(theta_cycle_df, "theta", scale="logit") + assert "ep_std" not in result.columns + assert np.isfinite(result.iloc[0]["ep_obs"]) + + def test_logit_clamps_bounds_finite(self): + """theta at exactly 0/1 is clamped so logit stays finite (no warning).""" + df = pd.DataFrame({ + "genotype": ["wt", "A10G", "P25L", "A10G/P25L"], + "theta": [1.0, 0.8, 0.5, 0.0], + }) + result = extract_epistasis(df, "theta", scale="logit") + assert np.isfinite(result.iloc[0]["ep_obs"]) + + def test_logit_warns_on_out_of_range(self): + """Values outside [0, 1] trigger a warning and are clamped.""" + df = pd.DataFrame({ + "genotype": ["wt", "A10G", "P25L", "A10G/P25L"], + "theta": [0.9, 1.5, 0.5, 0.3], # 1.5 is out of range + }) + with pytest.warns(UserWarning, match="expects an observable in"): + result = extract_epistasis(df, "theta", scale="logit") + assert np.isfinite(result.iloc[0]["ep_obs"]) + + def test_logit_custom_eps(self): + """logit_eps controls the clamp applied at the bounds.""" + df = pd.DataFrame({ + "genotype": ["wt", "A10G", "P25L", "A10G/P25L"], + "theta": [1.0, 0.8, 0.5, 0.3], + }) + eps = 1e-3 + result = extract_epistasis(df, "theta", scale="logit", logit_eps=eps) + # wt (00) clamped to 1 - eps; recompute the expected diff-of-diffs + expected = (_logit(0.3) - _logit(0.5)) - (_logit(0.8) - _logit(1.0 - eps)) + assert np.isclose(result.iloc[0]["ep_obs"], expected) \ No newline at end of file From a104747e3c14de9b15bf84c21fb86df5d6771fda Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 00:49:55 -0400 Subject: [PATCH 16/26] updated cat response --- CLAUDE.md | 4 +- .../analysis/cat_response/cat_assess.py | 79 ++++++++++ src/tfscreen/analysis/cat_response/cat_fit.py | 149 +++++++++++++++--- .../analysis/cat_response/cat_response.py | 20 ++- .../analysis/scripts/cat_response_cli.py | 7 + .../analysis/cat_response/test_cat_assess.py | 46 ++++++ .../analysis/cat_response/test_cat_fit.py | 77 ++++++++- .../cat_response/test_cat_response.py | 15 +- 8 files changed, 364 insertions(+), 33 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f57ffbf..580d285 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -284,12 +284,12 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( **Model x-scale (concentration-parameterized vs log-conc).** `MODEL_LIBRARY` models are **not** interchangeable on x-scale. The Hill family (`repressor`/`inducer`/`hill_*`) and `biphasic_*` are parameterized in **raw concentration** and take `log(x)` internally (`_hill` does `np.log(x)`), so they are already sigmoids/peaks *in log-concentration* and **must be handed raw x** — feeding them `log10(x)` (negative) both double-logs and NaNs them. The geometric models (`bell_peak`/`bell_dip` Gaussian-in-x, `linear`) are shapes in raw x; their `*_log` counterparts (`bell_peak_log`/`bell_dip_log` = Gaussian in `log10(x)` with a free real `center`, and `linear_log` = line in `log10(x)`) are the log-concentration versions. The `*_log` models own their transform via `models._to_log10_x` (x stays **raw concentration** in the data — there is no `--log_x` flag and no separate log column): `x <= 0` (the no-titrant point) is floored to `min(x[x>0])/100` before the log, computed per-call (identical across groups for a shared titration grid). `flat` is scale-invariant. When adding a new model, decide which camp it's in — never blanket-transform x. `DEFAULT_MODELS` (in `curve_models/__init__.py`) is the curated set fit when `--models`/`models_to_run` is omitted (was: all of `MODEL_LIBRARY`): `flat, linear_log, repressor, inducer, bell_peak_log, bell_dip_log` — one parameterization per qualitative response. All other models (raw-x `bell_*`/`linear`, 4-param `hill_*`, `biphasic_*`) stay registered and reachable via `--models`. -- **Shape** (which model): AICc weight selection in `cat_fit.py`. Uses the small-sample-corrected AICc, not AIC — with ~8 points and up to 4 params the correction `2k(k+1)/(n-k-1)` is large and flips selections; when `n-k-1 <= 0` the model gets `aicc=inf` (can't win, but its params are still reported). AIC/R² are computed on the **weighted** residuals (`chi2 = sum(((y-yfit)/y_std)**2)`; `aic = 2k + chi2`) — the fit was always weighted, so selection is too now. +- **Shape** (which model): **adequacy-first** selection in `cat_fit.py` (`select_by_adequacy`), *not* pure AICc. Each model's AICc is still computed (small-sample-corrected, on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported) and reported per model, but the selected `best_model` is the **simplest model whose residuals are not systematically clustered** — judged by a **one-sided (lower-tail) Wald-Wolfowitz runs test** on the sign sequence of the residuals ordered by x (`runs_p >= adequacy_alpha`, default 0.05) — with AICc breaking ties *within* a complexity (`k`) tier. This fixes AICc's small-sample tendency to crown a simple model (e.g. `linear`) that clearly misfits: at ~8 pts the parsimony penalty `2k(k+1)/(n-k-1)` makes a 4-param curve need a huge χ² drop to win, so a structured-residual `linear` used to slip through. The runs test is **sign-based → robust to `y_std` miscalibration** (important on the logit scale); it needs `n >= _MIN_RUNS_N` (4) usable residuals and has real power only at `n >~ 8`. `shape` = the qualitative form of `best_model` (`flat`/`linear`/`sigmoid`/`peaked`/`biphasic`, via `_SHAPE_BY_MODEL`); `shape_status` = `adequate` / `unmodeled` (all fits fail the runs test → fall back to AICc-best) / `unassessable` (no computable runs test) / `none`. `aicc_best_model` records what AICc alone would have picked (for transparency when they diverge). A weighted-χ² **goodness-of-fit** p (`goodness_of_fit_p` → `gof_p|*`, `best_model_gof_p`) is also reported but **does not gate** selection (it depends on the `y_std` scale, unlike the runs test). This selection is a form axis, **orthogonal to** the magnitude/`response_class` axis below. - **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading the *best* model against zero. Per-point `sig_nonzero` z-test at the observed x; a per-curve **omnibus** Wald/χ² `W = yhat @ pinv(Σ_yhat) @ yhat` (df = rank(Σ) = n_params) using the **full** prediction covariance (`predict_with_error(..., full_cov=True)` = `J·Cov·Jᵀ`), which rewards both magnitude and how-many-points and handles the strong correlation among predicted points; then **Benjamini-Hochberg** across curves → `omnibus_q`. `equiv_zero` (a point's whole CI ⊂ `[-delta, delta]`) is the confidence axis that separates "confidently flat" from "too noisy to tell". `delta` defaults to `delta_c * median(predicted y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits — hence the post-hoc pass); pass `--delta` for a fixed biological region. `response_class` is 3-valued with **equivalence taking precedence over significance** (a tiny-but-significant offset inside the ROPE is still practically zero): `confident_zero` (all points equiv) → else `real` (omnibus_q < alpha) → else `indeterminate` (the "flat because error is high" bucket to exclude downstream). -Outputs: rollups (`omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. +Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_gof_p`, `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. ## YAML Standards diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py index 1cb91b7..196db09 100644 --- a/src/tfscreen/analysis/cat_response/cat_assess.py +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -31,6 +31,10 @@ from tfscreen.mle import predict_with_error +# Minimum number of usable (finite, nonzero) residuals for the runs test to be +# computed. Below this the test is uninformative and returns NaN. +_MIN_RUNS_N = 4 + def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): """ @@ -130,6 +134,81 @@ def _omnibus_chi2(y_est, y_cov): return W, df, p +def residual_runs_p(resid): + """ + Wald-Wolfowitz runs-test p-value for systematic structure in residuals. + + Given residuals ordered along the independent variable, tests whether the + sign sequence shows *positive clustering* -- fewer runs than random -- which + is the fingerprint of a systematically wrong shape (the residuals sit on one + side, then the other). This is a **one-sided, lower-tail** test: it flags + same-sign clustering but not over-dispersion (alternating residuals are not + a shape error), which also gives it usable power at the small n typical + here. It uses only the residual *signs*, so it is robust to the scale of the + ``y_std`` used for weighting. This is the primary adequacy check for shape + selection. + + Parameters + ---------- + resid : array-like + Residuals in independent-variable order (e.g. weighted residuals sorted + by x). Non-finite and exactly-zero residuals are dropped. + + Returns + ------- + float + Lower-tail p-value under the null of random sign order; small when the + residuals cluster by sign (systematic misfit). NaN when it cannot be + computed: fewer than ``_MIN_RUNS_N`` usable residuals, or all residuals + share one sign (itself a sign of a biased fit; callers treat NaN as "not + adequate" unless *no* model could be assessed). + """ + resid = np.asarray(resid, dtype=float) + resid = resid[np.isfinite(resid) & (resid != 0.0)] + n = resid.size + if n < _MIN_RUNS_N: + return np.nan + + signs = resid > 0 + n_pos = int(np.count_nonzero(signs)) + n_neg = n - n_pos + if n_pos == 0 or n_neg == 0: + return np.nan + + runs = 1 + int(np.count_nonzero(signs[1:] != signs[:-1])) + mu = 1.0 + 2.0 * n_pos * n_neg / n + var = (2.0 * n_pos * n_neg * (2.0 * n_pos * n_neg - n) + / (n ** 2 * (n - 1.0))) + if var <= 0: + return np.nan + + z = (runs - mu) / np.sqrt(var) + return float(norm.cdf(z)) + + +def goodness_of_fit_p(chi2_w, n, k): + """ + Lack-of-fit p-value from the weighted chi-square of a fit. + + Under a correct model with calibrated ``y_std`` the weighted residual sum of + squares ~ chi2(n - k), so ``p = chi2.sf(chi2_w, n - k)`` is an *absolute* + adequacy check complementary to AICc's relative one. Reported alongside + selection but -- unlike the runs test -- not used to gate it, since it + depends on the ``y_std`` scale. NaN when ``n - k <= 0``. + + Parameters + ---------- + chi2_w : float + Weighted residual sum of squares, ``sum(((y - yfit) / y_std) ** 2)``. + n, k : int + Number of points and number of fitted parameters. + """ + df = n - k + if df <= 0: + return np.nan + return float(chi2.sf(chi2_w, df)) + + def compute_delta(pred_std, delta_c=2.0): """ Default region-of-practical-equivalence half-width from prediction error. diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index 3c46b2e..b6d477c 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -7,11 +7,71 @@ ) from tfscreen.util.numerical import xfill -from .cat_assess import assess_best_model +from .cat_assess import assess_best_model, residual_runs_p, goodness_of_fit_p import numpy as np import pandas as pd +# Qualitative response shape of each model, surfaced as the ``shape`` column so +# curves can be categorized by form (independent of the zero/magnitude axis). +# A model not listed here maps to "other". +_SHAPE_BY_MODEL = { + "flat": "flat", + "linear": "linear", "linear_log": "linear", + "repressor": "sigmoid", "inducer": "sigmoid", + "hill_repressor": "sigmoid", "hill_inducer": "sigmoid", + "bell_peak": "peaked", "bell_dip": "peaked", + "bell_peak_log": "peaked", "bell_dip_log": "peaked", + "biphasic_peak": "biphasic", "biphasic_dip": "biphasic", +} + + +def select_by_adequacy(models, adequacy_alpha): + """ + Pick the simplest *adequate* model; AICc breaks ties within a complexity tier. + + AICc alone selects the least-bad candidate but never asks whether the winner + actually fits, so at small n its parsimony penalty can crown a simple model + whose residuals are clearly systematic. Here a model is *adequate* when the + residual runs test does not reject randomness (``runs_p >= adequacy_alpha``). + Walking complexity (number of parameters ``k``) from low to high, the winner + is the lowest-AICc adequate model in the first tier that has one. + + Parameters + ---------- + models : list of dict + One dict per selectable fit, with keys ``model``, ``k``, ``AICc``, + ``runs_p`` (already filtered to successful fits with a usable covariance + and finite AICc). + adequacy_alpha : float + Runs-test significance threshold. ``runs_p`` below this flags + systematic residuals (inadequate shape). + + Returns + ------- + best_model : str or None + Selected model name (None if ``models`` is empty). + shape_status : str + ``"adequate"`` if an adequate model was found; otherwise the fallback is + the overall lowest-AICc model, tagged ``"unmodeled"`` (some fit could be + assessed but none passed) or ``"unassessable"`` (no fit had a computable + runs test), or ``"none"`` when there are no models. + """ + if not models: + return None, "none" + + aicc_best = min(models, key=lambda m: m["AICc"])["model"] + + adequate = [m for m in models + if np.isfinite(m["runs_p"]) and m["runs_p"] >= adequacy_alpha] + if adequate: + min_k = min(m["k"] for m in adequate) + tier = [m for m in adequate if m["k"] == min_k] + return min(tier, key=lambda m: m["AICc"])["model"], "adequate" + + assessable = any(np.isfinite(m["runs_p"]) for m in models) + return aicc_best, ("unmodeled" if assessable else "unassessable") + # Keys returned by assess_best_model's per-point dict (the model curve + tests). _PER_POINT_COLS = ["x", "y_model", "y_model_std", "z", "sig_nonzero", "direction"] @@ -33,6 +93,18 @@ "any_nonzero"] +def _set_no_best_model(flat_output): + """Emit the best-model / shape keys for a curve with no selectable model.""" + flat_output["best_model"] = "None" + flat_output["aicc_best_model"] = "None" + flat_output["best_model_R2"] = np.nan + flat_output["best_model_AIC_weight"] = np.nan + flat_output["best_model_gof_p"] = np.nan + flat_output["best_model_runs_p"] = np.nan + flat_output["shape"] = "none" + flat_output["shape_status"] = "none" + + def _empty_assess_df(): return pd.DataFrame({ c: pd.Series(dtype=_ASSESS_DTYPES.get(c, float)) for c in _ASSESS_COLS @@ -40,16 +112,21 @@ def _empty_assess_df(): def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, - alpha=0.05, verbose=False): + alpha=0.05, adequacy_alpha=0.05, verbose=False): """ Fits multiple models to a single dataset and returns a flat dictionary of all results, suitable for aggregation. - Model selection uses AICc (the small-sample-corrected AIC) computed from - the *weighted* residuals, since with only a handful of points and up to - four parameters the correction term is large and the error bars carry real - information. After selection, the best model is evaluated at the observed x - and tested against zero (see :mod:`cat_assess`). + Model selection is *adequacy-first*: each model's AICc (the small-sample- + corrected AIC on the weighted residuals) is still reported, but the selected + ``best_model`` is the **simplest model whose residuals are not systematically + structured** -- judged by a Wald-Wolfowitz runs test (``runs_p >= + adequacy_alpha``) -- with AICc breaking ties within a complexity tier (see + :func:`select_by_adequacy`). This avoids AICc's small-sample tendency to + crown a simple model (e.g. linear) that clearly misfits. A weighted-chi2 + goodness-of-fit p-value is also reported per model (``gof_p|*``) but does not + gate selection. After selection, the best model is evaluated at the observed + x and tested against zero (see :mod:`cat_assess`). Parameters ---------- @@ -69,6 +146,11 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, alpha : float, optional Two-sided significance level for the per-point ``sig_nonzero`` test. Default 0.05. + adequacy_alpha : float, optional + Runs-test threshold for the adequacy-first selection. A model whose + ``runs_p`` falls below this is treated as having systematic (structured) + residuals and is not selected unless no adequate model exists. Default + 0.05. verbose : bool, optional If True, prints warnings to the console when a model fails to fit. Defaults to False. @@ -117,15 +199,15 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, if len(x) < 2: flat_output['status'] = "missing" - flat_output['best_model'] = "None" - flat_output['best_model_R2'] = np.nan - flat_output['best_model_AIC_weight'] = np.nan + _set_no_best_model(flat_output) for key in _ROLLUP_KEYS: flat_output[key] = np.nan for name in models_to_run: param_names = MODEL_LIBRARY[name]["param_names"] flat_output[f"R2|{name}"] = np.nan flat_output[f"AIC_weight|{name}"] = np.nan + flat_output[f"gof_p|{name}"] = np.nan + flat_output[f"runs_p|{name}"] = np.nan for p_name in param_names: flat_output[f"{name}|{p_name}|est"] = np.nan flat_output[f"{name}|{p_name}|std"] = np.nan @@ -181,6 +263,13 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, resid = (y - y_fit) / y_std chi2 = float(np.sum(resid ** 2)) + # Adequacy diagnostics. runs_p (primary, scale-robust) tests the + # sign order of the residuals along x for systematic structure; + # gof_p is the absolute weighted-chi2 lack-of-fit p (reported only). + order = np.argsort(x, kind="stable") + runs_p = residual_runs_p(resid[order]) + gof_p = goodness_of_fit_p(chi2, n, k) + w = 1.0 / (y_std ** 2) y_wmean = np.average(y, weights=w) ss_tot = float(np.sum(w * (y - y_wmean) ** 2)) @@ -210,8 +299,10 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, aicc = np.inf cov_matrix = None - summary_results.append({"model": name, "R2": r2, "chi2": chi2, - "AIC": aic, "AICc": aicc, "success": True}) + summary_results.append({"model": name, "k": k, "R2": r2, + "chi2": chi2, "AIC": aic, "AICc": aicc, + "gof_p": gof_p, "runs_p": runs_p, + "success": True}) param_results[name] = {"params": params, "std_err": std_err, "cov": cov_matrix, "names": param_names, "model_func": model_func} @@ -220,9 +311,10 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, if verbose: print(f"Warning: Model '{name}' failed to fit. Reason: {e}") - summary_results.append({"model": name, "R2": np.nan, "chi2": np.nan, - "AIC": np.nan, "AICc": np.nan, - "success": False}) + summary_results.append({"model": name, "k": k, "R2": np.nan, + "chi2": np.nan, "AIC": np.nan, + "AICc": np.nan, "gof_p": np.nan, + "runs_p": np.nan, "success": False}) param_results[name] = { "params": np.full(k, np.nan), "std_err": np.full(k, np.nan), "cov": None, "names": param_names, "model_func": model_func @@ -251,6 +343,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, model_name = row['model'] flat_output[f"AIC_weight|{model_name}"] = row['AIC_weight'] flat_output[f"R2|{model_name}"] = row['R2'] + flat_output[f"gof_p|{model_name}"] = row['gof_p'] + flat_output[f"runs_p|{model_name}"] = row['runs_p'] p_res = param_results[model_name] for i, p_name in enumerate(p_res['names']): @@ -267,16 +361,27 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, flat_output['status'] = "partial" if not valid.empty: - best_model_row = summary_df.iloc[0] - best_model = best_model_row['model'] + # AICc still ranks the models (summary_df is sorted by it), but the + # selected best_model is the simplest *adequate* one; AICc only breaks + # ties within a complexity tier. aicc_best_model records what AICc alone + # would have chosen, for transparency when the two diverge. + aicc_best_model = summary_df.iloc[0]['model'] + model_records = valid[['model', 'k', 'AICc', 'runs_p']].to_dict('records') + best_model, shape_status = select_by_adequacy(model_records, + adequacy_alpha) + + best_row = summary_df.set_index('model').loc[best_model] flat_output['best_model'] = best_model - flat_output['best_model_R2'] = best_model_row['R2'] - flat_output['best_model_AIC_weight'] = best_model_row['AIC_weight'] + flat_output['aicc_best_model'] = aicc_best_model + flat_output['best_model_R2'] = best_row['R2'] + flat_output['best_model_AIC_weight'] = best_row['AIC_weight'] + flat_output['best_model_gof_p'] = best_row['gof_p'] + flat_output['best_model_runs_p'] = best_row['runs_p'] + flat_output['shape'] = _SHAPE_BY_MODEL.get(best_model, "other") + flat_output['shape_status'] = shape_status else: best_model = None - flat_output['best_model'] = "None" - flat_output['best_model_R2'] = np.nan - flat_output['best_model_AIC_weight'] = np.nan + _set_no_best_model(flat_output) # Predicted curves. Only successfully-fit models can be predicted, and only # the best model unless best_only is False. diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 620de8b..cf239d1 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -31,13 +31,14 @@ def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, best_only, alpha, - verbose): + adequacy_alpha, verbose): """Run cat_fit for one group and tag the results with the group key.""" flat_out, pred_df, assess_df = cat_fit(x, y, y_std, x_pred=x_pred, models_to_run=models_to_run, best_only=best_only, alpha=alpha, + adequacy_alpha=adequacy_alpha, verbose=verbose) for col, val in group_key.items(): flat_out[col] = val @@ -65,6 +66,7 @@ def cat_response(df, models_to_run=None, best_only=True, alpha=0.05, + adequacy_alpha=0.05, delta=None, delta_c=2.0, num_workers=1, @@ -106,6 +108,10 @@ def cat_response(df, the confidence level of the ``equiv_zero`` interval. Also the threshold applied to omnibus q-values when assigning ``response_class``. Default 0.05. + adequacy_alpha : float, optional + Runs-test threshold for adequacy-first model selection (see + :func:`cat_fit`). A curve's ``best_model`` is the simplest model whose + residuals are not systematically structured at this level. Default 0.05. delta : float or None, optional Region-of-practical-equivalence half-width around zero. If None (default), computed globally as ``delta_c * median(predicted y_std)`` @@ -127,11 +133,17 @@ def cat_response(df, ------- results_df : pandas.DataFrame One row per group. Group-key columns, then the flat cat_fit output - (``status``, ``best_model``, per-model ``AIC_weight|*`` / ``R2|*``, and + (``status``, ``best_model``, ``aicc_best_model``, ``shape``, + ``shape_status``, ``best_model_gof_p`` / ``best_model_runs_p``, per-model + ``AIC_weight|*`` / ``R2|*`` / ``gof_p|*`` / ``runs_p|*``, and ``||est`` / ``std`` columns), then the assessment rollups (``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``omnibus_q``, ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, ``response_class``). - Column names keep the ``|`` delimiter; presentation is left to callers. + ``best_model`` is the adequacy-first pick (see :func:`cat_fit`) and + ``shape`` its qualitative form (flat/linear/sigmoid/peaked/biphasic); + ``shape_status`` flags whether that shape actually fits (adequate / + unmodeled / unassessable). Column names keep the ``|`` delimiter; + presentation is left to callers. predictions_df : pandas.DataFrame Predicted curves, concatenated across groups. Columns are the group-key columns followed by ``model``, ``x``, ``y_model``, ``y_model_std``, @@ -185,7 +197,7 @@ def cat_response(df, ys = np.ones(len(x), dtype=float) work_items.append((group_key, x, y, ys, x_pred, models_to_run, - best_only, alpha, verbose)) + best_only, alpha, adequacy_alpha, verbose)) n_total = len(work_items) if n_total == 0: diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index b612583..5a4ed15 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -63,6 +63,7 @@ def cat_response(data_file, group_by=None, models=None, alpha=0.05, + adequacy_alpha=0.05, delta=None, delta_c=2.0, write_all_predictions=False, @@ -115,6 +116,11 @@ def cat_response(data_file, alpha : float, optional Significance level for the per-point tests and the omnibus q-value threshold used to call a curve 'real'. Default 0.05. + adequacy_alpha : float, optional + Runs-test threshold for adequacy-first shape selection: ``best_model`` + is the simplest model whose residuals are not systematically clustered + at this level (see ``shape``/``shape_status`` in the output). Default + 0.05. delta : float or None, optional Region-of-practical-equivalence half-width around zero. If None (default), derived globally as ``delta_c * median(predicted y_std)``. @@ -170,6 +176,7 @@ def cat_response(data_file, models_to_run=models, best_only=(not write_all_predictions), alpha=alpha, + adequacy_alpha=adequacy_alpha, delta=delta, delta_c=delta_c, num_workers=num_workers, diff --git a/tests/tfscreen/analysis/cat_response/test_cat_assess.py b/tests/tfscreen/analysis/cat_response/test_cat_assess.py index 099d239..e3ed82f 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_assess.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -11,10 +11,56 @@ compute_delta, classify_equiv, benjamini_hochberg, + residual_runs_p, + goodness_of_fit_p, _omnibus_chi2, ) +# --- residual runs test ------------------------------------------------------ + +class TestResidualRunsP: + def test_clustered_residuals_flagged(self): + """Same-sign clustering (systematic misfit) -> small p.""" + resid = np.array([-3, -2, -1, -0.5, 1, 2, 3, 4], dtype=float) + assert residual_runs_p(resid) < 0.05 + + def test_random_looking_residuals_not_flagged(self): + """A well-mixed sign sequence -> large p (adequate).""" + resid = np.array([1, -1, 2, -2, 1, -1, 2, -2], dtype=float) + assert residual_runs_p(resid) > 0.5 + + def test_over_dispersion_not_flagged(self): + """One-sided: too-many-runs (alternating) is not a shape error.""" + resid = np.array([1, -1, 1, -1, 1, -1, 1, -1], dtype=float) + assert residual_runs_p(resid) > 0.05 + + def test_too_few_points_is_nan(self): + assert np.isnan(residual_runs_p(np.array([1.0, -1.0, 1.0]))) + + def test_all_one_sign_is_nan(self): + assert np.isnan(residual_runs_p(np.array([1, 2, 3, 4, 5.0]))) + + def test_zeros_and_nonfinite_dropped(self): + # Zeros/NaN removed; remaining 4 clustered residuals still assessable. + resid = np.array([0.0, np.nan, -2, -1, 1, 2], dtype=float) + assert np.isfinite(residual_runs_p(resid)) + + +# --- goodness of fit --------------------------------------------------------- + +class TestGoodnessOfFitP: + def test_matches_chi2_sf(self): + assert goodness_of_fit_p(10.0, 12, 2) == pytest.approx(chi2.sf(10.0, 10)) + + def test_large_chi2_small_p(self): + assert goodness_of_fit_p(100.0, 12, 2) < 0.001 + + def test_nonpositive_df_is_nan(self): + assert np.isnan(goodness_of_fit_p(5.0, 4, 4)) + assert np.isnan(goodness_of_fit_p(5.0, 3, 4)) + + def _linear(params, x): m, b = params return m * x + b diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index 32c4d79..9629989 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -6,10 +6,49 @@ import numpy as np import pytest -from tfscreen.analysis.cat_response.cat_fit import cat_fit +from tfscreen.analysis.cat_response.cat_fit import cat_fit, select_by_adequacy from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS +# --- adequacy-first selection ------------------------------------------------ + +def _rec(model, k, aicc, runs_p): + return {"model": model, "k": k, "AICc": aicc, "runs_p": runs_p} + + +class TestSelectByAdequacy: + def test_picks_simplest_adequate(self): + # linear (k=2) is adequate and simplest even though bell has lower AICc. + models = [_rec("linear", 2, 10.0, 0.5), _rec("bell", 4, 2.0, 0.6)] + assert select_by_adequacy(models, 0.05) == ("linear", "adequate") + + def test_escalates_when_simple_inadequate(self): + # flat/linear have structured residuals (low runs_p); bell is adequate. + models = [_rec("flat", 1, 5.0, 0.001), _rec("linear", 2, 4.0, 0.01), + _rec("bell", 4, 8.0, 0.4)] + assert select_by_adequacy(models, 0.05) == ("bell", "adequate") + + def test_tie_break_by_aicc_within_tier(self): + # Two adequate models at the same k -> lower AICc wins. + models = [_rec("repressor", 3, 9.0, 0.3), _rec("inducer", 3, 4.0, 0.4)] + assert select_by_adequacy(models, 0.05) == ("inducer", "adequate") + + def test_none_adequate_falls_back_unmodeled(self): + models = [_rec("flat", 1, 5.0, 0.001), _rec("linear", 2, 4.0, 0.002)] + best, status = select_by_adequacy(models, 0.05) + assert best == "linear" # overall AICc-best fallback + assert status == "unmodeled" + + def test_unassessable_when_all_runs_nan(self): + models = [_rec("flat", 1, 5.0, np.nan), _rec("linear", 2, 4.0, np.nan)] + best, status = select_by_adequacy(models, 0.05) + assert best == "linear" + assert status == "unassessable" + + def test_empty(self): + assert select_by_adequacy([], 0.05) == (None, "none") + + # A titration-like grid with enough points that low-parameter models are usable. X = np.array([0.0, 1.0, 3.0, 10.0, 30.0, 100.0]) @@ -63,11 +102,43 @@ def test_flat_selected_for_flat_data(): x, y, ys = _flat_data() flat_output, _, _ = cat_fit(x, y, ys, models_to_run=["flat", "hill_inducer"]) - # AICc prefers the cheaper (1-param) flat model over a 4-param hill that - # buys no fit improvement. + # Adequacy-first prefers the simplest adequate (1-param flat) model over a + # 4-param hill that buys no fit improvement. assert flat_output["best_model"] == "flat" +def test_adequacy_columns_present_and_shape(): + x, y, ys = _hill_data() + flat_output, _, _ = cat_fit(x, y, ys, + models_to_run=["flat", "hill_inducer"]) + # New diagnostic + shape columns are emitted. + for key in ["aicc_best_model", "best_model_gof_p", "best_model_runs_p", + "shape", "shape_status", "gof_p|flat", "runs_p|flat", + "gof_p|hill_inducer", "runs_p|hill_inducer"]: + assert key in flat_output + # Sigmoid data -> hill_inducer selected -> shape "sigmoid", adequate. + assert flat_output["best_model"] == "hill_inducer" + assert flat_output["shape"] == "sigmoid" + assert flat_output["shape_status"] == "adequate" + + +def test_flat_misfit_escalates_off_linear(): + """A clearly curved dataset is not left on flat/linear by adequacy-first.""" + # Monotone-saturating data with small, non-clustered wobble and enough + # points for the runs test to have power. + x = np.array([0.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0, 55.0]) + from tfscreen.mle.curve_models.models import model_hill_4p + y = model_hill_4p([0.05, 0.9, np.log(5.0), 1.5], x) + ys = np.full_like(x, 0.02) + flat_output, _, _ = cat_fit(x, y, ys, + models_to_run=["flat", "linear_log", + "inducer"]) + assert flat_output["best_model"] != "flat" + assert flat_output["shape_status"] == "adequate" + # flat's residuals cluster by sign -> flagged inadequate. + assert flat_output["runs_p|flat"] < 0.05 + + def test_aicc_excludes_overparameterized_models(): """With few points a 4-param model has n-k-1<=0 -> can't win, params kept.""" # 4 points, hill_inducer has k=4 -> denom = 4-4-1 = -1 -> AICc = inf. diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 7f7f4b6..7a82230 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -62,9 +62,11 @@ def _assess(n=2, y_model=0.0, y_model_std=0.1): def _capturing_fit(store, **fit_kwargs): """A fake cat_fit that records the (x, y, y_std) it was handed per call.""" def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, - best_only=True, alpha=0.05, verbose=False): + best_only=True, alpha=0.05, adequacy_alpha=0.05, + verbose=False): store.append({"x": list(x), "y": list(y), "y_std": list(y_std), - "best_only": best_only, "alpha": alpha}) + "best_only": best_only, "alpha": alpha, + "adequacy_alpha": adequacy_alpha}) return _flat(**fit_kwargs), _pred(len(x)), _assess(len(np.unique(x))) return fake_fit @@ -158,6 +160,15 @@ def test_best_only_and_alpha_threaded_to_fitter(self): assert all(call["best_only"] is False for call in store) assert all(call["alpha"] == 0.01 for call in store) + def test_adequacy_alpha_threaded_to_fitter(self): + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", adequacy_alpha=0.2) + assert all(call["adequacy_alpha"] == 0.2 for call in store) + # --- validation -------------------------------------------------------------- From 77ccd9a0e9a0c7468192e19cc30d55b0673c1839 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 07:12:57 -0400 Subject: [PATCH 17/26] updated cat_response caller --- CLAUDE.md | 2 +- src/tfscreen/analysis/cat_response/cat_fit.py | 134 +++++++++++------- .../analysis/cat_response/cat_response.py | 28 ++-- .../analysis/scripts/cat_response_cli.py | 13 +- .../analysis/cat_response/test_cat_fit.py | 132 ++++++++++++----- .../cat_response/test_cat_response.py | 22 ++- 6 files changed, 220 insertions(+), 111 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 580d285..ad6dba1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -284,7 +284,7 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( **Model x-scale (concentration-parameterized vs log-conc).** `MODEL_LIBRARY` models are **not** interchangeable on x-scale. The Hill family (`repressor`/`inducer`/`hill_*`) and `biphasic_*` are parameterized in **raw concentration** and take `log(x)` internally (`_hill` does `np.log(x)`), so they are already sigmoids/peaks *in log-concentration* and **must be handed raw x** — feeding them `log10(x)` (negative) both double-logs and NaNs them. The geometric models (`bell_peak`/`bell_dip` Gaussian-in-x, `linear`) are shapes in raw x; their `*_log` counterparts (`bell_peak_log`/`bell_dip_log` = Gaussian in `log10(x)` with a free real `center`, and `linear_log` = line in `log10(x)`) are the log-concentration versions. The `*_log` models own their transform via `models._to_log10_x` (x stays **raw concentration** in the data — there is no `--log_x` flag and no separate log column): `x <= 0` (the no-titrant point) is floored to `min(x[x>0])/100` before the log, computed per-call (identical across groups for a shared titration grid). `flat` is scale-invariant. When adding a new model, decide which camp it's in — never blanket-transform x. `DEFAULT_MODELS` (in `curve_models/__init__.py`) is the curated set fit when `--models`/`models_to_run` is omitted (was: all of `MODEL_LIBRARY`): `flat, linear_log, repressor, inducer, bell_peak_log, bell_dip_log` — one parameterization per qualitative response. All other models (raw-x `bell_*`/`linear`, 4-param `hill_*`, `biphasic_*`) stay registered and reachable via `--models`. -- **Shape** (which model): **adequacy-first** selection in `cat_fit.py` (`select_by_adequacy`), *not* pure AICc. Each model's AICc is still computed (small-sample-corrected, on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported) and reported per model, but the selected `best_model` is the **simplest model whose residuals are not systematically clustered** — judged by a **one-sided (lower-tail) Wald-Wolfowitz runs test** on the sign sequence of the residuals ordered by x (`runs_p >= adequacy_alpha`, default 0.05) — with AICc breaking ties *within* a complexity (`k`) tier. This fixes AICc's small-sample tendency to crown a simple model (e.g. `linear`) that clearly misfits: at ~8 pts the parsimony penalty `2k(k+1)/(n-k-1)` makes a 4-param curve need a huge χ² drop to win, so a structured-residual `linear` used to slip through. The runs test is **sign-based → robust to `y_std` miscalibration** (important on the logit scale); it needs `n >= _MIN_RUNS_N` (4) usable residuals and has real power only at `n >~ 8`. `shape` = the qualitative form of `best_model` (`flat`/`linear`/`sigmoid`/`peaked`/`biphasic`, via `_SHAPE_BY_MODEL`); `shape_status` = `adequate` / `unmodeled` (all fits fail the runs test → fall back to AICc-best) / `unassessable` (no computable runs test) / `none`. `aicc_best_model` records what AICc alone would have picked (for transparency when they diverge). A weighted-χ² **goodness-of-fit** p (`goodness_of_fit_p` → `gof_p|*`, `best_model_gof_p`) is also reported but **does not gate** selection (it depends on the `y_std` scale, unlike the runs test). This selection is a form axis, **orthogonal to** the magnitude/`response_class` axis below. +- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py`. **Default `"aicc"`**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). This is the robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. Opt-in `select_by="adequacy"` (`select_by_adequacy`) is an **escalate-only** refinement: keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat` (killing the omnibus "real" call). Per-model diagnostics are reported but **do not gate the default**: `runs_p|*` (sign-based, robust to `y_std` scale; needs `n >= _MIN_RUNS_N` (4), real power only at `n >~ 8`) and weighted-χ² `gof_p|*` (`goodness_of_fit_p`, depends on `y_std` scale). `shape` = qualitative form of `best_model` (`flat`/`linear`/`sigmoid`/`peaked`/`biphasic`, via `_SHAPE_BY_MODEL`); `shape_status` = diagnostic on the **selected** model's residuals (`adequate` / `misfit` / `unassessable` / `none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only in adequacy mode when it escalates). This is a form axis, **orthogonal to** the magnitude/`response_class` axis below. - **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading the *best* model against zero. Per-point `sig_nonzero` z-test at the observed x; a per-curve **omnibus** Wald/χ² `W = yhat @ pinv(Σ_yhat) @ yhat` (df = rank(Σ) = n_params) using the **full** prediction covariance (`predict_with_error(..., full_cov=True)` = `J·Cov·Jᵀ`), which rewards both magnitude and how-many-points and handles the strong correlation among predicted points; then **Benjamini-Hochberg** across curves → `omnibus_q`. `equiv_zero` (a point's whole CI ⊂ `[-delta, delta]`) is the confidence axis that separates "confidently flat" from "too noisy to tell". `delta` defaults to `delta_c * median(predicted y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits — hence the post-hoc pass); pass `--delta` for a fixed biological region. `response_class` is 3-valued with **equivalence taking precedence over significance** (a tiny-but-significant offset inside the ROPE is still practically zero): `confident_zero` (all points equiv) → else `real` (omnibus_q < alpha) → else `indeterminate` (the "flat because error is high" bucket to exclude downstream). diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index b6d477c..33e6eeb 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -26,51 +26,57 @@ } +def _shape_status(runs_p, adequacy_alpha): + """Diagnostic label for the selected model from its runs-test p-value.""" + if not np.isfinite(runs_p): + return "unassessable" + return "adequate" if runs_p >= adequacy_alpha else "misfit" + + def select_by_adequacy(models, adequacy_alpha): """ - Pick the simplest *adequate* model; AICc breaks ties within a complexity tier. - - AICc alone selects the least-bad candidate but never asks whether the winner - actually fits, so at small n its parsimony penalty can crown a simple model - whose residuals are clearly systematic. Here a model is *adequate* when the - residual runs test does not reject randomness (``runs_p >= adequacy_alpha``). - Walking complexity (number of parameters ``k``) from low to high, the winner - is the lowest-AICc adequate model in the first tier that has one. + Escalate-only refinement of the AICc pick (the ``select_by="adequacy"`` mode). + + Selection starts from the lowest-AICc model. If its residuals are + systematically structured (the one-sided runs test rejects, + ``runs_p < adequacy_alpha``), escalate to the lowest-AICc *adequate* model + that is **no simpler** (``k >= `` the AICc pick's ``k``); otherwise keep the + AICc pick. Selection is never moved to a *simpler* model, so a low-power runs + test -- e.g. on noisy, heteroscedastic (logit) data where the many + near-baseline points wash out the residual sign pattern -- can only leave the + AICc pick unchanged. It can never demote a confident curved fit down to flat + (the failure mode of the earlier "simplest adequate" rule). Parameters ---------- models : list of dict One dict per selectable fit, with keys ``model``, ``k``, ``AICc``, ``runs_p`` (already filtered to successful fits with a usable covariance - and finite AICc). + and finite AICc). Non-empty. adequacy_alpha : float - Runs-test significance threshold. ``runs_p`` below this flags - systematic residuals (inadequate shape). + Runs-test threshold. ``runs_p`` below this flags systematic residuals. Returns ------- - best_model : str or None - Selected model name (None if ``models`` is empty). - shape_status : str - ``"adequate"`` if an adequate model was found; otherwise the fallback is - the overall lowest-AICc model, tagged ``"unmodeled"`` (some fit could be - assessed but none passed) or ``"unassessable"`` (no fit had a computable - runs test), or ``"none"`` when there are no models. + dict + The selected model's record (an element of ``models``). """ - if not models: - return None, "none" - - aicc_best = min(models, key=lambda m: m["AICc"])["model"] - - adequate = [m for m in models - if np.isfinite(m["runs_p"]) and m["runs_p"] >= adequacy_alpha] - if adequate: - min_k = min(m["k"] for m in adequate) - tier = [m for m in adequate if m["k"] == min_k] - return min(tier, key=lambda m: m["AICc"])["model"], "adequate" - - assessable = any(np.isfinite(m["runs_p"]) for m in models) - return aicc_best, ("unmodeled" if assessable else "unassessable") + aicc_best = min(models, key=lambda m: m["AICc"]) + + rp = aicc_best["runs_p"] + if (not np.isfinite(rp)) or rp >= adequacy_alpha: + # AICc pick is adequate (or unassessable): keep it, never escalate off. + return aicc_best + + # AICc pick is flagged: escalate to the lowest-AICc adequate model that is + # no simpler. If there is none, keep the (flagged) AICc pick. + k0 = aicc_best["k"] + cands = [m for m in models + if m["k"] >= k0 and np.isfinite(m["runs_p"]) + and m["runs_p"] >= adequacy_alpha] + if cands: + return min(cands, key=lambda m: m["AICc"]) + return aicc_best # Keys returned by assess_best_model's per-point dict (the model curve + tests). _PER_POINT_COLS = ["x", "y_model", "y_model_std", "z", "sig_nonzero", @@ -112,21 +118,28 @@ def _empty_assess_df(): def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, - alpha=0.05, adequacy_alpha=0.05, verbose=False): + alpha=0.05, select_by="aicc", adequacy_alpha=0.05, verbose=False): """ Fits multiple models to a single dataset and returns a flat dictionary of all results, suitable for aggregation. - Model selection is *adequacy-first*: each model's AICc (the small-sample- - corrected AIC on the weighted residuals) is still reported, but the selected - ``best_model`` is the **simplest model whose residuals are not systematically - structured** -- judged by a Wald-Wolfowitz runs test (``runs_p >= - adequacy_alpha``) -- with AICc breaking ties within a complexity tier (see - :func:`select_by_adequacy`). This avoids AICc's small-sample tendency to - crown a simple model (e.g. linear) that clearly misfits. A weighted-chi2 - goodness-of-fit p-value is also reported per model (``gof_p|*``) but does not - gate selection. After selection, the best model is evaluated at the observed - x and tested against zero (see :mod:`cat_assess`). + Model selection (``select_by``): + + - ``"aicc"`` (default): ``best_model`` is the lowest-AICc model (the small- + sample-corrected AIC on the weighted residuals). This is the robust default + -- the weighted chi2 correctly weights the informative points, which the + sign-based runs test does not. + - ``"adequacy"``: escalate-only refinement of the AICc pick -- keep it unless + its residuals are systematically structured, in which case move to a + no-simpler adequate model (see :func:`select_by_adequacy`). Never demotes, + so it cannot collapse a confident curved fit to flat. + + Either way, per-model diagnostics are reported but do **not** gate the default + selection: the runs-test p (``runs_p|*``; one-sided, sign-based, robust to + ``y_std`` scale) and the weighted-chi2 goodness-of-fit p (``gof_p|*``). The + selected model's ``shape``/``shape_status`` summarize its form and whether its + residuals look adequate. After selection, the best model is evaluated at the + observed x and tested against zero (see :mod:`cat_assess`). Parameters ---------- @@ -146,11 +159,15 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, alpha : float, optional Two-sided significance level for the per-point ``sig_nonzero`` test. Default 0.05. + select_by : {"aicc", "adequacy"}, optional + Model-selection strategy. ``"aicc"`` (default) picks the lowest-AICc + model. ``"adequacy"`` starts from the AICc pick and escalates to a + no-simpler adequate model only if the AICc pick's residuals are flagged + as structured (never demotes). Default ``"aicc"``. adequacy_alpha : float, optional - Runs-test threshold for the adequacy-first selection. A model whose - ``runs_p`` falls below this is treated as having systematic (structured) - residuals and is not selected unless no adequate model exists. Default - 0.05. + Runs-test threshold used for the ``shape_status`` diagnostic and (when + ``select_by="adequacy"``) for escalation. ``runs_p`` below this flags + systematic residuals. Default 0.05. verbose : bool, optional If True, prints warnings to the console when a model fails to fit. Defaults to False. @@ -178,6 +195,11 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, if models_to_run is None: models_to_run = list(DEFAULT_MODELS) + if select_by not in ("aicc", "adequacy"): + raise ValueError( + f"select_by must be 'aicc' or 'adequacy', got {select_by!r}" + ) + flat_output = {} # Sanitize inputs @@ -361,14 +383,17 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, flat_output['status'] = "partial" if not valid.empty: - # AICc still ranks the models (summary_df is sorted by it), but the - # selected best_model is the simplest *adequate* one; AICc only breaks - # ties within a complexity tier. aicc_best_model records what AICc alone - # would have chosen, for transparency when the two diverge. + # summary_df is sorted by AICc, so iloc[0] is the AICc pick. select_by + # decides whether to keep it ("aicc") or apply the escalate-only + # adequacy refinement ("adequacy"). aicc_best_model records the AICc pick + # for transparency when the two diverge. aicc_best_model = summary_df.iloc[0]['model'] model_records = valid[['model', 'k', 'AICc', 'runs_p']].to_dict('records') - best_model, shape_status = select_by_adequacy(model_records, - adequacy_alpha) + if select_by == "adequacy": + chosen = select_by_adequacy(model_records, adequacy_alpha) + else: + chosen = min(model_records, key=lambda m: m["AICc"]) + best_model = chosen["model"] best_row = summary_df.set_index('model').loc[best_model] flat_output['best_model'] = best_model @@ -378,7 +403,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, flat_output['best_model_gof_p'] = best_row['gof_p'] flat_output['best_model_runs_p'] = best_row['runs_p'] flat_output['shape'] = _SHAPE_BY_MODEL.get(best_model, "other") - flat_output['shape_status'] = shape_status + flat_output['shape_status'] = _shape_status(best_row['runs_p'], + adequacy_alpha) else: best_model = None _set_no_best_model(flat_output) diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index cf239d1..6e623af 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -31,13 +31,14 @@ def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, best_only, alpha, - adequacy_alpha, verbose): + select_by, adequacy_alpha, verbose): """Run cat_fit for one group and tag the results with the group key.""" flat_out, pred_df, assess_df = cat_fit(x, y, y_std, x_pred=x_pred, models_to_run=models_to_run, best_only=best_only, alpha=alpha, + select_by=select_by, adequacy_alpha=adequacy_alpha, verbose=verbose) for col, val in group_key.items(): @@ -66,6 +67,7 @@ def cat_response(df, models_to_run=None, best_only=True, alpha=0.05, + select_by="aicc", adequacy_alpha=0.05, delta=None, delta_c=2.0, @@ -108,10 +110,14 @@ def cat_response(df, the confidence level of the ``equiv_zero`` interval. Also the threshold applied to omnibus q-values when assigning ``response_class``. Default 0.05. + select_by : {"aicc", "adequacy"}, optional + Model-selection strategy (see :func:`cat_fit`). ``"aicc"`` (default) + selects the lowest-AICc model. ``"adequacy"`` keeps the AICc pick unless + its residuals are systematically structured, then escalates to a + no-simpler adequate model (never demotes). Default ``"aicc"``. adequacy_alpha : float, optional - Runs-test threshold for adequacy-first model selection (see - :func:`cat_fit`). A curve's ``best_model`` is the simplest model whose - residuals are not systematically structured at this level. Default 0.05. + Runs-test threshold used for the ``shape_status`` diagnostic and, when + ``select_by="adequacy"``, for escalation. Default 0.05. delta : float or None, optional Region-of-practical-equivalence half-width around zero. If None (default), computed globally as ``delta_c * median(predicted y_std)`` @@ -139,11 +145,12 @@ def cat_response(df, ``||est`` / ``std`` columns), then the assessment rollups (``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``omnibus_q``, ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, ``response_class``). - ``best_model`` is the adequacy-first pick (see :func:`cat_fit`) and - ``shape`` its qualitative form (flat/linear/sigmoid/peaked/biphasic); - ``shape_status`` flags whether that shape actually fits (adequate / - unmodeled / unassessable). Column names keep the ``|`` delimiter; - presentation is left to callers. + ``best_model`` follows ``select_by`` (default lowest-AICc; see + :func:`cat_fit`), ``shape`` is its qualitative form + (flat/linear/sigmoid/peaked/biphasic), and ``shape_status`` is a + diagnostic on the selected model's residuals (adequate / misfit / + unassessable). Column names keep the ``|`` delimiter; presentation is + left to callers. predictions_df : pandas.DataFrame Predicted curves, concatenated across groups. Columns are the group-key columns followed by ``model``, ``x``, ``y_model``, ``y_model_std``, @@ -197,7 +204,8 @@ def cat_response(df, ys = np.ones(len(x), dtype=float) work_items.append((group_key, x, y, ys, x_pred, models_to_run, - best_only, alpha, adequacy_alpha, verbose)) + best_only, alpha, select_by, adequacy_alpha, + verbose)) n_total = len(work_items) if n_total == 0: diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index 5a4ed15..92ade36 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -63,6 +63,7 @@ def cat_response(data_file, group_by=None, models=None, alpha=0.05, + select_by="aicc", adequacy_alpha=0.05, delta=None, delta_c=2.0, @@ -116,11 +117,14 @@ def cat_response(data_file, alpha : float, optional Significance level for the per-point tests and the omnibus q-value threshold used to call a curve 'real'. Default 0.05. + select_by : {"aicc", "adequacy"}, optional + Model-selection strategy. ``"aicc"`` (default) selects the lowest-AICc + model. ``"adequacy"`` keeps the AICc pick unless its residuals are + systematically structured, then escalates to a no-simpler adequate model + (never demotes). Default ``"aicc"``. adequacy_alpha : float, optional - Runs-test threshold for adequacy-first shape selection: ``best_model`` - is the simplest model whose residuals are not systematically clustered - at this level (see ``shape``/``shape_status`` in the output). Default - 0.05. + Runs-test threshold for the ``shape_status`` diagnostic and, when + ``select_by="adequacy"``, for escalation. Default 0.05. delta : float or None, optional Region-of-practical-equivalence half-width around zero. If None (default), derived globally as ``delta_c * median(predicted y_std)``. @@ -176,6 +180,7 @@ def cat_response(data_file, models_to_run=models, best_only=(not write_all_predictions), alpha=alpha, + select_by=select_by, adequacy_alpha=adequacy_alpha, delta=delta, delta_c=delta_c, diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index 9629989..d0cc08a 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -6,47 +6,61 @@ import numpy as np import pytest -from tfscreen.analysis.cat_response.cat_fit import cat_fit, select_by_adequacy +from tfscreen.analysis.cat_response.cat_fit import ( + cat_fit, select_by_adequacy, _shape_status, +) from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS -# --- adequacy-first selection ------------------------------------------------ +# --- escalate-only adequacy selection ---------------------------------------- def _rec(model, k, aicc, runs_p): return {"model": model, "k": k, "AICc": aicc, "runs_p": runs_p} class TestSelectByAdequacy: - def test_picks_simplest_adequate(self): - # linear (k=2) is adequate and simplest even though bell has lower AICc. + def test_keeps_adequate_aicc_pick(self): + # AICc pick (bell, lowest AICc) is adequate -> kept, not demoted. models = [_rec("linear", 2, 10.0, 0.5), _rec("bell", 4, 2.0, 0.6)] - assert select_by_adequacy(models, 0.05) == ("linear", "adequate") + assert select_by_adequacy(models, 0.05)["model"] == "bell" - def test_escalates_when_simple_inadequate(self): - # flat/linear have structured residuals (low runs_p); bell is adequate. - models = [_rec("flat", 1, 5.0, 0.001), _rec("linear", 2, 4.0, 0.01), + def test_escalates_off_flagged_pick(self): + # AICc pick (linear) is flagged; escalate to the adequate, no-simpler bell. + models = [_rec("flat", 1, 5.0, 0.5), _rec("linear", 2, 4.0, 0.01), _rec("bell", 4, 8.0, 0.4)] - assert select_by_adequacy(models, 0.05) == ("bell", "adequate") + assert select_by_adequacy(models, 0.05)["model"] == "bell" + + def test_never_demotes_flagged_complex_pick(self): + # AICc pick (bell) is flagged but nothing no-simpler is adequate; the + # only adequate model is *simpler* (flat) -> KEEP bell, never demote. + # This is the failure mode the escalate-only rule exists to prevent. + models = [_rec("flat", 1, 10.0, 0.9), _rec("bell", 4, 2.0, 0.01)] + assert select_by_adequacy(models, 0.05)["model"] == "bell" + + def test_escalation_tie_break_by_aicc(self): + # Flagged linear -> among no-simpler adequate models, lowest AICc wins. + models = [_rec("linear", 2, 4.0, 0.01), _rec("repressor", 3, 9.0, 0.3), + _rec("inducer", 3, 6.0, 0.4)] + assert select_by_adequacy(models, 0.05)["model"] == "inducer" + + def test_keeps_flagged_pick_when_no_adequate_alternative(self): + models = [_rec("flat", 1, 5.0, 0.002), _rec("linear", 2, 4.0, 0.002)] + assert select_by_adequacy(models, 0.05)["model"] == "linear" + + def test_unassessable_pick_kept(self): + models = [_rec("flat", 1, 5.0, np.nan), _rec("linear", 2, 4.0, np.nan)] + assert select_by_adequacy(models, 0.05)["model"] == "linear" - def test_tie_break_by_aicc_within_tier(self): - # Two adequate models at the same k -> lower AICc wins. - models = [_rec("repressor", 3, 9.0, 0.3), _rec("inducer", 3, 4.0, 0.4)] - assert select_by_adequacy(models, 0.05) == ("inducer", "adequate") - def test_none_adequate_falls_back_unmodeled(self): - models = [_rec("flat", 1, 5.0, 0.001), _rec("linear", 2, 4.0, 0.002)] - best, status = select_by_adequacy(models, 0.05) - assert best == "linear" # overall AICc-best fallback - assert status == "unmodeled" +class TestShapeStatus: + def test_adequate(self): + assert _shape_status(0.5, 0.05) == "adequate" - def test_unassessable_when_all_runs_nan(self): - models = [_rec("flat", 1, 5.0, np.nan), _rec("linear", 2, 4.0, np.nan)] - best, status = select_by_adequacy(models, 0.05) - assert best == "linear" - assert status == "unassessable" + def test_misfit(self): + assert _shape_status(0.01, 0.05) == "misfit" - def test_empty(self): - assert select_by_adequacy([], 0.05) == (None, "none") + def test_unassessable(self): + assert _shape_status(np.nan, 0.05) == "unassessable" # A titration-like grid with enough points that low-parameter models are usable. @@ -122,21 +136,61 @@ def test_adequacy_columns_present_and_shape(): assert flat_output["shape_status"] == "adequate" -def test_flat_misfit_escalates_off_linear(): - """A clearly curved dataset is not left on flat/linear by adequacy-first.""" - # Monotone-saturating data with small, non-clustered wobble and enough - # points for the runs test to have power. - x = np.array([0.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0, 55.0]) - from tfscreen.mle.curve_models.models import model_hill_4p - y = model_hill_4p([0.05, 0.9, np.log(5.0), 1.5], x) - ys = np.full_like(x, 0.02) - flat_output, _, _ = cat_fit(x, y, ys, - models_to_run=["flat", "linear_log", - "inducer"]) +def _curved_hetero_data(): + """A real curve where the misfit lives in a few precise points and the many + plateau points are noisy -- the heteroscedastic case where the sign-based + runs test on flat is diluted but AICc still sees the curve.""" + x = np.logspace(-8, -2, 15) + lx = np.log10(x) + bump = 1.2 * np.exp(-0.5 * ((lx + 5.0) / 0.6) ** 2) + ystd = np.where(np.abs(lx + 5.0) < 1.2, 0.05, 0.6) + rng = np.random.default_rng(4) + return x, bump + rng.normal(0, ystd), ystd + + +def test_aicc_default_does_not_collapse_to_flat(): + """Regression: default select_by='aicc' keeps the AICc-confident curve even + when the runs test is too weak to flag flat (the reported bug).""" + x, y, ys = _curved_hetero_data() + models = ["flat", "linear_log", "inducer", "bell_peak_log", "bell_dip_log"] + flat_output, _, _ = cat_fit(x, y, ys, models_to_run=models) # default aicc assert flat_output["best_model"] != "flat" - assert flat_output["shape_status"] == "adequate" - # flat's residuals cluster by sign -> flagged inadequate. - assert flat_output["runs_p|flat"] < 0.05 + assert flat_output["best_model"] == flat_output["aicc_best_model"] + + +def test_adequacy_mode_never_demotes_confident_curve(): + """Even in adequacy mode, a confident curved AICc pick is never demoted to + flat just because the diluted runs test can't reject flat.""" + x, y, ys = _curved_hetero_data() + models = ["flat", "linear_log", "inducer", "bell_peak_log", "bell_dip_log"] + flat_output, _, _ = cat_fit(x, y, ys, models_to_run=models, + select_by="adequacy") + assert flat_output["best_model"] != "flat" + + +def test_adequacy_mode_escalates_off_flagged_pick(): + """A clean curved dataset where flat is the AICc pick but is flagged: + adequacy mode escalates to the curved model; aicc mode would keep flat.""" + # Gentle curvature + tiny homoscedastic noise, enough points for power. + x = np.array([0.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0, 55.0]) + lx = np.log10(x + 1.0) + y = 0.15 * lx ** 2 # mild parabola in log-x + ys = np.full_like(x, 0.05) + models = ["flat", "linear_log", "bell_peak_log"] + aicc_out, _, _ = cat_fit(x, y, ys, models_to_run=models, select_by="aicc") + adeq_out, _, _ = cat_fit(x, y, ys, models_to_run=models, + select_by="adequacy") + # flat's residuals cluster (curvature) -> flagged. + assert aicc_out["runs_p|flat"] < 0.05 + if aicc_out["best_model"] == "flat": + # The regime this test targets: adequacy escalates off the flagged flat. + assert adeq_out["best_model"] != "flat" + + +def test_invalid_select_by_raises(): + x, y, ys = _hill_data() + with pytest.raises(ValueError, match="select_by"): + cat_fit(x, y, ys, models_to_run=["flat"], select_by="bogus") def test_aicc_excludes_overparameterized_models(): diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 7a82230..7020bc4 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -62,11 +62,11 @@ def _assess(n=2, y_model=0.0, y_model_std=0.1): def _capturing_fit(store, **fit_kwargs): """A fake cat_fit that records the (x, y, y_std) it was handed per call.""" def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, - best_only=True, alpha=0.05, adequacy_alpha=0.05, - verbose=False): + best_only=True, alpha=0.05, select_by="aicc", + adequacy_alpha=0.05, verbose=False): store.append({"x": list(x), "y": list(y), "y_std": list(y_std), "best_only": best_only, "alpha": alpha, - "adequacy_alpha": adequacy_alpha}) + "select_by": select_by, "adequacy_alpha": adequacy_alpha}) return _flat(**fit_kwargs), _pred(len(x)), _assess(len(np.unique(x))) return fake_fit @@ -169,6 +169,22 @@ def test_adequacy_alpha_threaded_to_fitter(self): y_std="theta_std", adequacy_alpha=0.2) assert all(call["adequacy_alpha"] == 0.2 for call in store) + def test_select_by_threaded_to_fitter(self): + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", select_by="adequacy") + assert all(call["select_by"] == "adequacy" for call in store) + # Default is the robust AICc selection. + store.clear() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std") + assert all(call["select_by"] == "aicc" for call in store) + # --- validation -------------------------------------------------------------- From 54ea3d06c1fb294f3088c28afa8add3c3278991f Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 08:15:32 -0400 Subject: [PATCH 18/26] updating cat_response caller --- CLAUDE.md | 6 +- .../analysis/cat_response/cat_assess.py | 42 +++++ src/tfscreen/analysis/cat_response/cat_fit.py | 161 ++++++++++++++---- .../analysis/cat_response/cat_response.py | 33 ++-- .../analysis/scripts/cat_response_cli.py | 17 +- src/tfscreen/mle/curve_models/__init__.py | 17 ++ .../analysis/cat_response/test_cat_assess.py | 26 +++ .../analysis/cat_response/test_cat_fit.py | 79 ++++++++- .../cat_response/test_cat_response.py | 26 ++- 9 files changed, 352 insertions(+), 55 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ad6dba1..d14b6be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -284,12 +284,14 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( **Model x-scale (concentration-parameterized vs log-conc).** `MODEL_LIBRARY` models are **not** interchangeable on x-scale. The Hill family (`repressor`/`inducer`/`hill_*`) and `biphasic_*` are parameterized in **raw concentration** and take `log(x)` internally (`_hill` does `np.log(x)`), so they are already sigmoids/peaks *in log-concentration* and **must be handed raw x** — feeding them `log10(x)` (negative) both double-logs and NaNs them. The geometric models (`bell_peak`/`bell_dip` Gaussian-in-x, `linear`) are shapes in raw x; their `*_log` counterparts (`bell_peak_log`/`bell_dip_log` = Gaussian in `log10(x)` with a free real `center`, and `linear_log` = line in `log10(x)`) are the log-concentration versions. The `*_log` models own their transform via `models._to_log10_x` (x stays **raw concentration** in the data — there is no `--log_x` flag and no separate log column): `x <= 0` (the no-titrant point) is floored to `min(x[x>0])/100` before the log, computed per-call (identical across groups for a shared titration grid). `flat` is scale-invariant. When adding a new model, decide which camp it's in — never blanket-transform x. `DEFAULT_MODELS` (in `curve_models/__init__.py`) is the curated set fit when `--models`/`models_to_run` is omitted (was: all of `MODEL_LIBRARY`): `flat, linear_log, repressor, inducer, bell_peak_log, bell_dip_log` — one parameterization per qualitative response. All other models (raw-x `bell_*`/`linear`, 4-param `hill_*`, `biphasic_*`) stay registered and reachable via `--models`. -- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py`. **Default `"aicc"`**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). This is the robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. Opt-in `select_by="adequacy"` (`select_by_adequacy`) is an **escalate-only** refinement: keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat` (killing the omnibus "real" call). Per-model diagnostics are reported but **do not gate the default**: `runs_p|*` (sign-based, robust to `y_std` scale; needs `n >= _MIN_RUNS_N` (4), real power only at `n >~ 8`) and weighted-χ² `gof_p|*` (`goodness_of_fit_p`, depends on `y_std` scale). `shape` = qualitative form of `best_model` (`flat`/`linear`/`sigmoid`/`peaked`/`biphasic`, via `_SHAPE_BY_MODEL`); `shape_status` = diagnostic on the **selected** model's residuals (`adequate` / `misfit` / `unassessable` / `none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only in adequacy mode when it escalates). This is a form axis, **orthogonal to** the magnitude/`response_class` axis below. +- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py` (three modes). **`"aicc"` (default)**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). Robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. **`"adequacy"`** (`select_by_adequacy`): **escalate-only** refinement — keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat`. **`"shape"`** (`select_by_shape`): liberal, prior-aligned classifier for *exploration* (AICc is too conservative — its small-n penalty buries a well-fit curve, e.g. an R²=0.96 dip called `flat`). Two steps, **no AICc parsimony**: (1) **flat-vs-curvy** gate on structure in the *flat* fit's residuals — curvy iff `autocorr_p|flat < curvy_cutoff` (weighted Durbin-Watson lag-1 autocorrelation p, `residual_autocorr`; magnitude/`y_std`-aware, so unlike the runs test it isn't washed out by many near-baseline points); (2) among curvy-shape models (step/peak/dip/biphasic; `linear` excluded as unphysical) pick the best **weighted R²**, preferring the simpler within `r2_margin` (0.02). `curvy_cutoff` (default 0.1) is the sweepable knob — run a set and visually inspect. When `models_to_run` is None, shape mode defaults to `SHAPE_MODELS` (physical vocabulary: `flat, inducer, repressor, bell_peak_log, bell_dip_log, biphasic_peak, biphasic_dip` — no `linear_log`; adds biphasic) instead of `DEFAULT_MODELS`. + + Per-model diagnostics are always reported and (except the shape gate) **do not gate selection**: `runs_p|*` (sign-based; needs `n >= _MIN_RUNS_N` (4), power only at `n >~ 8`), `autocorr|*`/`autocorr_p|*` (weighted DW lag-1, the shape gate's signal), weighted-χ² `gof_p|*` (`goodness_of_fit_p`). `shape` = qualitative form of `best_model` (`flat`/`linear`/`step`/`peak`/`dip`/`biphasic`, via `_SHAPE_BY_MODEL`; `_CURVY_SHAPES` = the non-flat/non-linear ones); `shape_status` = runs-test diagnostic on the **selected** model (`adequate`/`misfit`/`unassessable`/`none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only when adequacy escalates or shape reclassifies). This form axis is **orthogonal to** the magnitude/`response_class` axis below — the intended exploratory hierarchy is their cross: `response_class` gives *flat-real / can't-tell (indeterminate) / confident_zero*, and `shape` gives *flat vs which kind of curvy*. - **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading the *best* model against zero. Per-point `sig_nonzero` z-test at the observed x; a per-curve **omnibus** Wald/χ² `W = yhat @ pinv(Σ_yhat) @ yhat` (df = rank(Σ) = n_params) using the **full** prediction covariance (`predict_with_error(..., full_cov=True)` = `J·Cov·Jᵀ`), which rewards both magnitude and how-many-points and handles the strong correlation among predicted points; then **Benjamini-Hochberg** across curves → `omnibus_q`. `equiv_zero` (a point's whole CI ⊂ `[-delta, delta]`) is the confidence axis that separates "confidently flat" from "too noisy to tell". `delta` defaults to `delta_c * median(predicted y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits — hence the post-hoc pass); pass `--delta` for a fixed biological region. `response_class` is 3-valued with **equivalence taking precedence over significance** (a tiny-but-significant offset inside the ROPE is still practically zero): `confident_zero` (all points equiv) → else `real` (omnibus_q < alpha) → else `indeterminate` (the "flat because error is high" bucket to exclude downstream). -Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_gof_p`, `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. +Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. ## YAML Standards diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py index 196db09..a5d594b 100644 --- a/src/tfscreen/analysis/cat_response/cat_assess.py +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -186,6 +186,48 @@ def residual_runs_p(resid): return float(norm.cdf(z)) +def residual_autocorr(resid): + """ + Lag-1 autocorrelation of residuals ordered along the independent variable. + + A *weighted*, magnitude-aware structure detector (pass the standardized + residuals ``(y - yfit) / y_std``): smooth systematic misfit makes consecutive + residuals track each other, so a bounded flat/constant fit to a real curve + shows strong positive autocorrelation. Unlike the sign-based runs test, this + is not washed out by many near-baseline points, so it catches structure the + runs test misses on noisy, heteroscedastic (logit) data. + + Parameters + ---------- + resid : array-like + Residuals in independent-variable order. Non-finite values are dropped. + + Returns + ------- + (autocorr, autocorr_p) : (float, float) + ``autocorr`` is the Durbin-Watson lag-1 autocorrelation estimate + ``1 - DW/2`` (~0 = no structure, ->1 = smooth positive autocorrelation). + ``autocorr_p`` is a one-sided (positive-autocorrelation) p-value from the + normal approximation ``DW ~ N(2, 4/n)``; small = systematic structure. + Both NaN when fewer than ``_MIN_RUNS_N`` residuals or all-zero. + """ + resid = np.asarray(resid, dtype=float) + resid = resid[np.isfinite(resid)] + n = resid.size + if n < _MIN_RUNS_N: + return np.nan, np.nan + + ss = float(np.sum(resid ** 2)) + if ss <= 0.0: + return np.nan, np.nan + + dw = float(np.sum(np.diff(resid) ** 2) / ss) + autocorr = 1.0 - dw / 2.0 + z = (dw - 2.0) / (2.0 / np.sqrt(n)) + autocorr_p = float(norm.cdf(z)) # lower tail: DW < 2 -> positive autocorr + return autocorr, autocorr_p + + def goodness_of_fit_p(chi2_w, n, k): """ Lack-of-fit p-value from the weighted chi-square of a fit. diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index 33e6eeb..fc262c9 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -1,4 +1,4 @@ -from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS, SHAPE_MODELS from tfscreen.mle import ( run_least_squares, @@ -7,7 +7,9 @@ ) from tfscreen.util.numerical import xfill -from .cat_assess import assess_best_model, residual_runs_p, goodness_of_fit_p +from .cat_assess import ( + assess_best_model, residual_runs_p, residual_autocorr, goodness_of_fit_p, +) import numpy as np import pandas as pd @@ -18,13 +20,77 @@ _SHAPE_BY_MODEL = { "flat": "flat", "linear": "linear", "linear_log": "linear", - "repressor": "sigmoid", "inducer": "sigmoid", - "hill_repressor": "sigmoid", "hill_inducer": "sigmoid", - "bell_peak": "peaked", "bell_dip": "peaked", - "bell_peak_log": "peaked", "bell_dip_log": "peaked", + "repressor": "step", "inducer": "step", + "hill_repressor": "step", "hill_inducer": "step", + "bell_peak": "peak", "bell_peak_log": "peak", + "bell_dip": "dip", "bell_dip_log": "dip", "biphasic_peak": "biphasic", "biphasic_dip": "biphasic", } +# Shapes considered "curvy" (real, non-flat responses) by the shape classifier. +_CURVY_SHAPES = ("step", "peak", "dip", "biphasic") + + +def _resolve_models(models_to_run, select_by): + """The model set to fit: explicit list, else the default for this mode.""" + if models_to_run is not None: + return models_to_run + return list(SHAPE_MODELS if select_by == "shape" else DEFAULT_MODELS) + + +def select_by_shape(models, curvy_cutoff, r2_margin=0.02): + """ + Liberal, prior-aligned shape classifier (the ``select_by="shape"`` mode). + + A two-step decision that deliberately does *not* use AICc parsimony (too + conservative at small n -- it buries a well-fit curve behind the penalty for + its extra parameters): + + 1. **flat vs curvy** -- gate on structure in the *flat* fit's residuals: the + curve is "curvy" when flat's residual autocorrelation is significant + (``autocorr_p < curvy_cutoff``). This is the sweepable knob. + 2. **which curvy shape** -- among the curvy-shape models (step/peak/dip/ + biphasic; ``linear`` is excluded as unphysical), pick the best-fitting one + by weighted R2, preferring the simpler model when two fit within + ``r2_margin``. Fit quality, not AICc, decides -- so an R2=0.96 dip is + chosen over an R2=0.5 step even though the step has fewer parameters. + + Parameters + ---------- + models : list of dict + Records with keys ``model``, ``k``, ``AICc``, ``R2``, ``autocorr_p`` + (successful, selectable fits). Non-empty. + curvy_cutoff : float + Threshold on the flat fit's ``autocorr_p``. Larger = more curves called + curvy (more liberal). Sweep this and inspect. + r2_margin : float, optional + Two curvy models within this weighted-R2 margin are treated as + equivalent; the simpler (fewer-parameter) one wins. Default 0.02. + + Returns + ------- + dict + The selected model's record. + """ + by_name = {m["model"]: m for m in models} + flat = by_name.get("flat") + aicc_best = min(models, key=lambda m: m["AICc"]) + + structured = (flat is not None and np.isfinite(flat["autocorr_p"]) + and flat["autocorr_p"] < curvy_cutoff) + if not structured: + return flat if flat is not None else aicc_best + + curvy = [m for m in models + if _SHAPE_BY_MODEL.get(m["model"]) in _CURVY_SHAPES + and np.isfinite(m["R2"])] + if not curvy: + return flat if flat is not None else aicc_best + + best_r2 = max(m["R2"] for m in curvy) + cands = [m for m in curvy if m["R2"] >= best_r2 - r2_margin] + return min(cands, key=lambda m: (m["k"], m["AICc"])) + def _shape_status(runs_p, adequacy_alpha): """Diagnostic label for the selected model from its runs-test p-value.""" @@ -107,6 +173,8 @@ def _set_no_best_model(flat_output): flat_output["best_model_AIC_weight"] = np.nan flat_output["best_model_gof_p"] = np.nan flat_output["best_model_runs_p"] = np.nan + flat_output["best_model_autocorr"] = np.nan + flat_output["best_model_autocorr_p"] = np.nan flat_output["shape"] = "none" flat_output["shape_status"] = "none" @@ -118,7 +186,8 @@ def _empty_assess_df(): def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, - alpha=0.05, select_by="aicc", adequacy_alpha=0.05, verbose=False): + alpha=0.05, select_by="aicc", adequacy_alpha=0.05, + curvy_cutoff=0.1, verbose=False): """ Fits multiple models to a single dataset and returns a flat dictionary of all results, suitable for aggregation. @@ -126,20 +195,26 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, Model selection (``select_by``): - ``"aicc"`` (default): ``best_model`` is the lowest-AICc model (the small- - sample-corrected AIC on the weighted residuals). This is the robust default - -- the weighted chi2 correctly weights the informative points, which the - sign-based runs test does not. + sample-corrected AIC on the weighted residuals). Robust default -- the + weighted chi2 correctly weights the informative points. - ``"adequacy"``: escalate-only refinement of the AICc pick -- keep it unless - its residuals are systematically structured, in which case move to a - no-simpler adequate model (see :func:`select_by_adequacy`). Never demotes, - so it cannot collapse a confident curved fit to flat. - - Either way, per-model diagnostics are reported but do **not** gate the default - selection: the runs-test p (``runs_p|*``; one-sided, sign-based, robust to - ``y_std`` scale) and the weighted-chi2 goodness-of-fit p (``gof_p|*``). The - selected model's ``shape``/``shape_status`` summarize its form and whether its - residuals look adequate. After selection, the best model is evaluated at the - observed x and tested against zero (see :mod:`cat_assess`). + its residuals are systematically structured, then move to a no-simpler + adequate model (see :func:`select_by_adequacy`). Never demotes. + - ``"shape"``: liberal, prior-aligned shape classifier for exploration (see + :func:`select_by_shape`). Gates flat-vs-curvy on the flat fit's residual + autocorrelation (``autocorr_p < curvy_cutoff``), then names the curvy shape + by best weighted R2 -- *not* AICc, so a well-fit curve is not buried by the + parsimony penalty. When ``models_to_run`` is None this mode defaults to the + physical ``SHAPE_MODELS`` vocabulary (no ``linear``; includes biphasic). + + Per-model diagnostics are always reported: runs-test p (``runs_p|*``; sign- + based, robust to ``y_std`` scale), residual autocorrelation and its p + (``autocorr|*`` / ``autocorr_p|*``; weighted, the shape gate's signal), and + the weighted-chi2 goodness-of-fit p (``gof_p|*``). The selected model's + ``shape`` (flat/step/peak/dip/biphasic) and ``shape_status`` (runs-test + diagnostic on the pick) summarize its form. After selection, the best model + is evaluated at the observed x and tested against zero (see + :mod:`cat_assess`). Parameters ---------- @@ -159,15 +234,21 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, alpha : float, optional Two-sided significance level for the per-point ``sig_nonzero`` test. Default 0.05. - select_by : {"aicc", "adequacy"}, optional + select_by : {"aicc", "adequacy", "shape"}, optional Model-selection strategy. ``"aicc"`` (default) picks the lowest-AICc model. ``"adequacy"`` starts from the AICc pick and escalates to a - no-simpler adequate model only if the AICc pick's residuals are flagged - as structured (never demotes). Default ``"aicc"``. + no-simpler adequate model only if flagged (never demotes). ``"shape"`` + is the liberal shape classifier (structure-gated flat-vs-curvy, then + best-R2 curvy shape). Default ``"aicc"``. adequacy_alpha : float, optional Runs-test threshold used for the ``shape_status`` diagnostic and (when ``select_by="adequacy"``) for escalation. ``runs_p`` below this flags systematic residuals. Default 0.05. + curvy_cutoff : float, optional + Only used when ``select_by="shape"``. A curve is classified "curvy" + (rather than flat) when the flat fit's residual-autocorrelation p-value + ``autocorr_p`` is below this. Larger = more liberal (more curves called + curvy). Default 0.1. verbose : bool, optional If True, prints warnings to the console when a model fails to fit. Defaults to False. @@ -192,14 +273,14 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, downstream in ``cat_response`` once the global delta is known.) """ - if models_to_run is None: - models_to_run = list(DEFAULT_MODELS) - - if select_by not in ("aicc", "adequacy"): + if select_by not in ("aicc", "adequacy", "shape"): raise ValueError( - f"select_by must be 'aicc' or 'adequacy', got {select_by!r}" + "select_by must be 'aicc', 'adequacy', or 'shape', got " + f"{select_by!r}" ) + models_to_run = _resolve_models(models_to_run, select_by) + flat_output = {} # Sanitize inputs @@ -230,6 +311,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, flat_output[f"AIC_weight|{name}"] = np.nan flat_output[f"gof_p|{name}"] = np.nan flat_output[f"runs_p|{name}"] = np.nan + flat_output[f"autocorr|{name}"] = np.nan + flat_output[f"autocorr_p|{name}"] = np.nan for p_name in param_names: flat_output[f"{name}|{p_name}|est"] = np.nan flat_output[f"{name}|{p_name}|std"] = np.nan @@ -285,11 +368,13 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, resid = (y - y_fit) / y_std chi2 = float(np.sum(resid ** 2)) - # Adequacy diagnostics. runs_p (primary, scale-robust) tests the - # sign order of the residuals along x for systematic structure; - # gof_p is the absolute weighted-chi2 lack-of-fit p (reported only). + # Structure / adequacy diagnostics on the residuals ordered by x. + # runs_p (sign-based, scale-robust); autocorr/autocorr_p (weighted + # Durbin-Watson lag-1 -- the shape gate's signal); gof_p (absolute + # weighted-chi2 lack-of-fit). All reported; none gate the default. order = np.argsort(x, kind="stable") runs_p = residual_runs_p(resid[order]) + autocorr, autocorr_p = residual_autocorr(resid[order]) gof_p = goodness_of_fit_p(chi2, n, k) w = 1.0 / (y_std ** 2) @@ -324,6 +409,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, summary_results.append({"model": name, "k": k, "R2": r2, "chi2": chi2, "AIC": aic, "AICc": aicc, "gof_p": gof_p, "runs_p": runs_p, + "autocorr": autocorr, + "autocorr_p": autocorr_p, "success": True}) param_results[name] = {"params": params, "std_err": std_err, "cov": cov_matrix, "names": param_names, @@ -336,7 +423,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, summary_results.append({"model": name, "k": k, "R2": np.nan, "chi2": np.nan, "AIC": np.nan, "AICc": np.nan, "gof_p": np.nan, - "runs_p": np.nan, "success": False}) + "runs_p": np.nan, "autocorr": np.nan, + "autocorr_p": np.nan, "success": False}) param_results[name] = { "params": np.full(k, np.nan), "std_err": np.full(k, np.nan), "cov": None, "names": param_names, "model_func": model_func @@ -367,6 +455,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, flat_output[f"R2|{model_name}"] = row['R2'] flat_output[f"gof_p|{model_name}"] = row['gof_p'] flat_output[f"runs_p|{model_name}"] = row['runs_p'] + flat_output[f"autocorr|{model_name}"] = row['autocorr'] + flat_output[f"autocorr_p|{model_name}"] = row['autocorr_p'] p_res = param_results[model_name] for i, p_name in enumerate(p_res['names']): @@ -388,9 +478,12 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, # adequacy refinement ("adequacy"). aicc_best_model records the AICc pick # for transparency when the two diverge. aicc_best_model = summary_df.iloc[0]['model'] - model_records = valid[['model', 'k', 'AICc', 'runs_p']].to_dict('records') + model_records = valid[['model', 'k', 'AICc', 'R2', 'runs_p', + 'autocorr_p']].to_dict('records') if select_by == "adequacy": chosen = select_by_adequacy(model_records, adequacy_alpha) + elif select_by == "shape": + chosen = select_by_shape(model_records, curvy_cutoff) else: chosen = min(model_records, key=lambda m: m["AICc"]) best_model = chosen["model"] @@ -402,6 +495,8 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, flat_output['best_model_AIC_weight'] = best_row['AIC_weight'] flat_output['best_model_gof_p'] = best_row['gof_p'] flat_output['best_model_runs_p'] = best_row['runs_p'] + flat_output['best_model_autocorr'] = best_row['autocorr'] + flat_output['best_model_autocorr_p'] = best_row['autocorr_p'] flat_output['shape'] = _SHAPE_BY_MODEL.get(best_model, "other") flat_output['shape_status'] = _shape_status(best_row['runs_p'], adequacy_alpha) diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 6e623af..395e8e6 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -20,7 +20,7 @@ from .cat_fit import cat_fit from .cat_assess import compute_delta, classify_equiv, benjamini_hochberg -from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS, SHAPE_MODELS from tfscreen.util import resolve_workers from tfscreen.util.numerical import xfill @@ -31,7 +31,7 @@ def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, best_only, alpha, - select_by, adequacy_alpha, verbose): + select_by, adequacy_alpha, curvy_cutoff, verbose): """Run cat_fit for one group and tag the results with the group key.""" flat_out, pred_df, assess_df = cat_fit(x, y, y_std, x_pred=x_pred, @@ -40,6 +40,7 @@ def _fit_one(group_key, x, y, y_std, x_pred, models_to_run, best_only, alpha, alpha=alpha, select_by=select_by, adequacy_alpha=adequacy_alpha, + curvy_cutoff=curvy_cutoff, verbose=verbose) for col, val in group_key.items(): flat_out[col] = val @@ -69,6 +70,7 @@ def cat_response(df, alpha=0.05, select_by="aicc", adequacy_alpha=0.05, + curvy_cutoff=0.1, delta=None, delta_c=2.0, num_workers=1, @@ -110,14 +112,20 @@ def cat_response(df, the confidence level of the ``equiv_zero`` interval. Also the threshold applied to omnibus q-values when assigning ``response_class``. Default 0.05. - select_by : {"aicc", "adequacy"}, optional + select_by : {"aicc", "adequacy", "shape"}, optional Model-selection strategy (see :func:`cat_fit`). ``"aicc"`` (default) selects the lowest-AICc model. ``"adequacy"`` keeps the AICc pick unless - its residuals are systematically structured, then escalates to a - no-simpler adequate model (never demotes). Default ``"aicc"``. + flagged, then escalates to a no-simpler adequate model (never demotes). + ``"shape"`` is the liberal shape classifier (structure-gated flat-vs- + curvy, then best-R2 curvy shape); with ``models_to_run=None`` it defaults + to the physical ``SHAPE_MODELS`` vocabulary. Default ``"aicc"``. adequacy_alpha : float, optional Runs-test threshold used for the ``shape_status`` diagnostic and, when ``select_by="adequacy"``, for escalation. Default 0.05. + curvy_cutoff : float, optional + Only used when ``select_by="shape"``: the flat-vs-curvy gate on the flat + fit's residual-autocorrelation p-value (larger = more liberal). Sweep it + and inspect. Default 0.1. delta : float or None, optional Region-of-practical-equivalence half-width around zero. If None (default), computed globally as ``delta_c * median(predicted y_std)`` @@ -147,10 +155,12 @@ def cat_response(df, ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, ``response_class``). ``best_model`` follows ``select_by`` (default lowest-AICc; see :func:`cat_fit`), ``shape`` is its qualitative form - (flat/linear/sigmoid/peaked/biphasic), and ``shape_status`` is a + (flat/linear/step/peak/dip/biphasic), and ``shape_status`` is a diagnostic on the selected model's residuals (adequate / misfit / - unassessable). Column names keep the ``|`` delimiter; presentation is - left to callers. + unassessable). Per-model ``autocorr|*`` / ``autocorr_p|*`` (residual + autocorrelation, the shape gate's signal) and ``gof_p|*`` are also + included. Column names keep the ``|`` delimiter; presentation is left to + callers. predictions_df : pandas.DataFrame Predicted curves, concatenated across groups. Columns are the group-key columns followed by ``model``, ``x``, ``y_model``, ``y_model_std``, @@ -166,7 +176,10 @@ def cat_response(df, ``delta_c`` if not supplied). """ if models_to_run is None: - models_to_run = list(DEFAULT_MODELS) + # The "shape" classifier defaults to the physical shape vocabulary + # (no linear; includes biphasic); other modes use DEFAULT_MODELS. + models_to_run = list(SHAPE_MODELS if select_by == "shape" + else DEFAULT_MODELS) bad = [m for m in models_to_run if m not in MODEL_LIBRARY] if bad: @@ -205,7 +218,7 @@ def cat_response(df, work_items.append((group_key, x, y, ys, x_pred, models_to_run, best_only, alpha, select_by, adequacy_alpha, - verbose)) + curvy_cutoff, verbose)) n_total = len(work_items) if n_total == 0: diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index 92ade36..5e9bf7a 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -65,6 +65,7 @@ def cat_response(data_file, alpha=0.05, select_by="aicc", adequacy_alpha=0.05, + curvy_cutoff=0.1, delta=None, delta_c=2.0, write_all_predictions=False, @@ -117,14 +118,21 @@ def cat_response(data_file, alpha : float, optional Significance level for the per-point tests and the omnibus q-value threshold used to call a curve 'real'. Default 0.05. - select_by : {"aicc", "adequacy"}, optional + select_by : {"aicc", "adequacy", "shape"}, optional Model-selection strategy. ``"aicc"`` (default) selects the lowest-AICc - model. ``"adequacy"`` keeps the AICc pick unless its residuals are - systematically structured, then escalates to a no-simpler adequate model - (never demotes). Default ``"aicc"``. + model. ``"adequacy"`` keeps the AICc pick unless flagged, then escalates + to a no-simpler adequate model (never demotes). ``"shape"`` is the + liberal shape classifier: it gates flat-vs-curvy on the flat fit's + residual autocorrelation and names the curvy shape by best R2 (defaults + to the physical ``SHAPE_MODELS`` vocabulary -- no linear, includes + biphasic). Default ``"aicc"``. adequacy_alpha : float, optional Runs-test threshold for the ``shape_status`` diagnostic and, when ``select_by="adequacy"``, for escalation. Default 0.05. + curvy_cutoff : float, optional + Only used when ``select_by="shape"``: flat-vs-curvy gate on the flat + fit's residual-autocorrelation p-value (larger = more curves called + curvy). Sweep it and visually inspect. Default 0.1. delta : float or None, optional Region-of-practical-equivalence half-width around zero. If None (default), derived globally as ``delta_c * median(predicted y_std)``. @@ -182,6 +190,7 @@ def cat_response(data_file, alpha=alpha, select_by=select_by, adequacy_alpha=adequacy_alpha, + curvy_cutoff=curvy_cutoff, delta=delta, delta_c=delta_c, num_workers=num_workers, diff --git a/src/tfscreen/mle/curve_models/__init__.py b/src/tfscreen/mle/curve_models/__init__.py index 32dbe79..9999a87 100644 --- a/src/tfscreen/mle/curve_models/__init__.py +++ b/src/tfscreen/mle/curve_models/__init__.py @@ -167,3 +167,20 @@ "bell_peak_log", "bell_dip_log", ] + +# Curated "physical shape" vocabulary used by tfs-cat-response when +# ``select_by="shape"`` and no models are given. Same log-concentration set as +# DEFAULT_MODELS but with ``linear_log`` dropped (a sloped line is not a +# physical titration response -- only ever the middle of a sigmoid) and the +# biphasic peak/dip shapes added (for dispersive responses). Shapes: +# flat -> flat; inducer/repressor -> step; bell_*_log -> peak/dip; +# biphasic_* -> biphasic. +SHAPE_MODELS = [ + "flat", + "inducer", + "repressor", + "bell_peak_log", + "bell_dip_log", + "biphasic_peak", + "biphasic_dip", +] diff --git a/tests/tfscreen/analysis/cat_response/test_cat_assess.py b/tests/tfscreen/analysis/cat_response/test_cat_assess.py index e3ed82f..c953f03 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_assess.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -12,11 +12,37 @@ classify_equiv, benjamini_hochberg, residual_runs_p, + residual_autocorr, goodness_of_fit_p, _omnibus_chi2, ) +# --- residual autocorrelation (shape gate) ----------------------------------- + +class TestResidualAutocorr: + def test_smooth_structure_positive_and_significant(self): + # A smooth ramp = strong positive autocorrelation -> small p. + resid = np.array([-3, -2, -1, 0, 1, 2, 3, 4], dtype=float) + ac, p = residual_autocorr(resid) + assert ac > 0.5 + assert p < 0.05 + + def test_alternating_not_positive(self): + resid = np.array([1, -1, 1, -1, 1, -1, 1, -1], dtype=float) + ac, p = residual_autocorr(resid) + assert ac < 0.0 # negative autocorrelation + assert p > 0.5 # not flagged as positive structure + + def test_too_few_points_is_nan(self): + ac, p = residual_autocorr(np.array([1.0, -1.0, 1.0])) + assert np.isnan(ac) and np.isnan(p) + + def test_all_zero_is_nan(self): + ac, p = residual_autocorr(np.zeros(6)) + assert np.isnan(ac) and np.isnan(p) + + # --- residual runs test ------------------------------------------------------ class TestResidualRunsP: diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index d0cc08a..243b388 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -7,9 +7,9 @@ import pytest from tfscreen.analysis.cat_response.cat_fit import ( - cat_fit, select_by_adequacy, _shape_status, + cat_fit, select_by_adequacy, select_by_shape, _shape_status, ) -from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS, SHAPE_MODELS # --- escalate-only adequacy selection ---------------------------------------- @@ -63,6 +63,75 @@ def test_unassessable(self): assert _shape_status(np.nan, 0.05) == "unassessable" +# --- shape classifier -------------------------------------------------------- + +def _srec(model, k, aicc, r2, autocorr_p): + return {"model": model, "k": k, "AICc": aicc, "R2": r2, + "autocorr_p": autocorr_p} + + +class TestSelectByShape: + def test_flat_when_no_structure(self): + # flat autocorr_p above cutoff -> not curvy -> flat, even if a curve fits. + models = [_srec("flat", 1, 5.0, 0.0, 0.6), + _srec("bell_dip_log", 4, 2.0, 0.95, 0.6)] + assert select_by_shape(models, curvy_cutoff=0.1)["model"] == "flat" + + def test_curvy_picks_best_r2_shape(self): + # flat is structured (low autocorr_p); the dip fits far better than the + # step, so it's chosen despite more parameters (no AICc penalty). + models = [_srec("flat", 1, 2.0, -0.05, 0.02), + _srec("inducer", 3, 4.0, 0.51, 0.3), + _srec("bell_dip_log", 4, 8.0, 0.96, 0.3)] + chosen = select_by_shape(models, curvy_cutoff=0.1) + assert chosen["model"] == "bell_dip_log" # -> shape "dip" + + def test_prefers_simpler_within_r2_margin(self): + # step and dip fit within r2_margin -> the simpler (step) wins. + models = [_srec("flat", 1, 2.0, -0.05, 0.02), + _srec("inducer", 3, 4.0, 0.951, 0.3), + _srec("bell_dip_log", 4, 3.0, 0.955, 0.3)] + chosen = select_by_shape(models, curvy_cutoff=0.1, r2_margin=0.02) + assert chosen["model"] == "inducer" + + def test_curvy_but_no_curvy_model_falls_back_to_flat(self): + models = [_srec("flat", 1, 2.0, -0.05, 0.02), + _srec("linear_log", 2, 1.0, 0.3, 0.02)] # linear is not curvy + assert select_by_shape(models, curvy_cutoff=0.1)["model"] == "flat" + + def test_cutoff_controls_flat_vs_curvy(self): + models = [_srec("flat", 1, 2.0, -0.05, 0.08), + _srec("inducer", 3, 4.0, 0.9, 0.08)] + # strict cutoff -> flat; liberal cutoff -> curvy + assert select_by_shape(models, curvy_cutoff=0.05)["model"] == "flat" + assert select_by_shape(models, curvy_cutoff=0.20)["model"] == "inducer" + + +def test_shape_mode_default_models(): + """select_by='shape' with models_to_run=None fits the SHAPE_MODELS set.""" + x, y, ys = _hill_data() + flat_output, _, _ = cat_fit(x, y, ys, select_by="shape") + fit_models = {k.split("|", 1)[1] for k in flat_output + if k.startswith("AIC_weight|")} + assert fit_models == set(SHAPE_MODELS) + assert "linear_log" not in fit_models + assert "biphasic_peak" in fit_models + + +def test_shape_mode_classifies_dip(): + """The reported real-data dip (called flat by AICc) -> 'dip' in shape mode.""" + conc = np.array([0, 1e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 1.0]) + ep = np.array([0.00922, -2.4289, -3.0633, -3.2761, -2.5217, -1.3812, + -0.6332, 2.1689]) + es = np.array([1.4067, 1.5657, 0.9956, 0.8424, 1.2566, 1.2217, 1.2639, + 2.6622]) + aicc_out, _, _ = cat_fit(conc, ep, es, select_by="aicc") + shape_out, _, _ = cat_fit(conc, ep, es, select_by="shape", curvy_cutoff=0.1) + assert aicc_out["best_model"] == "flat" # AICc buries the dip + assert shape_out["shape"] in ("dip", "biphasic") # classifier recovers it + assert shape_out["best_model"] != "flat" + + # A titration-like grid with enough points that low-parameter models are usable. X = np.array([0.0, 1.0, 3.0, 10.0, 30.0, 100.0]) @@ -127,12 +196,14 @@ def test_adequacy_columns_present_and_shape(): models_to_run=["flat", "hill_inducer"]) # New diagnostic + shape columns are emitted. for key in ["aicc_best_model", "best_model_gof_p", "best_model_runs_p", + "best_model_autocorr", "best_model_autocorr_p", "shape", "shape_status", "gof_p|flat", "runs_p|flat", + "autocorr|flat", "autocorr_p|flat", "gof_p|hill_inducer", "runs_p|hill_inducer"]: assert key in flat_output - # Sigmoid data -> hill_inducer selected -> shape "sigmoid", adequate. + # Sigmoid data -> hill_inducer selected -> shape "step", adequate. assert flat_output["best_model"] == "hill_inducer" - assert flat_output["shape"] == "sigmoid" + assert flat_output["shape"] == "step" assert flat_output["shape_status"] == "adequate" diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 7020bc4..b52f3b6 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -63,10 +63,12 @@ def _capturing_fit(store, **fit_kwargs): """A fake cat_fit that records the (x, y, y_std) it was handed per call.""" def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, alpha=0.05, select_by="aicc", - adequacy_alpha=0.05, verbose=False): + adequacy_alpha=0.05, curvy_cutoff=0.1, verbose=False): store.append({"x": list(x), "y": list(y), "y_std": list(y_std), "best_only": best_only, "alpha": alpha, - "select_by": select_by, "adequacy_alpha": adequacy_alpha}) + "select_by": select_by, "adequacy_alpha": adequacy_alpha, + "curvy_cutoff": curvy_cutoff, + "models_to_run": models_to_run}) return _flat(**fit_kwargs), _pred(len(x)), _assess(len(np.unique(x))) return fake_fit @@ -185,6 +187,26 @@ def test_select_by_threaded_to_fitter(self): y_std="theta_std") assert all(call["select_by"] == "aicc" for call in store) + def test_curvy_cutoff_threaded_to_fitter(self): + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", curvy_cutoff=0.25) + assert all(call["curvy_cutoff"] == 0.25 for call in store) + + def test_shape_mode_defaults_to_shape_models(self): + from tfscreen.mle.curve_models import SHAPE_MODELS + store = [] + df = _basic_df() + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit(store)): + cat_response(df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", select_by="shape") + assert all(call["models_to_run"] == list(SHAPE_MODELS) + for call in store) + # --- validation -------------------------------------------------------------- From cc4442a75b4e7777aaca87f4ff9cfed628e737d4 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 09:46:39 -0400 Subject: [PATCH 19/26] improving cat response --- CLAUDE.md | 6 +- .../analysis/cat_response/cat_assess.py | 85 ++++++++++++++----- src/tfscreen/analysis/cat_response/cat_fit.py | 31 ++++--- .../analysis/cat_response/cat_response.py | 51 ++++++----- .../analysis/cat_response/test_cat_assess.py | 62 ++++++++++++-- .../cat_response/test_cat_response.py | 37 +++++--- 6 files changed, 199 insertions(+), 73 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d14b6be..7630434 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -287,11 +287,11 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( - **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py` (three modes). **`"aicc"` (default)**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). Robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. **`"adequacy"`** (`select_by_adequacy`): **escalate-only** refinement — keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat`. **`"shape"`** (`select_by_shape`): liberal, prior-aligned classifier for *exploration* (AICc is too conservative — its small-n penalty buries a well-fit curve, e.g. an R²=0.96 dip called `flat`). Two steps, **no AICc parsimony**: (1) **flat-vs-curvy** gate on structure in the *flat* fit's residuals — curvy iff `autocorr_p|flat < curvy_cutoff` (weighted Durbin-Watson lag-1 autocorrelation p, `residual_autocorr`; magnitude/`y_std`-aware, so unlike the runs test it isn't washed out by many near-baseline points); (2) among curvy-shape models (step/peak/dip/biphasic; `linear` excluded as unphysical) pick the best **weighted R²**, preferring the simpler within `r2_margin` (0.02). `curvy_cutoff` (default 0.1) is the sweepable knob — run a set and visually inspect. When `models_to_run` is None, shape mode defaults to `SHAPE_MODELS` (physical vocabulary: `flat, inducer, repressor, bell_peak_log, bell_dip_log, biphasic_peak, biphasic_dip` — no `linear_log`; adds biphasic) instead of `DEFAULT_MODELS`. Per-model diagnostics are always reported and (except the shape gate) **do not gate selection**: `runs_p|*` (sign-based; needs `n >= _MIN_RUNS_N` (4), power only at `n >~ 8`), `autocorr|*`/`autocorr_p|*` (weighted DW lag-1, the shape gate's signal), weighted-χ² `gof_p|*` (`goodness_of_fit_p`). `shape` = qualitative form of `best_model` (`flat`/`linear`/`step`/`peak`/`dip`/`biphasic`, via `_SHAPE_BY_MODEL`; `_CURVY_SHAPES` = the non-flat/non-linear ones); `shape_status` = runs-test diagnostic on the **selected** model (`adequate`/`misfit`/`unassessable`/`none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only when adequacy escalates or shape reclassifies). This form axis is **orthogonal to** the magnitude/`response_class` axis below — the intended exploratory hierarchy is their cross: `response_class` gives *flat-real / can't-tell (indeterminate) / confident_zero*, and `shape` gives *flat vs which kind of curvy*. -- **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading the *best* model against zero. Per-point `sig_nonzero` z-test at the observed x; a per-curve **omnibus** Wald/χ² `W = yhat @ pinv(Σ_yhat) @ yhat` (df = rank(Σ) = n_params) using the **full** prediction covariance (`predict_with_error(..., full_cov=True)` = `J·Cov·Jᵀ`), which rewards both magnitude and how-many-points and handles the strong correlation among predicted points; then **Benjamini-Hochberg** across curves → `omnibus_q`. +- **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading each curve against zero **on the observed data, not the fitted curve** (`assess_best_model` takes `y_obs`/`y_std`). The driver is a model-free **portmanteau** `nonzero_chi2 = sum((y_obs/y_std)**2) ~ χ²(n)` (`_nonzero_chi2`) → `nonzero_p` → **Benjamini-Hochberg** across curves → `nonzero_q`. This replaced a model-based **omnibus** `W = yhat @ pinv(J·Cov·Jᵀ) @ yhat` as the gate because that test reads the *fitted* curve's covariance, which is wildly overconfident when a flexible model is fit to noisy data (it called curves whose observed error bars all overlap zero "real"). The omnibus (`omnibus_W/df/p/q`) is **still computed and reported** but **gates nothing**; `y_model`/`y_model_std` are still emitted for plotting. Per-point `sig_nonzero`/`z`/`direction` also use observed `y_obs/y_std`. -`equiv_zero` (a point's whole CI ⊂ `[-delta, delta]`) is the confidence axis that separates "confidently flat" from "too noisy to tell". `delta` defaults to `delta_c * median(predicted y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits — hence the post-hoc pass); pass `--delta` for a fixed biological region. `response_class` is 3-valued with **equivalence taking precedence over significance** (a tiny-but-significant offset inside the ROPE is still practically zero): `confident_zero` (all points equiv) → else `real` (omnibus_q < alpha) → else `indeterminate` (the "flat because error is high" bucket to exclude downstream). +`equiv_zero` (a point's whole **observed** CI `|y_obs| + z·y_std ⊂ [-delta, delta]`) is the confidence axis separating "confidently flat" from "too noisy to tell"; `delta` defaults to `delta_c * median(observed y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits), or pass `--delta` for a fixed biological region. `response_class` is 3-valued, **real-first** (differs-from-zero is checked first, per the design decision — a real signal wins even if it lands inside the ROPE): `real` (`nonzero_q < alpha`) → else `confident_zero` (all points `equiv_zero` — confidently flat at zero) → else `indeterminate` (not distinguishable from zero and error bars too wide for the ROPE = "can't tell"). This magnitude axis is **orthogonal to** the `shape` axis above; the exploratory read is their cross-tab (filter `response_class == real`, then look at `shape`). -Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. +Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `response_class`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. ## YAML Standards diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py index a5d594b..52c143e 100644 --- a/src/tfscreen/analysis/cat_response/cat_assess.py +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -36,9 +36,20 @@ _MIN_RUNS_N = 4 -def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): +def assess_best_model(model_func, params, cov_matrix, x, y_obs, y_std, + alpha=0.05): """ - Evaluate the best-fit model at the observed x and test it against zero. + Grade the best-fit curve against zero -- data-driven, with the model test + reported alongside. + + The "distinguishable from zero" decision uses the **observed** points + (``y_obs`` +/- ``y_std``), not the fitted curve: a flexible model fit to + noisy data reports an overconfident curve, so its propagated error can call a + curve "nonzero" even when every observed error bar overlaps zero. The + per-point z-test, the ``nonzero`` portmanteau chi-square, and the equivalence + test all read the observed errors. The fitted curve (``y_model`` / + ``y_model_std``) is still returned for plotting, and the model-based omnibus + (``omnibus_*``) is still computed but is reported-only (it gates nothing). Parameters ---------- @@ -47,12 +58,13 @@ def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): params : np.ndarray Best-fit parameters. cov_matrix : np.ndarray - Covariance matrix of the fitted parameters. May contain NaN (e.g. when - the fit failed); in that case per-point errors and the omnibus test are - returned as NaN. + Covariance matrix of the fitted parameters. May contain NaN (failed + fit); the fitted curve and model omnibus are then NaN, but the + data-based tests still run on ``y_obs``/``y_std``. x : np.ndarray - The observed independent-variable values (typically the ~8 titrant - concentrations). Predictions and per-point tests are evaluated here. + Assessment grid (the unique observed x). + y_obs, y_std : np.ndarray + Observed values and their standard errors on the ``x`` grid. alpha : float, optional Two-sided significance level for the per-point ``sig_nonzero`` flag. Default 0.05. @@ -60,41 +72,50 @@ def assess_best_model(model_func, params, cov_matrix, x, alpha=0.05): Returns ------- per_point : dict - Arrays of length ``len(x)``: ``x``, ``y_model`` (best-fit curve value), - ``y_model_std`` (propagated fit uncertainty), ``z``, ``sig_nonzero`` - (bool), ``direction`` (-1/0/+1). + Arrays of length ``len(x)``: ``x``, ``y_model`` (fitted curve value), + ``y_model_std`` (propagated fit uncertainty), ``z`` (= y_obs/y_std), + ``sig_nonzero`` (bool), ``direction`` (-1/0/+1). rollup : dict - Scalars: ``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``n_nonzero``, - ``any_nonzero``. + Scalars: data-based ``nonzero_chi2``/``nonzero_df``/``nonzero_p``, + model-based ``omnibus_W``/``omnibus_df``/``omnibus_p`` (reported-only), + ``n_nonzero``, ``any_nonzero``. """ x = np.asarray(x, dtype=float) + y_obs = np.asarray(y_obs, dtype=float) + y_std = np.asarray(y_std, dtype=float) - y_est, y_std, y_cov = predict_with_error( + # Fitted curve (for plotting) + its full covariance (for the model omnibus). + y_model, y_model_std, y_cov = predict_with_error( model_func, params, cov_matrix, args=[x], full_cov=True ) - y_est = np.asarray(y_est, dtype=float) - y_std = np.asarray(y_std, dtype=float) + y_model = np.asarray(y_model, dtype=float) + y_model_std = np.asarray(y_model_std, dtype=float) - # Two-sided per-point z-test against zero. z_crit turns alpha into a - # threshold on |z| (and, below, on the equivalence CI half-width). + # Per-point z-test against zero on the OBSERVED data. z_crit = norm.ppf(1.0 - alpha / 2.0) with np.errstate(divide="ignore", invalid="ignore"): - z = y_est / y_std + z = y_obs / y_std sig_nonzero = np.isfinite(z) & (np.abs(z) > z_crit) - direction = np.where(sig_nonzero, np.sign(y_est), 0).astype(int) + direction = np.where(sig_nonzero, np.sign(y_obs), 0).astype(int) per_point = { "x": x, - "y_model": y_est, - "y_model_std": y_std, + "y_model": y_model, + "y_model_std": y_model_std, "z": z, "sig_nonzero": sig_nonzero, "direction": direction, } - omnibus_W, omnibus_df, omnibus_p = _omnibus_chi2(y_est, y_cov) + # Data-based portmanteau chi-square vs the zero line (drives response_class). + nonzero_chi2, nonzero_df, nonzero_p = _nonzero_chi2(y_obs, y_std) + # Model-based omnibus on the fitted curve (reported only). + omnibus_W, omnibus_df, omnibus_p = _omnibus_chi2(y_model, y_cov) rollup = { + "nonzero_chi2": nonzero_chi2, + "nonzero_df": nonzero_df, + "nonzero_p": nonzero_p, "omnibus_W": omnibus_W, "omnibus_df": omnibus_df, "omnibus_p": omnibus_p, @@ -134,6 +155,26 @@ def _omnibus_chi2(y_est, y_cov): return W, df, p +def _nonzero_chi2(y_obs, y_std): + """ + Model-free test that the observed curve differs from the zero line. + + Weighted portmanteau ``chi2 = sum((y_obs / y_std) ** 2) ~ chi2(n)`` under the + null that every point is zero. Uses the *observed* error bars, so a curve + whose CIs all overlap zero is not called nonzero no matter how confidently a + flexible model fits it. Returns ``(chi2, df, p)``; ``(nan, 0, nan)`` when no + point has a finite value and positive std. + """ + y_obs = np.asarray(y_obs, dtype=float) + y_std = np.asarray(y_std, dtype=float) + m = np.isfinite(y_obs) & np.isfinite(y_std) & (y_std > 0) + df = int(np.count_nonzero(m)) + if df == 0: + return np.nan, 0, np.nan + stat = float(np.sum((y_obs[m] / y_std[m]) ** 2)) + return stat, df, float(chi2.sf(stat, df)) + + def residual_runs_p(resid): """ Wald-Wolfowitz runs-test p-value for systematic structure in residuals. diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index fc262c9..ef7088d 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -161,7 +161,8 @@ def select_by_adequacy(models, adequacy_alpha): # Rollup keys added to flat_output by the post-hoc assessment. Listed here so # the insufficient-data / all-fail paths can emit them as NaN. -_ROLLUP_KEYS = ["omnibus_W", "omnibus_df", "omnibus_p", "n_nonzero", +_ROLLUP_KEYS = ["nonzero_chi2", "nonzero_df", "nonzero_p", + "omnibus_W", "omnibus_df", "omnibus_p", "n_nonzero", "any_nonzero"] @@ -258,8 +259,9 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, dict A single, flat dictionary containing the best model summary, AICc weights for all tested models, all parameter estimates and standard - errors for all tested models, and the best-model omnibus rollup - (``omnibus_W/df/p``, ``n_nonzero``, ``any_nonzero``). + errors for all tested models, and the best-model zero-assessment rollup + (data-based ``nonzero_chi2/df/p``, reported-only model ``omnibus_W/df/p``, + ``n_nonzero``, ``any_nonzero``). pd.DataFrame Model predictions with columns ``model``, ``x``, ``y_model``, ``y_model_std``, ``is_best_model``. Restricted to the best model unless @@ -535,22 +537,25 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, "y_model_std": pd.Series(dtype=float)}) pred_df["is_best_model"] = pred_df["model"] == flat_output["best_model"] - # Per-point assessment + omnibus rollup for the best model. + # Per-point assessment + rollup for the best model. The zero tests are + # data-driven, so aggregate the observed data to the assessment grid first + # and hand it to assess_best_model. With one observation per concentration + # (the shared-grid assumption) this is a straight alignment; replicate + # concentrations are mean-collapsed. if best_model is not None: p_res = param_results[best_model] + obs = (pd.DataFrame({"x": x, "y_obs": y, "y_std": y_std}) + .groupby("x", sort=True).mean().reindex(x_assess)) + y_obs_a = obs["y_obs"].to_numpy() + y_std_a = obs["y_std"].to_numpy() + per_point, rollup = assess_best_model( p_res['model_func'], p_res['params'], p_res['cov'], x_assess, - alpha=alpha + y_obs_a, y_std_a, alpha=alpha ) assess_df = pd.DataFrame({c: per_point[c] for c in _PER_POINT_COLS}) - - # Attach the observed data aggregated to the assessment grid. With one - # observation per concentration (the shared-grid assumption) this is a - # straight alignment; replicate concentrations are mean-collapsed. - obs = (pd.DataFrame({"x": x, "y_obs": y, "y_std": y_std}) - .groupby("x", sort=True).mean().reindex(x_assess)) - assess_df["y_obs"] = obs["y_obs"].to_numpy() - assess_df["y_std"] = obs["y_std"].to_numpy() + assess_df["y_obs"] = y_obs_a + assess_df["y_std"] = y_std_a assess_df["model"] = best_model assess_df = assess_df[_ASSESS_COLS] diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 395e8e6..ffe3333 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -151,7 +151,9 @@ def cat_response(df, ``shape_status``, ``best_model_gof_p`` / ``best_model_runs_p``, per-model ``AIC_weight|*`` / ``R2|*`` / ``gof_p|*`` / ``runs_p|*``, and ``||est`` / ``std`` columns), then the assessment rollups - (``omnibus_W``, ``omnibus_df``, ``omnibus_p``, ``omnibus_q``, + (data-based ``nonzero_chi2`` / ``nonzero_df`` / ``nonzero_p`` / + ``nonzero_q`` which drive the zero call, reported-only model + ``omnibus_W`` / ``omnibus_df`` / ``omnibus_p`` / ``omnibus_q``, ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, ``response_class``). ``best_model`` follows ``select_by`` (default lowest-AICc; see :func:`cat_fit`), ``shape`` is its qualitative form @@ -263,16 +265,18 @@ def cat_response(df, assessment_df = pd.concat(assess_list, ignore_index=True) # --- Post-hoc pass: global delta, equivalence, FDR, response class ------- + # The zero axis is data-driven: delta and the equivalence test read the + # observed y_obs/y_std, not the (overconfident) fitted-curve error. resolved_delta = delta if resolved_delta is None: resolved_delta = compute_delta( - assessment_df.get("y_model_std", []), delta_c + assessment_df.get("y_std", []), delta_c ) if len(assessment_df): assessment_df["equiv_zero"] = classify_equiv( - assessment_df["y_model"].to_numpy(dtype=float), - assessment_df["y_model_std"].to_numpy(dtype=float), + assessment_df["y_obs"].to_numpy(dtype=float), + assessment_df["y_std"].to_numpy(dtype=float), resolved_delta, alpha=alpha, ) @@ -292,7 +296,11 @@ def cat_response(df, else: results_df["all_equiv_zero"] = pd.Series(dtype=bool) - # FDR across curves on the omnibus p-values. + # FDR across curves. nonzero_q (data-based) drives response_class; omnibus_q + # (model-based) is kept for reference only. + results_df["nonzero_q"] = benjamini_hochberg( + results_df.get("nonzero_p", pd.Series(np.nan, index=results_df.index)) + ) results_df["omnibus_q"] = benjamini_hochberg( results_df.get("omnibus_p", pd.Series(np.nan, index=results_df.index)) ) @@ -312,28 +320,31 @@ def cat_response(df, def _response_class(results_df, alpha): """ - Assign each curve to confident_zero / real / indeterminate. - - Equivalence takes precedence over significance: a curve whose every point - sits inside the region of practical equivalence is ``confident_zero`` even - if the omnibus test finds a statistically significant but practically - negligible offset (tiny error bars make a hair-off-zero baseline - "significant"). The ROPE is what makes "real" mean *meaningfully* nonzero. - - - ``confident_zero``: every point's CI lies within [-delta, delta]. - - ``real``: not confident_zero, and omnibus q-value < alpha (distinguishable - from zero by more than the equivalence margin somewhere on the curve). - - ``indeterminate``: neither (too noisy to call, or unassessable). + Assign each curve to real / confident_zero / indeterminate (data-driven). + + "Distinguishable from zero" is checked first, on the observed error bars + (``nonzero_q``, the BH-adjusted data-based portmanteau test), so a real + signal wins even if it happens to fall inside the equivalence region. When a + curve is *not* distinguishable from zero, the ROPE separates the two + non-signal cases: tight error bars -> confidently flat at zero; wide error + bars -> too noisy to tell. + + - ``real``: ``nonzero_q < alpha`` -- the observed points collectively clear + the zero line. + - ``confident_zero``: not real, and every observed point's CI lies within + [-delta, delta] (confidently flat at zero). + - ``indeterminate``: not real and not tightly bounded (too noisy to call). """ - q = results_df.get("omnibus_q", + q = results_df.get("nonzero_q", pd.Series(np.nan, index=results_df.index)).to_numpy() all_equiv = results_df.get( "all_equiv_zero", pd.Series(False, index=results_df.index) ).fillna(False).to_numpy(dtype=bool) out = np.full(len(results_df), "indeterminate", dtype=object) + # Not distinguishable from zero but tightly bounded -> confident_zero. + out[all_equiv] = "confident_zero" + # Distinguishable from zero wins (real-first precedence). real = np.isfinite(q) & (q < alpha) out[real] = "real" - # Equivalence wins over a barely-significant offset. - out[all_equiv] = "confident_zero" return out diff --git a/tests/tfscreen/analysis/cat_response/test_cat_assess.py b/tests/tfscreen/analysis/cat_response/test_cat_assess.py index c953f03..22c2c35 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_assess.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -15,9 +15,32 @@ residual_autocorr, goodness_of_fit_p, _omnibus_chi2, + _nonzero_chi2, ) +class TestNonzeroChi2: + def test_signal_is_significant(self): + stat, df, p = _nonzero_chi2(np.array([1.0, 2.0, 3.0]), + np.array([0.1, 0.1, 0.1])) + assert df == 3 and stat > 100 and p < 1e-6 + + def test_consistent_with_zero(self): + stat, df, p = _nonzero_chi2(np.array([0.1, -0.2, 0.05]), + np.array([2.0, 3.0, 1.0])) + assert df == 3 and p > 0.5 + + def test_drops_bad_points(self): + stat, df, p = _nonzero_chi2(np.array([1.0, np.nan, 3.0]), + np.array([0.1, 0.1, 0.0])) + assert df == 1 # only the first point is usable + + def test_no_usable_points_is_nan(self): + stat, df, p = _nonzero_chi2(np.array([1.0, 2.0]), + np.array([0.0, -1.0])) + assert df == 0 and np.isnan(stat) and np.isnan(p) + + # --- residual autocorrelation (shape gate) ----------------------------------- class TestResidualAutocorr: @@ -120,27 +143,56 @@ def test_nan_cov_returns_nan(self): assert np.isnan(W) and df == 0 and np.isnan(p) def test_assess_best_model_flags_and_rollup(self): - # y = 1*x + 0 at x=[1,2,3] -> y_model = [1,2,3], all clearly nonzero. + # Observed points [1,2,3] with tight errors -> all clearly nonzero, and + # the data-based nonzero test is significant. The fitted line also gives + # a (reported) model omnibus. x = np.array([1.0, 2.0, 3.0]) params = np.array([1.0, 0.0]) - cov = np.diag([1e-4, 1e-4]) # tiny -> small pred se -> significant - per_point, rollup = assess_best_model(_linear, params, cov, x) + cov = np.diag([1e-4, 1e-4]) + y_obs = np.array([1.0, 2.0, 3.0]) + y_std = np.array([0.01, 0.01, 0.01]) + per_point, rollup = assess_best_model(_linear, params, cov, x, + y_obs, y_std) assert per_point["y_model"] == pytest.approx([1.0, 2.0, 3.0]) + # Per-point z-test now reads the observed data. + assert per_point["z"] == pytest.approx(y_obs / y_std) assert np.all(per_point["sig_nonzero"]) assert np.all(per_point["direction"] == 1) assert rollup["n_nonzero"] == 3 assert rollup["any_nonzero"] is True - assert rollup["omnibus_df"] == 2 # two free params + assert rollup["nonzero_df"] == 3 # data test: one df per point + assert rollup["nonzero_p"] < 1e-6 + assert rollup["omnibus_df"] == 2 # model test: two free params assert rollup["omnibus_p"] < 1e-6 + def test_assess_best_model_data_beats_overconfident_model(self): + # The reported failure mode: observed error bars overlap zero, but the + # fitted curve is confident. The DATA test must not call it nonzero. + x = np.array([1.0, 2.0, 3.0]) + params = np.array([1.0, 0.0]) + cov = np.diag([1e-6, 1e-6]) # overconfident fit + y_obs = np.array([1.0, 2.0, 3.0]) + y_std = np.array([50.0, 50.0, 50.0]) # huge observed errors + per_point, rollup = assess_best_model(_linear, params, cov, x, + y_obs, y_std) + assert rollup["n_nonzero"] == 0 # no observed point clears 0 + assert rollup["nonzero_p"] > 0.5 # data: consistent with zero + assert rollup["omnibus_p"] < 1e-6 # model: overconfident nonzero + def test_assess_best_model_nan_cov(self): + # NaN fit covariance -> model omnibus NaN, but the data-based tests still + # run on the observed points. x = np.array([1.0, 2.0, 3.0]) + y_obs = np.array([0.0, 0.0, 0.0]) + y_std = np.array([0.1, 0.1, 0.1]) per_point, rollup = assess_best_model( - _linear, np.array([1.0, 0.0]), np.full((2, 2), np.nan), x + _linear, np.array([1.0, 0.0]), np.full((2, 2), np.nan), x, + y_obs, y_std ) assert np.all(~per_point["sig_nonzero"]) assert np.isnan(rollup["omnibus_p"]) + assert np.isfinite(rollup["nonzero_p"]) assert rollup["n_nonzero"] == 0 diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index b52f3b6..9c31165 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -28,7 +28,8 @@ def _flat(best="flat", **extra): """A minimal flat cat_fit result dict with the assessment rollups.""" d = {"status": "success", "best_model": best, - "omnibus_p": 0.5, "n_nonzero": 0, "any_nonzero": False} + "nonzero_p": 0.5, "omnibus_p": 0.5, + "n_nonzero": 0, "any_nonzero": False} d.update(extra) return d @@ -277,8 +278,8 @@ def test_explicit_delta_is_used(self): assert delta == 0.75 def test_response_class_confident_zero(self): - # y_model=0, y_model_std tiny -> CI well inside delta -> all_equiv_zero - # -> and omnibus_p high (0.5) -> confident_zero. + # Observed y=0 with tiny error -> CI well inside delta -> all_equiv_zero, + # and nonzero_p high (0.5) -> not distinguishable -> confident_zero. fit = _capturing_fit([]) def fake(*a, **k): @@ -292,14 +293,14 @@ def fake(*a, **k): y_std="theta_std", delta=0.5) assert set(results["response_class"]) == {"confident_zero"} - def test_equivalence_wins_over_significance(self): - # Significant omnibus (tiny p) but every point inside the ROPE - # (y_model=0, tiny std) -> practically zero should win -> confident_zero. + def test_real_wins_over_equivalence(self): + # Real-first precedence: distinguishable from zero (nonzero_p tiny) wins + # even when every observed point sits inside the ROPE. fit = _capturing_fit([]) def fake(*a, **k): flat, pred, _ = fit(*a, **k) - flat["omnibus_p"] = 1e-9 + flat["nonzero_p"] = 1e-9 x = a[0] return flat, pred, _assess(len(np.unique(x)), y_model=0.0, y_model_std=1e-4) @@ -307,16 +308,32 @@ def fake(*a, **k): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", delta=0.5) - assert set(results["response_class"]) == {"confident_zero"} + assert set(results["response_class"]) == {"real"} + + def test_indeterminate_when_noisy_and_not_equiv(self): + # Not distinguishable from zero (nonzero_p high) and error bars too wide + # for the ROPE -> indeterminate (can't tell). + fit = _capturing_fit([]) + + def fake(*a, **k): + flat, pred, _ = fit(*a, **k) + x = a[0] + return flat, pred, _assess(len(np.unique(x)), y_model=0.0, + y_model_std=5.0) # huge observed std + with patch.object(cat_response_mod, "cat_fit", side_effect=fake): + results, _, _, _ = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta=0.5) + assert set(results["response_class"]) == {"indeterminate"} def test_response_class_real_from_low_q(self): with patch.object(cat_response_mod, "cat_fit", - side_effect=_capturing_fit([], omnibus_p=1e-8)): + side_effect=_capturing_fit([], nonzero_p=1e-8)): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", delta=1e-6) assert set(results["response_class"]) == {"real"} - assert (results["omnibus_q"] < 0.05).all() + assert (results["nonzero_q"] < 0.05).all() # --- chunk helper ------------------------------------------------------------ From 5721bd880bb9de2719009a336080f4d0e9d9dfeb Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 10:36:22 -0400 Subject: [PATCH 20/26] updating cat response --- CLAUDE.md | 2 +- .../analysis/cat_response/cat_response.py | 18 ++++++++++++++- .../cat_response/test_cat_response.py | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7630434..8ede9f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -291,7 +291,7 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( `equiv_zero` (a point's whole **observed** CI `|y_obs| + z·y_std ⊂ [-delta, delta]`) is the confidence axis separating "confidently flat" from "too noisy to tell"; `delta` defaults to `delta_c * median(observed y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits), or pass `--delta` for a fixed biological region. `response_class` is 3-valued, **real-first** (differs-from-zero is checked first, per the design decision — a real signal wins even if it lands inside the ROPE): `real` (`nonzero_q < alpha`) → else `confident_zero` (all points `equiv_zero` — confidently flat at zero) → else `indeterminate` (not distinguishable from zero and error bars too wide for the ROPE = "can't tell"). This magnitude axis is **orthogonal to** the `shape` axis above; the exploratory read is their cross-tab (filter `response_class == real`, then look at `shape`). -Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `response_class`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. +Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `response_class`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `response_class` (right after `model`; the curve's class carried on every point for filtering — model name and fitted values left intact), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. ## YAML Standards diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index ffe3333..035a0d2 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -172,7 +172,9 @@ def cat_response(df, Self-contained per-point best-model assessment at the observed (unique) x. Group-key columns followed by ``model``, ``x``, ``y_obs``, ``y_std``, ``y_model``, ``y_model_std``, ``z``, ``sig_nonzero``, ``direction``, - ``equiv_zero``. + ``equiv_zero``. ``response_class`` sits right after ``model`` and carries + the curve's class on every point for filtering (the ``model`` name and + its fitted values are left intact). delta : float The equivalence half-width actually used (resolved from ``delta`` / ``delta_c`` if not supplied). @@ -308,11 +310,25 @@ def cat_response(df, # Three-valued response class: real / confident_zero / indeterminate. results_df["response_class"] = _response_class(results_df, alpha) + # Surface response_class in the per-point assessment (its own column, so the + # model name and its y_model/y_model_std stay intact) for filtering there. + if len(assessment_df): + assessment_df = assessment_df.merge( + results_df[group_cols + ["response_class"]], + on=group_cols, how="left", + ) + else: + assessment_df["response_class"] = pd.Series(dtype=object) + # Order columns: group keys first. other = [c for c in results_df.columns if c not in group_cols] results_df = results_df[group_cols + other] assess_other = [c for c in assessment_df.columns if c not in group_cols] + # Keep response_class right next to the model column. + if "response_class" in assess_other and "model" in assess_other: + assess_other.remove("response_class") + assess_other.insert(assess_other.index("model") + 1, "response_class") assessment_df = assessment_df[group_cols + assess_other] return results_df, predictions_df, assessment_df, resolved_delta diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 9c31165..51bdd7f 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -258,8 +258,30 @@ def test_assessment_tagged_and_has_equiv_zero(self): y_std="theta_std", group_by=["titrant_name"]) assert list(assess.columns[:2]) == ["genotype", "titrant_name"] assert "equiv_zero" in assess.columns + assert "response_class" in assess.columns assert np.isfinite(delta) + def test_response_class_column_next_to_model(self): + # response_class is its own column immediately after model; the model + # name and its fitted values are left intact. + fit = _capturing_fit([]) + + def fake(*a, **k): + flat, pred, _ = fit(*a, **k) + x = a[0] + return flat, pred, _assess(len(np.unique(x)), y_model=0.0, + y_model_std=5.0) # huge observed std + with patch.object(cat_response_mod, "cat_fit", side_effect=fake): + _, _, assess, _ = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", delta=0.5) + assert set(assess["response_class"]) == {"indeterminate"} + # model name preserved (not overloaded), and y_model left intact. + assert set(assess["model"]) == {"flat"} + assert "indeterminate" not in set(assess["model"]) + cols = list(assess.columns) + assert cols[cols.index("model") + 1] == "response_class" + def test_delta_defaults_to_median_times_c(self): # Fake assessment always reports y_std=0.1 -> median=0.1 -> delta=0.2. with patch.object(cat_response_mod, "cat_fit", From 584b43cb663ed7c5d95929565fc7fe5f5067835e Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 12:22:38 -0400 Subject: [PATCH 21/26] updating cat response --- CLAUDE.md | 8 +- .../analysis/cat_response/cat_assess.py | 53 ++++---- src/tfscreen/analysis/cat_response/cat_fit.py | 33 ++--- .../analysis/cat_response/cat_response.py | 122 +++++++++--------- .../analysis/scripts/cat_response_cli.py | 61 ++++----- .../analysis/cat_response/test_cat_assess.py | 33 +++-- .../analysis/cat_response/test_cat_fit.py | 31 +++-- .../cat_response/test_cat_response.py | 43 +++--- .../analysis/scripts/test_cat_response_cli.py | 8 +- 9 files changed, 200 insertions(+), 192 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8ede9f6..cb572bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -284,14 +284,14 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( **Model x-scale (concentration-parameterized vs log-conc).** `MODEL_LIBRARY` models are **not** interchangeable on x-scale. The Hill family (`repressor`/`inducer`/`hill_*`) and `biphasic_*` are parameterized in **raw concentration** and take `log(x)` internally (`_hill` does `np.log(x)`), so they are already sigmoids/peaks *in log-concentration* and **must be handed raw x** — feeding them `log10(x)` (negative) both double-logs and NaNs them. The geometric models (`bell_peak`/`bell_dip` Gaussian-in-x, `linear`) are shapes in raw x; their `*_log` counterparts (`bell_peak_log`/`bell_dip_log` = Gaussian in `log10(x)` with a free real `center`, and `linear_log` = line in `log10(x)`) are the log-concentration versions. The `*_log` models own their transform via `models._to_log10_x` (x stays **raw concentration** in the data — there is no `--log_x` flag and no separate log column): `x <= 0` (the no-titrant point) is floored to `min(x[x>0])/100` before the log, computed per-call (identical across groups for a shared titration grid). `flat` is scale-invariant. When adding a new model, decide which camp it's in — never blanket-transform x. `DEFAULT_MODELS` (in `curve_models/__init__.py`) is the curated set fit when `--models`/`models_to_run` is omitted (was: all of `MODEL_LIBRARY`): `flat, linear_log, repressor, inducer, bell_peak_log, bell_dip_log` — one parameterization per qualitative response. All other models (raw-x `bell_*`/`linear`, 4-param `hill_*`, `biphasic_*`) stay registered and reachable via `--models`. -- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py` (three modes). **`"aicc"` (default)**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). Robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. **`"adequacy"`** (`select_by_adequacy`): **escalate-only** refinement — keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat`. **`"shape"`** (`select_by_shape`): liberal, prior-aligned classifier for *exploration* (AICc is too conservative — its small-n penalty buries a well-fit curve, e.g. an R²=0.96 dip called `flat`). Two steps, **no AICc parsimony**: (1) **flat-vs-curvy** gate on structure in the *flat* fit's residuals — curvy iff `autocorr_p|flat < curvy_cutoff` (weighted Durbin-Watson lag-1 autocorrelation p, `residual_autocorr`; magnitude/`y_std`-aware, so unlike the runs test it isn't washed out by many near-baseline points); (2) among curvy-shape models (step/peak/dip/biphasic; `linear` excluded as unphysical) pick the best **weighted R²**, preferring the simpler within `r2_margin` (0.02). `curvy_cutoff` (default 0.1) is the sweepable knob — run a set and visually inspect. When `models_to_run` is None, shape mode defaults to `SHAPE_MODELS` (physical vocabulary: `flat, inducer, repressor, bell_peak_log, bell_dip_log, biphasic_peak, biphasic_dip` — no `linear_log`; adds biphasic) instead of `DEFAULT_MODELS`. +- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py` (three modes; **default `"shape"`**). **`"aicc"`**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). Robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. **`"adequacy"`** (`select_by_adequacy`): **escalate-only** refinement — keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat`. **`"shape"`** (`select_by_shape`): liberal, prior-aligned classifier for *exploration* (AICc is too conservative — its small-n penalty buries a well-fit curve, e.g. an R²=0.96 dip called `flat`). Two steps, **no AICc parsimony**: (1) **flat-vs-curvy** gate on structure in the *flat* fit's residuals — curvy iff `autocorr_p|flat < curvy_cutoff` (weighted Durbin-Watson lag-1 autocorrelation p, `residual_autocorr`; magnitude/`y_std`-aware, so unlike the runs test it isn't washed out by many near-baseline points); (2) among curvy-shape models (step/peak/dip/biphasic; `linear` excluded as unphysical) pick the best **weighted R²**, preferring the simpler within `r2_margin` (0.02). `curvy_cutoff` (default 0.1) is the sweepable knob — run a set and visually inspect. When `models_to_run` is None, shape mode defaults to `SHAPE_MODELS` (physical vocabulary: `flat, inducer, repressor, bell_peak_log, bell_dip_log, biphasic_peak, biphasic_dip` — no `linear_log`; adds biphasic) instead of `DEFAULT_MODELS`. Per-model diagnostics are always reported and (except the shape gate) **do not gate selection**: `runs_p|*` (sign-based; needs `n >= _MIN_RUNS_N` (4), power only at `n >~ 8`), `autocorr|*`/`autocorr_p|*` (weighted DW lag-1, the shape gate's signal), weighted-χ² `gof_p|*` (`goodness_of_fit_p`). `shape` = qualitative form of `best_model` (`flat`/`linear`/`step`/`peak`/`dip`/`biphasic`, via `_SHAPE_BY_MODEL`; `_CURVY_SHAPES` = the non-flat/non-linear ones); `shape_status` = runs-test diagnostic on the **selected** model (`adequate`/`misfit`/`unassessable`/`none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only when adequacy escalates or shape reclassifies). This form axis is **orthogonal to** the magnitude/`response_class` axis below — the intended exploratory hierarchy is their cross: `response_class` gives *flat-real / can't-tell (indeterminate) / confident_zero*, and `shape` gives *flat vs which kind of curvy*. -- **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading each curve against zero **on the observed data, not the fitted curve** (`assess_best_model` takes `y_obs`/`y_std`). The driver is a model-free **portmanteau** `nonzero_chi2 = sum((y_obs/y_std)**2) ~ χ²(n)` (`_nonzero_chi2`) → `nonzero_p` → **Benjamini-Hochberg** across curves → `nonzero_q`. This replaced a model-based **omnibus** `W = yhat @ pinv(J·Cov·Jᵀ) @ yhat` as the gate because that test reads the *fitted* curve's covariance, which is wildly overconfident when a flexible model is fit to noisy data (it called curves whose observed error bars all overlap zero "real"). The omnibus (`omnibus_W/df/p/q`) is **still computed and reported** but **gates nothing**; `y_model`/`y_model_std` are still emitted for plotting. Per-point `sig_nonzero`/`z`/`direction` also use observed `y_obs/y_std`. +- **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading each curve against zero **on the observed data, not the fitted curve** (`assess_best_model` takes `y_obs`/`y_std`). The driver is a model-free **portmanteau** `nonzero_chi2 = sum((y_obs/y_std)**2) ~ χ²(n)` (`_nonzero_chi2`) → `nonzero_p` → **Benjamini-Hochberg** across curves → `nonzero_q`. This replaced a model-based **omnibus** `W = yhat @ pinv(J·Cov·Jᵀ) @ yhat` as the gate because that test reads the *fitted* curve's covariance, which is wildly overconfident when a flexible model is fit to noisy data (it called curves whose observed error bars all overlap zero "real"). The omnibus (`omnibus_W/df/p/q`) is **still computed and reported** but **gates nothing**; `y_model`/`y_model_std` are still emitted for plotting. Per-point `sig_nonzero`/`z` also use observed `y_obs/y_std`. `alpha` has two roles: the per-point `sig_nonzero`/equiv CI level **and** the `nonzero_q` threshold that calls `real` (it *is* a calling threshold, not just a stat level). -`equiv_zero` (a point's whole **observed** CI `|y_obs| + z·y_std ⊂ [-delta, delta]`) is the confidence axis separating "confidently flat" from "too noisy to tell"; `delta` defaults to `delta_c * median(observed y_std)` (a **detectability** threshold, computed globally in `cat_response.py` after all fits), or pass `--delta` for a fixed biological region. `response_class` is 3-valued, **real-first** (differs-from-zero is checked first, per the design decision — a real signal wins even if it lands inside the ROPE): `real` (`nonzero_q < alpha`) → else `confident_zero` (all points `equiv_zero` — confidently flat at zero) → else `indeterminate` (not distinguishable from zero and error bars too wide for the ROPE = "can't tell"). This magnitude axis is **orthogonal to** the `shape` axis above; the exploratory read is their cross-tab (filter `response_class == real`, then look at `shape`). +The equivalence rollup `all_equiv_zero` (every observed point's CI `|y_obs| + z·y_std ⊂ [-rope_cutoff, rope_cutoff]`, via `classify_equiv`) separates "confidently flat" from "too noisy to tell"; `rope_cutoff` defaults to `rope_multiplier * median(observed y_std)` (`compute_rope`, a **detectability** threshold computed globally after all fits) — which scales with the noise and so **rarely lets a whole CI fit inside**, meaning `confident_zero` seldom fires under the auto value; pass an explicit `--rope_cutoff` (a biological region) to make it fire. The per-point `equiv_zero` flag is computed internally for this rollup but **not written** to the assessment CSV. `response_class` is 3-valued, **real-first**: `real` (`nonzero_q < alpha`) → else `confident_zero` (all points equiv — confidently flat at zero) → else `indeterminate` ("can't tell"). This magnitude axis is **orthogonal to** the `shape` axis above; the exploratory read is their cross-tab (filter `response_class == real`, then look at `shape`). -Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `response_class`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `response_class` (right after `model`; the curve's class carried on every point for filtering — model name and fitted values left intact), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z`/`sig_nonzero`/`direction`/`equiv_zero`; `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, delta)`. +Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `response_class`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `response_class` (right after `model`; the curve's class carried on every point for filtering — model name and fitted values left intact), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z` (= y_obs/y_std) and `sig_nonzero` (per-observed-point, not the model). `equiv_zero`/`direction` were dropped (the ROPE `equiv_zero` was ~always False; `direction` = `sign(y_obs)`). `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, rope_cutoff)`. ## YAML Standards diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py index 52c143e..d0f982f 100644 --- a/src/tfscreen/analysis/cat_response/cat_assess.py +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -19,11 +19,12 @@ - **equivalence test** (:func:`classify_equiv`): a point is ``equiv_zero`` when its whole confidence interval falls inside a region of practical equivalence - ``[-delta, delta]`` centered on zero. This is what separates "confidently - flat" from "too noisy to tell" — a plain significance test cannot. ``delta`` - defaults to a multiple of the median predicted standard error - (:func:`compute_delta`), i.e. a *detectability* threshold; pass an explicit - ``delta`` for a fixed biologically-meaningful region instead. + ``[-rope_cutoff, rope_cutoff]`` centered on zero. This is what separates + "confidently flat" from "too noisy to tell" (drives ``confident_zero``). The + auto ``rope_cutoff`` (:func:`compute_rope`, ``rope_multiplier * median(y_std)``) + is a *detectability* threshold and rarely lets a whole CI fit inside it -- pass + an explicit ``rope_cutoff`` (a biologically-meaningful region) to make + ``confident_zero`` fire. """ import numpy as np @@ -74,7 +75,7 @@ def assess_best_model(model_func, params, cov_matrix, x, y_obs, y_std, per_point : dict Arrays of length ``len(x)``: ``x``, ``y_model`` (fitted curve value), ``y_model_std`` (propagated fit uncertainty), ``z`` (= y_obs/y_std), - ``sig_nonzero`` (bool), ``direction`` (-1/0/+1). + ``sig_nonzero`` (bool). (``direction`` is derivable as ``sign(y_obs)``.) rollup : dict Scalars: data-based ``nonzero_chi2``/``nonzero_df``/``nonzero_p``, model-based ``omnibus_W``/``omnibus_df``/``omnibus_p`` (reported-only), @@ -96,7 +97,6 @@ def assess_best_model(model_func, params, cov_matrix, x, y_obs, y_std, with np.errstate(divide="ignore", invalid="ignore"): z = y_obs / y_std sig_nonzero = np.isfinite(z) & (np.abs(z) > z_crit) - direction = np.where(sig_nonzero, np.sign(y_obs), 0).astype(int) per_point = { "x": x, @@ -104,7 +104,6 @@ def assess_best_model(model_func, params, cov_matrix, x, y_obs, y_std, "y_model_std": y_model_std, "z": z, "sig_nonzero": sig_nonzero, - "direction": direction, } # Data-based portmanteau chi-square vs the zero line (drives response_class). @@ -292,44 +291,46 @@ def goodness_of_fit_p(chi2_w, n, k): return float(chi2.sf(chi2_w, df)) -def compute_delta(pred_std, delta_c=2.0): +def compute_rope(pred_std, rope_multiplier=2.0): """ - Default region-of-practical-equivalence half-width from prediction error. + Auto region-of-practical-equivalence (ROPE) half-width from the error bars. - ``delta = delta_c * median(pred_std)`` over all finite predicted standard + ``rope_cutoff = rope_multiplier * median(pred_std)`` over all finite standard errors. This ties "practically zero" to the typical *detectability* of the - experiment rather than to any biological effect size. Returns NaN if no - finite values are present. + experiment rather than to any biological effect size -- and because it scales + with the noise it rarely lets a whole CI fit inside, so ``confident_zero`` + seldom fires under the auto value; pass an explicit ``rope_cutoff`` for a + biologically-meaningful region. Returns NaN if no finite values are present. Parameters ---------- pred_std : array-like - Predicted per-point standard errors, pooled across all groups. - delta_c : float, optional + Per-point standard errors, pooled across all groups. + rope_multiplier : float, optional Multiplier on the median. Default 2.0. """ pred_std = np.asarray(pred_std, dtype=float) finite = pred_std[np.isfinite(pred_std)] if finite.size == 0: return np.nan - return float(delta_c * np.median(finite)) + return float(rope_multiplier * np.median(finite)) -def classify_equiv(y_est, y_std, delta, alpha=0.05): +def classify_equiv(y_est, y_std, rope_cutoff, alpha=0.05): """ - Flag points whose whole CI lies inside the equivalence region [-delta, delta]. + Flag points whose whole CI lies inside the ROPE [-rope_cutoff, rope_cutoff]. - A point is ``equiv_zero`` when ``|y_est| + z_crit * y_std <= delta`` — i.e. - the two-sided ``(1 - alpha)`` confidence interval is entirely within the - region of practical equivalence around zero. NaN std or NaN/invalid delta - yield False. + A point is ``equiv_zero`` when ``|y_est| + z_crit * y_std <= rope_cutoff`` -- + i.e. the two-sided ``(1 - alpha)`` confidence interval is entirely within the + region of practical equivalence around zero. NaN std or NaN/invalid + ``rope_cutoff`` yield False. Parameters ---------- y_est, y_std : array-like Per-point estimate and standard error. - delta : float - Equivalence half-width (see :func:`compute_delta`). + rope_cutoff : float + ROPE half-width (see :func:`compute_rope`). alpha : float, optional Matches the confidence level used elsewhere. Default 0.05. @@ -339,13 +340,13 @@ def classify_equiv(y_est, y_std, delta, alpha=0.05): """ y_est = np.asarray(y_est, dtype=float) y_std = np.asarray(y_std, dtype=float) - if not np.isfinite(delta): + if not np.isfinite(rope_cutoff): return np.zeros(y_est.shape, dtype=bool) z_crit = norm.ppf(1.0 - alpha / 2.0) ci_upper = np.abs(y_est) + z_crit * y_std with np.errstate(invalid="ignore"): - equiv = np.isfinite(ci_upper) & (ci_upper <= delta) + equiv = np.isfinite(ci_upper) & (ci_upper <= rope_cutoff) return equiv diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index ef7088d..47bcdef 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -145,8 +145,7 @@ def select_by_adequacy(models, adequacy_alpha): return aicc_best # Keys returned by assess_best_model's per-point dict (the model curve + tests). -_PER_POINT_COLS = ["x", "y_model", "y_model_std", "z", "sig_nonzero", - "direction"] +_PER_POINT_COLS = ["x", "y_model", "y_model_std", "z", "sig_nonzero"] # Columns of the per-point assessment frame, in order. Kept as a module # constant so empty-result paths emit an identically-shaped (empty) frame. This @@ -154,10 +153,10 @@ def select_by_adequacy(models, adequacy_alpha): # (``y_obs`` and its input error ``y_std``), the fitted curve at the observed x # (``y_model`` and its propagated error ``y_model_std``), and the zero tests. _ASSESS_COLS = ["model", "x", "y_obs", "y_std", "y_model", "y_model_std", - "z", "sig_nonzero", "direction"] + "z", "sig_nonzero"] # Non-float dtypes for the empty-frame path. -_ASSESS_DTYPES = {"model": object, "sig_nonzero": bool, "direction": int} +_ASSESS_DTYPES = {"model": object, "sig_nonzero": bool} # Rollup keys added to flat_output by the post-hoc assessment. Listed here so # the insufficient-data / all-fail paths can emit them as NaN. @@ -187,7 +186,7 @@ def _empty_assess_df(): def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, - alpha=0.05, select_by="aicc", adequacy_alpha=0.05, + alpha=0.05, select_by="shape", adequacy_alpha=0.05, curvy_cutoff=0.1, verbose=False): """ Fits multiple models to a single dataset and returns a flat dictionary @@ -233,14 +232,16 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, emitted in the returned prediction frame. If False, every successfully fit model's curve is emitted (the larger, "all models" output). alpha : float, optional - Two-sided significance level for the per-point ``sig_nonzero`` test. - Default 0.05. - select_by : {"aicc", "adequacy", "shape"}, optional - Model-selection strategy. ``"aicc"`` (default) picks the lowest-AICc - model. ``"adequacy"`` starts from the AICc pick and escalates to a - no-simpler adequate model only if flagged (never demotes). ``"shape"`` - is the liberal shape classifier (structure-gated flat-vs-curvy, then - best-R2 curvy shape). Default ``"aicc"``. + Two-sided significance level for the per-point ``sig_nonzero`` test + (``|z| > z_crit(alpha)``). The same ``alpha`` is the threshold applied to + ``nonzero_q`` in ``cat_response`` when calling a curve ``real``. Default + 0.05. + select_by : {"shape", "aicc", "adequacy"}, optional + Model-selection strategy. ``"shape"`` (default) is the liberal shape + classifier (structure-gated flat-vs-curvy, then best-R2 curvy shape; + defaults ``models_to_run`` to ``SHAPE_MODELS``). ``"aicc"`` picks the + lowest-AICc model. ``"adequacy"`` starts from the AICc pick and escalates + to a no-simpler adequate model only if flagged (never demotes). adequacy_alpha : float, optional Runs-test threshold used for the ``shape_status`` diagnostic and (when ``select_by="adequacy"``) for escalation. ``runs_p`` below this flags @@ -270,9 +271,9 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, Self-contained per-point assessment of the best model at the observed (unique) x, with columns ``model``, ``x``, ``y_obs`` (observed value), ``y_std`` (observed input error), ``y_model`` (fitted curve), - ``y_model_std`` (propagated fit error), ``z``, ``sig_nonzero``, - ``direction``. Empty if no model could be fit. (``equiv_zero`` is added - downstream in ``cat_response`` once the global delta is known.) + ``y_model_std`` (propagated fit error), ``z`` (= y_obs/y_std), and + ``sig_nonzero``. Empty if no model could be fit. (``response_class`` is + added downstream in ``cat_response``.) """ if select_by not in ("aicc", "adequacy", "shape"): diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 035a0d2..4924859 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -2,14 +2,15 @@ Generic per-group categorical-response engine. Fits a family of candidate models to a ``y_obs`` vs ``x_obs`` curve independently -within each group and selects the best by AICc weight. Grouping mirrors -``extract_epistasis``: the ``genotype`` column is always the primary axis, with -any additional ``group_by`` columns partitioning the analysis further. - -After fitting, a post-hoc pass grades each group's best curve against zero: a -per-point ``sig_nonzero`` test, an ``equiv_zero`` (region-of-practical- -equivalence) test using a global ``delta``, a per-curve omnibus chi-square test, -and a Benjamini-Hochberg FDR correction across curves (see :mod:`cat_assess`). +within each group and selects the best per ``select_by`` (default the ``shape`` +classifier). Grouping mirrors ``extract_epistasis``: the ``genotype`` column is +always the primary axis, with any additional ``group_by`` columns partitioning +the analysis further. + +After fitting, a post-hoc pass grades each group's curve against zero on the +observed data (a per-point ``sig_nonzero`` test and the per-curve data-based +``nonzero`` test), plus a ROPE-based equivalence rollup and a Benjamini-Hochberg +FDR correction across curves (see :mod:`cat_assess`), yielding ``response_class``. """ from concurrent.futures import ProcessPoolExecutor @@ -19,7 +20,7 @@ import tqdm from .cat_fit import cat_fit -from .cat_assess import compute_delta, classify_equiv, benjamini_hochberg +from .cat_assess import compute_rope, classify_equiv, benjamini_hochberg from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS, SHAPE_MODELS from tfscreen.util import resolve_workers from tfscreen.util.numerical import xfill @@ -68,11 +69,11 @@ def cat_response(df, models_to_run=None, best_only=True, alpha=0.05, - select_by="aicc", + select_by="shape", adequacy_alpha=0.05, curvy_cutoff=0.1, - delta=None, - delta_c=2.0, + rope_cutoff=None, + rope_multiplier=2.0, num_workers=1, progress=True, verbose=False): @@ -108,17 +109,16 @@ def cat_response(df, If True (default), the returned prediction frame holds only each group's best-model curve. If False, it holds every fit model's curve. alpha : float, optional - Two-sided significance level for the per-point ``sig_nonzero`` test and - the confidence level of the ``equiv_zero`` interval. Also the threshold - applied to omnibus q-values when assigning ``response_class``. Default - 0.05. - select_by : {"aicc", "adequacy", "shape"}, optional - Model-selection strategy (see :func:`cat_fit`). ``"aicc"`` (default) - selects the lowest-AICc model. ``"adequacy"`` keeps the AICc pick unless - flagged, then escalates to a no-simpler adequate model (never demotes). - ``"shape"`` is the liberal shape classifier (structure-gated flat-vs- - curvy, then best-R2 curvy shape); with ``models_to_run=None`` it defaults - to the physical ``SHAPE_MODELS`` vocabulary. Default ``"aicc"``. + Significance level with two roles: the per-point ``sig_nonzero`` test / + equivalence CI level, **and** the threshold on ``nonzero_q`` that calls a + curve ``real``. Default 0.05. + select_by : {"shape", "aicc", "adequacy"}, optional + Model-selection strategy (see :func:`cat_fit`). ``"shape"`` (default) is + the liberal shape classifier (structure-gated flat-vs-curvy, then best-R2 + curvy shape); with ``models_to_run=None`` it defaults to the physical + ``SHAPE_MODELS`` vocabulary. ``"aicc"`` selects the lowest-AICc model. + ``"adequacy"`` keeps the AICc pick unless flagged, then escalates to a + no-simpler adequate model (never demotes). adequacy_alpha : float, optional Runs-test threshold used for the ``shape_status`` diagnostic and, when ``select_by="adequacy"``, for escalation. Default 0.05. @@ -126,14 +126,16 @@ def cat_response(df, Only used when ``select_by="shape"``: the flat-vs-curvy gate on the flat fit's residual-autocorrelation p-value (larger = more liberal). Sweep it and inspect. Default 0.1. - delta : float or None, optional - Region-of-practical-equivalence half-width around zero. If None - (default), computed globally as ``delta_c * median(predicted y_std)`` - over all points (a detectability threshold). Pass a value for a fixed, - biologically-meaningful region. - delta_c : float, optional - Multiplier used when ``delta`` is derived from the median predicted - standard error. Default 2.0. + rope_cutoff : float or None, optional + Region-of-practical-equivalence (ROPE) half-width around zero, + separating ``confident_zero`` from ``indeterminate``. If None (default), + auto-derived as ``rope_multiplier * median(observed y_std)`` -- a + detectability threshold that rarely lets a whole CI fit inside, so + ``confident_zero`` seldom fires; pass an explicit value (a biologically- + meaningful region) to make it fire. + rope_multiplier : float, optional + Multiplier used when ``rope_cutoff`` is auto-derived from the median + observed standard error. Default 2.0. num_workers : int, optional Number of worker processes. ``1`` runs serially in-process; ``-1`` uses ``os.cpu_count() - 1``; ``N`` uses ``N`` processes. Default 1. @@ -170,14 +172,13 @@ def cat_response(df, ``best_only`` is False. assessment_df : pandas.DataFrame Self-contained per-point best-model assessment at the observed (unique) - x. Group-key columns followed by ``model``, ``x``, ``y_obs``, ``y_std``, - ``y_model``, ``y_model_std``, ``z``, ``sig_nonzero``, ``direction``, - ``equiv_zero``. ``response_class`` sits right after ``model`` and carries - the curve's class on every point for filtering (the ``model`` name and - its fitted values are left intact). - delta : float - The equivalence half-width actually used (resolved from ``delta`` / - ``delta_c`` if not supplied). + x. Group-key columns, then ``model``, ``response_class`` (carried on + every point for filtering; ``model`` name + fitted values left intact), + ``x``, ``y_obs``, ``y_std``, ``y_model``, ``y_model_std``, ``z`` + (= y_obs/y_std), ``sig_nonzero``. + rope_cutoff : float + The ROPE half-width actually used (resolved from ``rope_cutoff`` / + ``rope_multiplier`` if not supplied). """ if models_to_run is None: # The "shape" classifier defaults to the physical shape vocabulary @@ -227,8 +228,8 @@ def cat_response(df, n_total = len(work_items) if n_total == 0: empty = pd.DataFrame(columns=group_cols) - resolved_delta = delta if delta is not None else np.nan - return empty.copy(), empty.copy(), empty.copy(), resolved_delta + resolved_rope = rope_cutoff if rope_cutoff is not None else np.nan + return empty.copy(), empty.copy(), empty.copy(), resolved_rope workers = resolve_workers(num_workers) chunks = list(_iter_chunks(work_items, _CHUNK_SIZE)) @@ -266,31 +267,26 @@ def cat_response(df, assessment_df = pd.concat(assess_list, ignore_index=True) - # --- Post-hoc pass: global delta, equivalence, FDR, response class ------- - # The zero axis is data-driven: delta and the equivalence test read the + # --- Post-hoc pass: ROPE, equivalence, FDR, response class --------------- + # The zero axis is data-driven: the ROPE and equivalence test read the # observed y_obs/y_std, not the (overconfident) fitted-curve error. - resolved_delta = delta - if resolved_delta is None: - resolved_delta = compute_delta( - assessment_df.get("y_std", []), delta_c - ) - + resolved_rope = rope_cutoff + if resolved_rope is None: + resolved_rope = compute_rope(assessment_df.get("y_std", []), + rope_multiplier) + + # Per-curve all_equiv_zero rollup (a curve is "confidently flat at zero" only + # if *every* observed point's CI sits inside the ROPE). Computed internally; + # the per-point equiv flag is not written to the assessment output. if len(assessment_df): - assessment_df["equiv_zero"] = classify_equiv( + equiv = classify_equiv( assessment_df["y_obs"].to_numpy(dtype=float), assessment_df["y_std"].to_numpy(dtype=float), - resolved_delta, - alpha=alpha, + resolved_rope, alpha=alpha, ) - else: - assessment_df["equiv_zero"] = pd.Series(dtype=bool) - - # Per-group all_equiv_zero rollup (a curve is "confidently flat at zero" - # only if *every* point's CI sits inside the equivalence region). - if len(assessment_df): - all_equiv = (assessment_df.groupby(group_cols, sort=False, - observed=True)["equiv_zero"] - .all()) + tmp = assessment_df[group_cols].copy() + tmp["_equiv"] = equiv + all_equiv = tmp.groupby(group_cols, sort=False, observed=True)["_equiv"].all() results_df = results_df.merge( all_equiv.rename("all_equiv_zero").reset_index(), on=group_cols, how="left" @@ -331,7 +327,7 @@ def cat_response(df, assess_other.insert(assess_other.index("model") + 1, "response_class") assessment_df = assessment_df[group_cols + assess_other] - return results_df, predictions_df, assessment_df, resolved_delta + return results_df, predictions_df, assessment_df, resolved_rope def _response_class(results_df, alpha): @@ -348,7 +344,7 @@ def _response_class(results_df, alpha): - ``real``: ``nonzero_q < alpha`` -- the observed points collectively clear the zero line. - ``confident_zero``: not real, and every observed point's CI lies within - [-delta, delta] (confidently flat at zero). + [-rope_cutoff, rope_cutoff] (confidently flat at zero). - ``indeterminate``: not real and not tightly bounded (too noisy to call). """ q = results_df.get("nonzero_q", diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index 5e9bf7a..6a951d0 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -63,11 +63,11 @@ def cat_response(data_file, group_by=None, models=None, alpha=0.05, - select_by="aicc", + select_by="shape", adequacy_alpha=0.05, curvy_cutoff=0.1, - delta=None, - delta_c=2.0, + rope_cutoff=None, + rope_multiplier=2.0, write_all_predictions=False, num_workers=-1): """ @@ -75,15 +75,16 @@ def cat_response(data_file, Reads a long-form CSV and fits one or more response models to every group. Groups are defined by the 'genotype' column plus any --group_by columns. For - each group the best-fitting model is selected by AICc weight, then graded - against zero (per-point sig_nonzero / equiv_zero tests + a per-curve omnibus - chi-square with a Benjamini-Hochberg FDR correction). + each group the best model is selected per --select_by (default the 'shape' + classifier), then graded against zero on the observed data (per-point + sig_nonzero + a per-curve data-based nonzero test with a Benjamini-Hochberg + FDR correction) to assign response_class. Writes: - {out_prefix}.csv one row per group; all models' weights and parameter estimates, plus the assessment - rollups (omnibus_p/q, n_nonzero, - all_equiv_zero, response_class). + rollups (nonzero_p/q, omnibus_p/q, + n_nonzero, all_equiv_zero, response_class). - {out_prefix}_{model}.csv one file per model; that model's parameter table and per-group fit statistics. - {out_prefix}_predictions.csv best-model predicted curves (all models @@ -116,16 +117,17 @@ def cat_response(data_file, bell_dip_log). Pass explicit names to reach any model in MODEL_LIBRARY, including the raw-x and biphasic variants. alpha : float, optional - Significance level for the per-point tests and the omnibus q-value - threshold used to call a curve 'real'. Default 0.05. - select_by : {"aicc", "adequacy", "shape"}, optional - Model-selection strategy. ``"aicc"`` (default) selects the lowest-AICc - model. ``"adequacy"`` keeps the AICc pick unless flagged, then escalates - to a no-simpler adequate model (never demotes). ``"shape"`` is the - liberal shape classifier: it gates flat-vs-curvy on the flat fit's - residual autocorrelation and names the curvy shape by best R2 (defaults - to the physical ``SHAPE_MODELS`` vocabulary -- no linear, includes - biphasic). Default ``"aicc"``. + Significance level with two roles: the per-point ``sig_nonzero`` test, + and the ``nonzero_q`` threshold used to call a curve 'real'. Default + 0.05. + select_by : {"shape", "aicc", "adequacy"}, optional + Model-selection strategy. ``"shape"`` (default) is the liberal shape + classifier: it gates flat-vs-curvy on the flat fit's residual + autocorrelation and names the curvy shape by best R2 (defaults to the + physical ``SHAPE_MODELS`` vocabulary -- no linear, includes biphasic). + ``"aicc"`` selects the lowest-AICc model. ``"adequacy"`` keeps the AICc + pick unless flagged, then escalates to a no-simpler adequate model + (never demotes). adequacy_alpha : float, optional Runs-test threshold for the ``shape_status`` diagnostic and, when ``select_by="adequacy"``, for escalation. Default 0.05. @@ -133,12 +135,13 @@ def cat_response(data_file, Only used when ``select_by="shape"``: flat-vs-curvy gate on the flat fit's residual-autocorrelation p-value (larger = more curves called curvy). Sweep it and visually inspect. Default 0.1. - delta : float or None, optional - Region-of-practical-equivalence half-width around zero. If None - (default), derived globally as ``delta_c * median(predicted y_std)``. - Pass a value for a fixed, biologically-meaningful region. - delta_c : float, optional - Multiplier used when ``delta`` is auto-derived. Default 2.0. + rope_cutoff : float or None, optional + ROPE half-width separating ``confident_zero`` from ``indeterminate``. If + None (default), auto-derived as ``rope_multiplier * median(observed + y_std)`` -- a detectability threshold that rarely fires ``confident_zero``; + pass a fixed, biologically-meaningful value to make it fire. + rope_multiplier : float, optional + Multiplier used when ``rope_cutoff`` is auto-derived. Default 2.0. write_all_predictions : bool, optional If True, write every fit model's predicted curve rather than only the best model's. Default False. @@ -179,7 +182,7 @@ def cat_response(data_file, print(f" Fitting groups defined by {group_cols} " f"with {num_workers} worker(s)...", flush=True) - results_df, predictions_df, assessment_df, resolved_delta = _cat_response( + results_df, predictions_df, assessment_df, resolved_rope = _cat_response( df, x_obs=x_obs, y_obs=y_obs, @@ -191,12 +194,12 @@ def cat_response(data_file, select_by=select_by, adequacy_alpha=adequacy_alpha, curvy_cutoff=curvy_cutoff, - delta=delta, - delta_c=delta_c, + rope_cutoff=rope_cutoff, + rope_multiplier=rope_multiplier, num_workers=num_workers, ) - print(f" Using equivalence half-width delta = {resolved_delta:.6g}", + print(f" Using ROPE half-width rope_cutoff = {resolved_rope:.6g}", flush=True) # Main table: flat, one row per group, with clean column names. @@ -228,7 +231,7 @@ def main(): "y_std": str, "group_by": str, "models": str, - "delta": float}, + "rope_cutoff": float}, manual_arg_nargs={"group_by": "+", "models": "+"}) diff --git a/tests/tfscreen/analysis/cat_response/test_cat_assess.py b/tests/tfscreen/analysis/cat_response/test_cat_assess.py index 22c2c35..641572b 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_assess.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -8,7 +8,7 @@ from tfscreen.analysis.cat_response.cat_assess import ( assess_best_model, - compute_delta, + compute_rope, classify_equiv, benjamini_hochberg, residual_runs_p, @@ -158,7 +158,6 @@ def test_assess_best_model_flags_and_rollup(self): # Per-point z-test now reads the observed data. assert per_point["z"] == pytest.approx(y_obs / y_std) assert np.all(per_point["sig_nonzero"]) - assert np.all(per_point["direction"] == 1) assert rollup["n_nonzero"] == 3 assert rollup["any_nonzero"] is True assert rollup["nonzero_df"] == 3 # data test: one df per point @@ -201,46 +200,46 @@ def test_assess_best_model_nan_cov(self): class TestClassifyEquiv: def test_ci_inside_region_is_equiv(self): - # |y| + z*se = 0.1 + 1.96*0.01 ~ 0.12 < delta=0.5 -> equiv. - equiv = classify_equiv(np.array([0.1]), np.array([0.01]), delta=0.5) + # |y| + z*se = 0.1 + 1.96*0.01 ~ 0.12 < rope=0.5 -> equiv. + equiv = classify_equiv(np.array([0.1]), np.array([0.01]), rope_cutoff=0.5) assert equiv[0] def test_ci_straddles_boundary_not_equiv(self): # 0.4 + 1.96*0.1 ~ 0.596 > 0.5 -> not equiv (too wide / too far). - equiv = classify_equiv(np.array([0.4]), np.array([0.1]), delta=0.5) + equiv = classify_equiv(np.array([0.4]), np.array([0.1]), rope_cutoff=0.5) assert not equiv[0] def test_far_from_zero_not_equiv(self): - equiv = classify_equiv(np.array([5.0]), np.array([0.01]), delta=0.5) + equiv = classify_equiv(np.array([5.0]), np.array([0.01]), rope_cutoff=0.5) assert not equiv[0] - def test_nan_std_and_bad_delta(self): + def test_nan_std_and_bad_rope(self): assert not classify_equiv(np.array([0.0]), np.array([np.nan]), - delta=0.5)[0] + rope_cutoff=0.5)[0] assert not classify_equiv(np.array([0.0]), np.array([0.01]), - delta=np.nan)[0] + rope_cutoff=np.nan)[0] def test_boundary_uses_alpha(self): # Larger alpha -> narrower CI -> easier to be equivalent. - y, s, d = np.array([0.3]), np.array([0.1]), 0.5 + y, s, r = np.array([0.3]), np.array([0.1]), 0.5 # alpha=0.05 -> 0.3+1.96*0.1=0.496 < 0.5 -> equiv (just). - assert classify_equiv(y, s, d, alpha=0.05)[0] + assert classify_equiv(y, s, r, alpha=0.05)[0] -# --- compute_delta ----------------------------------------------------------- +# --- compute_rope ------------------------------------------------------------ -class TestComputeDelta: +class TestComputeRope: - def test_median_times_c(self): + def test_median_times_multiplier(self): stds = [0.1, 0.2, 0.3, np.nan] # median of finite [0.1,0.2,0.3] = 0.2 -> *2 = 0.4 - assert compute_delta(stds, delta_c=2.0) == pytest.approx(0.4) + assert compute_rope(stds, rope_multiplier=2.0) == pytest.approx(0.4) def test_all_nan_returns_nan(self): - assert np.isnan(compute_delta([np.nan, np.nan])) + assert np.isnan(compute_rope([np.nan, np.nan])) def test_empty_returns_nan(self): - assert np.isnan(compute_delta([])) + assert np.isnan(compute_rope([])) # --- benjamini-hochberg ------------------------------------------------------ diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index 243b388..9796139 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -219,12 +219,12 @@ def _curved_hetero_data(): return x, bump + rng.normal(0, ystd), ystd -def test_aicc_default_does_not_collapse_to_flat(): - """Regression: default select_by='aicc' keeps the AICc-confident curve even - when the runs test is too weak to flag flat (the reported bug).""" +def test_aicc_mode_does_not_collapse_to_flat(): + """Regression: select_by='aicc' keeps the AICc-confident curve even when the + runs test is too weak to flag flat (the reported bug).""" x, y, ys = _curved_hetero_data() models = ["flat", "linear_log", "inducer", "bell_peak_log", "bell_dip_log"] - flat_output, _, _ = cat_fit(x, y, ys, models_to_run=models) # default aicc + flat_output, _, _ = cat_fit(x, y, ys, models_to_run=models, select_by="aicc") assert flat_output["best_model"] != "flat" assert flat_output["best_model"] == flat_output["aicc_best_model"] @@ -312,7 +312,7 @@ def test_assessment_rollup_and_frame(): assert key in flat_output assert list(assess_df.columns) == ["model", "x", "y_obs", "y_std", "y_model", "y_model_std", "z", - "sig_nonzero", "direction"] + "sig_nonzero"] # One row per unique observed x. assert len(assess_df) == len(np.unique(x)) # Model name recorded; observed data carried through alongside the fit. @@ -380,16 +380,25 @@ def test_sanitize_filters_nonfinite(): assert len(assess_df) == 4 -def test_default_models_are_the_curated_set(): - """With models_to_run=None, cat_fit fits exactly DEFAULT_MODELS (not all).""" +def test_default_models_shape_mode_is_shape_set(): + """Default select_by='shape' with models_to_run=None fits SHAPE_MODELS.""" x, y, y_std = _hill_data() - flat, _, _ = cat_fit(x, y, y_std) # models_to_run defaults to None + flat, _, _ = cat_fit(x, y, y_std) # defaults: select_by='shape', models=None + fit_models = {k.split("|", 1)[1] for k in flat if k.startswith("AIC_weight|")} + assert fit_models == set(SHAPE_MODELS) + # No linear; biphasic included. + assert "linear_log" not in fit_models + assert "biphasic_peak" in fit_models + + +def test_default_models_aicc_mode_is_default_set(): + """select_by='aicc' with models_to_run=None fits DEFAULT_MODELS.""" + x, y, y_std = _hill_data() + flat, _, _ = cat_fit(x, y, y_std, select_by="aicc") fit_models = {k.split("|", 1)[1] for k in flat if k.startswith("AIC_weight|")} assert fit_models == set(DEFAULT_MODELS) - # The log-conc variants are in; the raw-x duplicates and biphasic are out. - assert "bell_peak_log" in fit_models + assert "linear_log" in fit_models assert "biphasic_peak" not in fit_models - assert "bell_peak" not in fit_models # --- degenerate-covariance guard --------------------------------------------- diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 51bdd7f..d460493 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -56,14 +56,13 @@ def _assess(n=2, y_model=0.0, y_model_std=0.1): "y_model_std": np.full(n, y_model_std, dtype=float), "z": np.full(n, y_model / y_model_std if y_model_std else 0.0), "sig_nonzero": np.zeros(n, dtype=bool), - "direction": np.zeros(n, dtype=int), }) def _capturing_fit(store, **fit_kwargs): """A fake cat_fit that records the (x, y, y_std) it was handed per call.""" def fake_fit(x, y, y_std, x_pred=None, models_to_run=None, - best_only=True, alpha=0.05, select_by="aicc", + best_only=True, alpha=0.05, select_by="shape", adequacy_alpha=0.05, curvy_cutoff=0.1, verbose=False): store.append({"x": list(x), "y": list(y), "y_std": list(y_std), "best_only": best_only, "alpha": alpha, @@ -180,13 +179,13 @@ def test_select_by_threaded_to_fitter(self): cat_response(df, x_obs="titrant_conc", y_obs="theta", y_std="theta_std", select_by="adequacy") assert all(call["select_by"] == "adequacy" for call in store) - # Default is the robust AICc selection. + # Default is the shape classifier. store.clear() with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit(store)): cat_response(df, x_obs="titrant_conc", y_obs="theta", y_std="theta_std") - assert all(call["select_by"] == "aicc" for call in store) + assert all(call["select_by"] == "shape" for call in store) def test_curvy_cutoff_threaded_to_fitter(self): store = [] @@ -250,16 +249,16 @@ def test_predictions_tagged_with_group_keys(self): class TestAssessmentPass: - def test_assessment_tagged_and_has_equiv_zero(self): + def test_assessment_tagged_and_has_response_class(self): with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit([])): - _, _, assess, delta = cat_response( + _, _, assess, rope = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", group_by=["titrant_name"]) assert list(assess.columns[:2]) == ["genotype", "titrant_name"] - assert "equiv_zero" in assess.columns assert "response_class" in assess.columns - assert np.isfinite(delta) + assert "equiv_zero" not in assess.columns # dropped from the output + assert np.isfinite(rope) def test_response_class_column_next_to_model(self): # response_class is its own column immediately after model; the model @@ -274,7 +273,7 @@ def fake(*a, **k): with patch.object(cat_response_mod, "cat_fit", side_effect=fake): _, _, assess, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta=0.5) + y_std="theta_std", rope_cutoff=0.5) assert set(assess["response_class"]) == {"indeterminate"} # model name preserved (not overloaded), and y_model left intact. assert set(assess["model"]) == {"flat"} @@ -282,22 +281,22 @@ def fake(*a, **k): cols = list(assess.columns) assert cols[cols.index("model") + 1] == "response_class" - def test_delta_defaults_to_median_times_c(self): - # Fake assessment always reports y_std=0.1 -> median=0.1 -> delta=0.2. + def test_rope_defaults_to_median_times_multiplier(self): + # Fake assessment always reports y_std=0.1 -> median=0.1 -> rope=0.2. with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit([])): - _, _, _, delta = cat_response( + _, _, _, rope = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta_c=2.0) - assert delta == pytest.approx(0.2) + y_std="theta_std", rope_multiplier=2.0) + assert rope == pytest.approx(0.2) - def test_explicit_delta_is_used(self): + def test_explicit_rope_cutoff_is_used(self): with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit([])): - _, _, _, delta = cat_response( + _, _, _, rope = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta=0.75) - assert delta == 0.75 + y_std="theta_std", rope_cutoff=0.75) + assert rope == 0.75 def test_response_class_confident_zero(self): # Observed y=0 with tiny error -> CI well inside delta -> all_equiv_zero, @@ -312,7 +311,7 @@ def fake(*a, **k): with patch.object(cat_response_mod, "cat_fit", side_effect=fake): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta=0.5) + y_std="theta_std", rope_cutoff=0.5) assert set(results["response_class"]) == {"confident_zero"} def test_real_wins_over_equivalence(self): @@ -329,7 +328,7 @@ def fake(*a, **k): with patch.object(cat_response_mod, "cat_fit", side_effect=fake): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta=0.5) + y_std="theta_std", rope_cutoff=0.5) assert set(results["response_class"]) == {"real"} def test_indeterminate_when_noisy_and_not_equiv(self): @@ -345,7 +344,7 @@ def fake(*a, **k): with patch.object(cat_response_mod, "cat_fit", side_effect=fake): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta=0.5) + y_std="theta_std", rope_cutoff=0.5) assert set(results["response_class"]) == {"indeterminate"} def test_response_class_real_from_low_q(self): @@ -353,7 +352,7 @@ def test_response_class_real_from_low_q(self): side_effect=_capturing_fit([], nonzero_p=1e-8)): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", - y_std="theta_std", delta=1e-6) + y_std="theta_std", rope_cutoff=1e-6) assert set(results["response_class"]) == {"real"} assert (results["nonzero_q"] < 0.05).all() diff --git a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py index 7e8d317..d3434c9 100644 --- a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py @@ -74,11 +74,11 @@ def test_writes_main_permodel_and_predictions(self, tmp_path): assess = pd.read_csv(f"{out_prefix}_assessment.csv") assert "genotype" in assess.columns - assert {"model", "x", "y_obs", "y_std", "y_model", "y_model_std", "z", - "sig_nonzero", "direction", "equiv_zero"}.issubset( - assess.columns) + assert {"model", "response_class", "x", "y_obs", "y_std", "y_model", + "y_model_std", "z", "sig_nonzero"}.issubset(assess.columns) + assert "equiv_zero" not in assess.columns # dropped # Rollups landed on the main table. - assert {"omnibus_p", "omnibus_q", "n_nonzero", + assert {"nonzero_p", "nonzero_q", "omnibus_p", "omnibus_q", "n_nonzero", "response_class"}.issubset(main.columns) def test_write_all_predictions_flag(self, tmp_path): From 9b1b02e0d17af58c018b3634bb7dfb69c509b340 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Mon, 20 Jul 2026 14:34:28 -0400 Subject: [PATCH 22/26] updating/locking down cat response --- CLAUDE.md | 10 +-- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- examples/toy_thermo/toy_thermo.ipynb | 8 +- reports/flake.txt | 85 +++++++++---------- reports/junit/junit.xml | 2 +- .../analysis/cat_response/cat_assess.py | 2 +- src/tfscreen/analysis/cat_response/cat_fit.py | 2 +- .../analysis/cat_response/cat_response.py | 68 ++++++--------- src/tfscreen/analysis/extract_epistasis.py | 47 ++++++++-- .../analysis/scripts/cat_response_cli.py | 4 +- .../analysis/scripts/extract_epistasis_cli.py | 9 ++ src/tfscreen/mle/curve_models/__init__.py | 9 +- .../analysis/cat_response/test_cat_fit.py | 13 +++ .../cat_response/test_cat_response.py | 47 +++++----- .../analysis/scripts/test_cat_response_cli.py | 4 +- .../scripts/test_extract_epistasis_cli.py | 24 ++++++ .../analysis/test_extract_epistasis.py | 79 ++++++++++++++++- .../simulate/toy_thermo/test_integration.py | 2 +- 19 files changed, 282 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cb572bc..dec2d60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ tfs-extract-params # Extract parameters from checkpoint tfs-predict-growth # Predict growth from fitted model tfs-predict-theta # Predict operator occupancy tfs-cat-response # Fit categorical response curves -tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table (--scale add|mult|logit) +tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table (--scale add|mult|logit; --scale_constant rescales the transform before epistasis, e.g. -RT to put logit onto a free-energy scale) tfs-compare-theta # Grade per-genotype theta stability across N estimate runs (seeds / k-fold dropouts) tfs-diagnose-nan # Diagnose NaN issues in inference tfs-simulate # Simulate a full experiment @@ -284,14 +284,14 @@ Integration: `library_prediction` gains a `phenotype_source: empirical` branch ( **Model x-scale (concentration-parameterized vs log-conc).** `MODEL_LIBRARY` models are **not** interchangeable on x-scale. The Hill family (`repressor`/`inducer`/`hill_*`) and `biphasic_*` are parameterized in **raw concentration** and take `log(x)` internally (`_hill` does `np.log(x)`), so they are already sigmoids/peaks *in log-concentration* and **must be handed raw x** — feeding them `log10(x)` (negative) both double-logs and NaNs them. The geometric models (`bell_peak`/`bell_dip` Gaussian-in-x, `linear`) are shapes in raw x; their `*_log` counterparts (`bell_peak_log`/`bell_dip_log` = Gaussian in `log10(x)` with a free real `center`, and `linear_log` = line in `log10(x)`) are the log-concentration versions. The `*_log` models own their transform via `models._to_log10_x` (x stays **raw concentration** in the data — there is no `--log_x` flag and no separate log column): `x <= 0` (the no-titrant point) is floored to `min(x[x>0])/100` before the log, computed per-call (identical across groups for a shared titration grid). `flat` is scale-invariant. When adding a new model, decide which camp it's in — never blanket-transform x. `DEFAULT_MODELS` (in `curve_models/__init__.py`) is the curated set fit when `--models`/`models_to_run` is omitted (was: all of `MODEL_LIBRARY`): `flat, linear_log, repressor, inducer, bell_peak_log, bell_dip_log` — one parameterization per qualitative response. All other models (raw-x `bell_*`/`linear`, 4-param `hill_*`, `biphasic_*`) stay registered and reachable via `--models`. -- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py` (three modes; **default `"shape"`**). **`"aicc"`**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). Robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. **`"adequacy"`** (`select_by_adequacy`): **escalate-only** refinement — keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat`. **`"shape"`** (`select_by_shape`): liberal, prior-aligned classifier for *exploration* (AICc is too conservative — its small-n penalty buries a well-fit curve, e.g. an R²=0.96 dip called `flat`). Two steps, **no AICc parsimony**: (1) **flat-vs-curvy** gate on structure in the *flat* fit's residuals — curvy iff `autocorr_p|flat < curvy_cutoff` (weighted Durbin-Watson lag-1 autocorrelation p, `residual_autocorr`; magnitude/`y_std`-aware, so unlike the runs test it isn't washed out by many near-baseline points); (2) among curvy-shape models (step/peak/dip/biphasic; `linear` excluded as unphysical) pick the best **weighted R²**, preferring the simpler within `r2_margin` (0.02). `curvy_cutoff` (default 0.1) is the sweepable knob — run a set and visually inspect. When `models_to_run` is None, shape mode defaults to `SHAPE_MODELS` (physical vocabulary: `flat, inducer, repressor, bell_peak_log, bell_dip_log, biphasic_peak, biphasic_dip` — no `linear_log`; adds biphasic) instead of `DEFAULT_MODELS`. +- **Shape** (which model): selection is controlled by `select_by` in `cat_fit.py` (three modes; **default `"shape"`**). **`"aicc"`**: `best_model` = lowest-AICc model (small-sample-corrected AIC on the **weighted** residuals `chi2 = sum(((y-yfit)/y_std)**2)`, `aic = 2k + chi2`; `aicc=inf` when `n-k-1 <= 0`, params still reported). Robust default — the weighted χ² correctly weights the few informative points, which the sign-based runs test does **not**. **`"adequacy"`** (`select_by_adequacy`): **escalate-only** refinement — keep the AICc pick unless its residuals are systematically clustered (one-sided lower-tail Wald-Wolfowitz runs test, `runs_p < adequacy_alpha`), then move to the lowest-AICc adequate model that is **no simpler** (`k >=` the AICc pick's `k`). It **never demotes**, so it cannot collapse a confident curved fit to `flat` — the failure mode of the earlier (removed) "simplest-adequate" rule, which on noisy heteroscedastic (logit) data let the diluted runs test override AICc and demote real curves to `flat`. **`"shape"`** (`select_by_shape`): liberal, prior-aligned classifier for *exploration* (AICc is too conservative — its small-n penalty buries a well-fit curve, e.g. an R²=0.96 dip called `flat`). Two steps, **no AICc parsimony**: (1) **flat-vs-curvy** gate on structure in the *flat* fit's residuals — curvy iff `autocorr_p|flat < curvy_cutoff` (weighted Durbin-Watson lag-1 autocorrelation p, `residual_autocorr`; magnitude/`y_std`-aware, so unlike the runs test it isn't washed out by many near-baseline points); (2) among curvy-shape models (step/peak/dip/biphasic; `linear` excluded as unphysical) pick the best **weighted R²**, preferring the simpler within `r2_margin` (0.02). `curvy_cutoff` (default 0.1) is the sweepable knob — run a set and visually inspect. When `models_to_run` is None, shape mode defaults to `SHAPE_MODELS` (physical vocabulary: `flat, inducer, repressor, bell_peak_log, bell_dip_log, biphasic_peak, biphasic_dip` — no `linear_log`; adds biphasic) instead of `DEFAULT_MODELS`. Note `biphasic_dip`'s `baseline`/`amplitude` bounds are **unbounded** (`curve_models/__init__.py`); a prior `>= 0` bound assumed a non-negative observable and gave a large-negative R² on signed data (logit epistasis), so it could never be selected. - Per-model diagnostics are always reported and (except the shape gate) **do not gate selection**: `runs_p|*` (sign-based; needs `n >= _MIN_RUNS_N` (4), power only at `n >~ 8`), `autocorr|*`/`autocorr_p|*` (weighted DW lag-1, the shape gate's signal), weighted-χ² `gof_p|*` (`goodness_of_fit_p`). `shape` = qualitative form of `best_model` (`flat`/`linear`/`step`/`peak`/`dip`/`biphasic`, via `_SHAPE_BY_MODEL`; `_CURVY_SHAPES` = the non-flat/non-linear ones); `shape_status` = runs-test diagnostic on the **selected** model (`adequate`/`misfit`/`unassessable`/`none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only when adequacy escalates or shape reclassifies). This form axis is **orthogonal to** the magnitude/`response_class` axis below — the intended exploratory hierarchy is their cross: `response_class` gives *flat-real / can't-tell (indeterminate) / confident_zero*, and `shape` gives *flat vs which kind of curvy*. + Per-model diagnostics are always reported and (except the shape gate) **do not gate selection**: `runs_p|*` (sign-based; needs `n >= _MIN_RUNS_N` (4), power only at `n >~ 8`), `autocorr|*`/`autocorr_p|*` (weighted DW lag-1, the shape gate's signal), weighted-χ² `gof_p|*` (`goodness_of_fit_p`). `shape` = qualitative form of `best_model` (`flat`/`linear`/`step`/`peak`/`dip`/`biphasic`, via `_SHAPE_BY_MODEL`; `_CURVY_SHAPES` = the non-flat/non-linear ones); `shape_status` = runs-test diagnostic on the **selected** model (`adequate`/`misfit`/`unassessable`/`none`, via `_shape_status`); `aicc_best_model` records the AICc pick (differs from `best_model` only when adequacy escalates or shape reclassifies). This form axis is **orthogonal to** the magnitude/`fittable` axis below — the intended exploratory hierarchy is their cross: `fittable` (× `all_equiv_zero`) gives *flat-real / can't-tell (indeterminate) / confident_zero*, and `shape` gives *flat vs which kind of curvy*. - **Magnitude** (distinguishable from zero): a post-hoc pass, `cat_assess.py`, grading each curve against zero **on the observed data, not the fitted curve** (`assess_best_model` takes `y_obs`/`y_std`). The driver is a model-free **portmanteau** `nonzero_chi2 = sum((y_obs/y_std)**2) ~ χ²(n)` (`_nonzero_chi2`) → `nonzero_p` → **Benjamini-Hochberg** across curves → `nonzero_q`. This replaced a model-based **omnibus** `W = yhat @ pinv(J·Cov·Jᵀ) @ yhat` as the gate because that test reads the *fitted* curve's covariance, which is wildly overconfident when a flexible model is fit to noisy data (it called curves whose observed error bars all overlap zero "real"). The omnibus (`omnibus_W/df/p/q`) is **still computed and reported** but **gates nothing**; `y_model`/`y_model_std` are still emitted for plotting. Per-point `sig_nonzero`/`z` also use observed `y_obs/y_std`. `alpha` has two roles: the per-point `sig_nonzero`/equiv CI level **and** the `nonzero_q` threshold that calls `real` (it *is* a calling threshold, not just a stat level). -The equivalence rollup `all_equiv_zero` (every observed point's CI `|y_obs| + z·y_std ⊂ [-rope_cutoff, rope_cutoff]`, via `classify_equiv`) separates "confidently flat" from "too noisy to tell"; `rope_cutoff` defaults to `rope_multiplier * median(observed y_std)` (`compute_rope`, a **detectability** threshold computed globally after all fits) — which scales with the noise and so **rarely lets a whole CI fit inside**, meaning `confident_zero` seldom fires under the auto value; pass an explicit `--rope_cutoff` (a biological region) to make it fire. The per-point `equiv_zero` flag is computed internally for this rollup but **not written** to the assessment CSV. `response_class` is 3-valued, **real-first**: `real` (`nonzero_q < alpha`) → else `confident_zero` (all points equiv — confidently flat at zero) → else `indeterminate` ("can't tell"). This magnitude axis is **orthogonal to** the `shape` axis above; the exploratory read is their cross-tab (filter `response_class == real`, then look at `shape`). +The equivalence rollup `all_equiv_zero` (every observed point's CI `|y_obs| + z·y_std ⊂ [-rope_cutoff, rope_cutoff]`, via `classify_equiv`) separates "confidently flat" from "too noisy to tell"; `rope_cutoff` defaults to `rope_multiplier * median(observed y_std)` (`compute_rope`, a **detectability** threshold computed globally after all fits) — which scales with the noise and so **rarely lets a whole CI fit inside**, meaning `confident_zero` seldom fires under the auto value; pass an explicit `--rope_cutoff` (a biological region) to make it fire. The per-point `equiv_zero` flag is computed internally for this rollup but **not written** to the assessment CSV. The magnitude call is a **bool `fittable`** (`_fittable`): `True` iff `nonzero_q < alpha` (distinguishable from zero, worth interpreting the shape). The old 3-way is recoverable as `fittable` × `all_equiv_zero`: `fittable=True` = "real"; `fittable=False & all_equiv_zero=True` = "confidently flat at zero"; `fittable=False & all_equiv_zero=False` = "can't tell". This magnitude axis is **orthogonal to** the `shape` axis above; the exploratory read is their cross-tab (filter `fittable`, then look at `shape`). -Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `response_class`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `response_class`) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `response_class` (right after `model`; the curve's class carried on every point for filtering — model name and fitted values left intact), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z` (= y_obs/y_std) and `sig_nonzero` (per-observed-point, not the model). `equiv_zero`/`direction` were dropped (the ROPE `equiv_zero` was ~always False; `direction` = `sign(y_obs)`). `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, rope_cutoff)`. +Outputs: rollups (`best_model`/`aicc_best_model`/`shape`/`shape_status`/`best_model_runs_p`/`best_model_autocorr_p`/`best_model_gof_p`, data-based `nonzero_p/q` (drives `fittable`), reported-only model `omnibus_p/q`, `n_nonzero`, `all_equiv_zero`, `fittable` (bool)) land in `{prefix}.csv`; `{prefix}_assessment.csv` is the self-contained per-point record — `model` (best model name), `fittable` (bool, right after `model`; carried on every point for filtering — model name and fitted values left intact), `x`, observed `y_obs`/`y_std`, fitted `y_model`/`y_model_std` (curve value + propagated fit error, **not** the observed error), then `z` (= y_obs/y_std) and `sig_nonzero` (per-observed-point, not the model). `equiv_zero`/`direction` were dropped (the ROPE `equiv_zero` was ~always False; `direction` = `sign(y_obs)`). `{prefix}_predictions.csv` holds only each group's **best** model (columns `model,x,y_model,y_model_std,is_best_model`; `best_only=True` threaded `cat_response→cat_fit` so the all-model curve is never built) unless `--write_all_predictions`. Note `y_model`/`y_model_std` are the **model prediction** at each observed x, distinct from `y_obs`/`y_std` (the experimental point + its input error). `cat_fit` returns a 3-tuple `(flat_output, pred_df, assess_df)`; `cat_response` a 4-tuple `(results_df, predictions_df, assessment_df, rope_cutoff)`. ## YAML Standards diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 6af4af5..80ba118 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 95.94%coverage95.94% \ No newline at end of file +coverage: 96.24%coverage96.24% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 6222380..68fdd82 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4220tests4220 \ No newline at end of file +tests: 4283tests4283 \ No newline at end of file diff --git a/examples/toy_thermo/toy_thermo.ipynb b/examples/toy_thermo/toy_thermo.ipynb index 2022b45..991727e 100644 --- a/examples/toy_thermo/toy_thermo.ipynb +++ b/examples/toy_thermo/toy_thermo.ipynb @@ -479,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "f7b22c30", "metadata": {}, "outputs": [ @@ -537,7 +537,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "04fe6860", "metadata": {}, "outputs": [ @@ -623,7 +623,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "id": "21596f1e", "metadata": {}, "outputs": [ @@ -674,7 +674,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "77e62a5d", "metadata": {}, "outputs": [ diff --git a/reports/flake.txt b/reports/flake.txt index 9eca229..1f40457 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -6,39 +6,35 @@ ./src/tfscreen/analysis/__init__.py:13:1: W391 blank line at end of file ./src/tfscreen/analysis/cat_response/__init__.py:2:77: W291 trailing whitespace ./src/tfscreen/analysis/cat_response/__init__.py:3:55: W291 trailing whitespace -./src/tfscreen/analysis/cat_response/cat_fit.py:42:1: C901 'cat_fit' is too complex (26) -./src/tfscreen/analysis/cat_response/cat_fit.py:175:72: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:175:74: E231 missing whitespace after ',' -./src/tfscreen/analysis/cat_response/cat_fit.py:262:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' -./src/tfscreen/analysis/cat_response/cat_fit.py:264:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' -./src/tfscreen/analysis/cat_response/cat_response.py:60:1: C901 'cat_response' is too complex (15) -./src/tfscreen/analysis/cat_response/cat_response.py:253:45: E127 continuation line over-indented for visual indent +./src/tfscreen/analysis/cat_response/cat_fit.py:148:1: E305 expected 2 blank lines after class or function definition, found 1 +./src/tfscreen/analysis/cat_response/cat_fit.py:188:1: C901 'cat_fit' is too complex (28) +./src/tfscreen/analysis/cat_response/cat_fit.py:365:72: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:365:74: E231 missing whitespace after ',' +./src/tfscreen/analysis/cat_response/cat_fit.py:471:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' +./src/tfscreen/analysis/cat_response/cat_fit.py:473:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' +./src/tfscreen/analysis/cat_response/cat_response.py:65:1: C901 'cat_response' is too complex (16) ./src/tfscreen/analysis/compare_theta.py:200:1: C901 'compare_theta' is too complex (12) ./src/tfscreen/analysis/compare_theta.py:632:1: C901 'aggregate_theta' is too complex (11) -./src/tfscreen/analysis/extract_epistasis.py:8:1: C901 'mutant_cycle_pivot' is too complex (11) -./src/tfscreen/analysis/extract_epistasis.py:8:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/analysis/extract_epistasis.py:78:25: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:90:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:96:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:100:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:113:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:125:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:129:1: C901 'extract_epistasis' is too complex (15) -./src/tfscreen/analysis/extract_epistasis.py:133:37: E252 missing whitespace around parameter equals -./src/tfscreen/analysis/extract_epistasis.py:133:38: E252 missing whitespace around parameter equals -./src/tfscreen/analysis/extract_epistasis.py:183:36: W291 trailing whitespace -./src/tfscreen/analysis/extract_epistasis.py:184:27: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:187:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:192:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:215:55: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:215:60: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:215:65: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:217:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:223:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:9:1: C901 'mutant_cycle_pivot' is too complex (11) +./src/tfscreen/analysis/extract_epistasis.py:9:1: E302 expected 2 blank lines, found 1 +./src/tfscreen/analysis/extract_epistasis.py:79:25: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:91:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:97:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:101:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:114:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:126:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:130:1: C901 'extract_epistasis' is too complex (19) +./src/tfscreen/analysis/extract_epistasis.py:134:37: E252 missing whitespace around parameter equals +./src/tfscreen/analysis/extract_epistasis.py:134:38: E252 missing whitespace around parameter equals +./src/tfscreen/analysis/extract_epistasis.py:212:27: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:212:34: E231 missing whitespace after ',' ./src/tfscreen/analysis/extract_epistasis.py:230:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:236:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:238:10: W291 trailing whitespace -./src/tfscreen/analysis/extract_epistasis.py:240:30: W291 trailing whitespace +./src/tfscreen/analysis/extract_epistasis.py:253:55: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:253:60: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:253:65: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:255:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:261:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:268:1: W293 blank line contains whitespace ./src/tfscreen/analysis/stats_test_suite.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/stats_test_suite.py:8:31: E231 missing whitespace after ',' ./src/tfscreen/analysis/stats_test_suite.py:8:42: E231 missing whitespace after ',' @@ -346,11 +342,11 @@ ./src/tfscreen/mle/curve_models/__init__.py:137:36: E231 missing whitespace after ':' ./src/tfscreen/mle/curve_models/__init__.py:138:31: E231 missing whitespace after ':' ./src/tfscreen/mle/curve_models/__init__.py:139:34: E201 whitespace after '[' -./src/tfscreen/mle/curve_models/__init__.py:142:34: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:143:34: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:144:35: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:145:30: E231 missing whitespace after ':' -./src/tfscreen/mle/curve_models/__init__.py:145:33: E201 whitespace after '[' +./src/tfscreen/mle/curve_models/__init__.py:145:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:146:34: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:147:35: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:148:30: E231 missing whitespace after ':' +./src/tfscreen/mle/curve_models/__init__.py:149:33: E201 whitespace after '[' ./src/tfscreen/mle/curve_models/guesses.py:3:69: W291 trailing whitespace ./src/tfscreen/mle/curve_models/guesses.py:13:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/mle/curve_models/guesses.py:27:20: E231 missing whitespace after ',' @@ -4057,7 +4053,8 @@ ./tests/tfscreen/analysis/test_extract_epistasis.py:220:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:226:1: W293 blank line contains whitespace ./tests/tfscreen/analysis/test_extract_epistasis.py:228:74: E231 missing whitespace after ',' -./tests/tfscreen/analysis/test_extract_epistasis.py:248:28: W292 no newline at end of file +./tests/tfscreen/analysis/test_extract_epistasis.py:287:9: E731 do not assign a lambda expression, use a def +./tests/tfscreen/analysis/test_extract_epistasis.py:411:79: W292 no newline at end of file ./tests/tfscreen/analysis/test_stats_test_suite.py:6:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/analysis/test_stats_test_suite.py:9:52: E261 at least two spaces before inline comment ./tests/tfscreen/analysis/test_stats_test_suite.py:10:1: W293 blank line contains whitespace @@ -7478,7 +7475,7 @@ ./tests/tfscreen/util/validation/test_check.py:8:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:26:1: E302 expected 2 blank lines, found 1 ./tests/tfscreen/util/validation/test_check.py:75:70: W292 no newline at end of file -54 C901 'cat_fit' is too complex (26) +54 C901 'cat_fit' is too complex (28) 10 E111 indentation is not a multiple of 4 6 E114 indentation is not a multiple of 4 (comment) 6 E116 unexpected indentation (comment) @@ -7486,7 +7483,7 @@ 1 E122 continuation line missing indentation or outdented 2 E124 closing bracket does not match visual indentation 6 E125 continuation line with same indent as next logical line -214 E127 continuation line over-indented for visual indent +213 E127 continuation line over-indented for visual indent 92 E128 continuation line under-indented for visual indent 3 E131 continuation line unaligned for hanging indent 46 E201 whitespace after '[' @@ -7496,7 +7493,7 @@ 1839 E221 multiple spaces before operator 26 E222 multiple spaces after operator 7 E225 missing whitespace around operator -1096 E231 missing whitespace after ',' +1097 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals 207 E261 at least two spaces before inline comment @@ -7506,7 +7503,7 @@ 1 E301 expected 1 blank line, found 0 767 E302 expected 2 blank lines, found 1 48 E303 too many blank lines (2) -19 E305 expected 2 blank lines after class or function definition, found 1 +20 E305 expected 2 blank lines after class or function definition, found 1 29 E306 expected 1 blank line before a nested definition, found 0 1 E401 multiple imports on one line 38 E402 module level import not at top of file @@ -7515,14 +7512,14 @@ 5 E712 comparison to True should be 'if cond is True:' or 'if cond:' 2 E713 test for membership should be 'not in' 1 E722 do not use bare 'except' -1 E731 do not assign a lambda expression, use a def +2 E731 do not assign a lambda expression, use a def 21 E741 ambiguous variable name 'l' 230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used -590 W291 trailing whitespace +587 W291 trailing whitespace 81 W292 no newline at end of file -1500 W293 blank line contains whitespace +1498 W293 blank line contains whitespace 28 W391 blank line at end of file -7480 +7477 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index e924dc8..f9a6083 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/cat_response/cat_assess.py b/src/tfscreen/analysis/cat_response/cat_assess.py index d0f982f..61a8f52 100644 --- a/src/tfscreen/analysis/cat_response/cat_assess.py +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -106,7 +106,7 @@ def assess_best_model(model_func, params, cov_matrix, x, y_obs, y_std, "sig_nonzero": sig_nonzero, } - # Data-based portmanteau chi-square vs the zero line (drives response_class). + # Data-based portmanteau chi-square vs the zero line (drives fittable). nonzero_chi2, nonzero_df, nonzero_p = _nonzero_chi2(y_obs, y_std) # Model-based omnibus on the fitted curve (reported only). omnibus_W, omnibus_df, omnibus_p = _omnibus_chi2(y_model, y_cov) diff --git a/src/tfscreen/analysis/cat_response/cat_fit.py b/src/tfscreen/analysis/cat_response/cat_fit.py index 47bcdef..407cfd8 100644 --- a/src/tfscreen/analysis/cat_response/cat_fit.py +++ b/src/tfscreen/analysis/cat_response/cat_fit.py @@ -272,7 +272,7 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, (unique) x, with columns ``model``, ``x``, ``y_obs`` (observed value), ``y_std`` (observed input error), ``y_model`` (fitted curve), ``y_model_std`` (propagated fit error), ``z`` (= y_obs/y_std), and - ``sig_nonzero``. Empty if no model could be fit. (``response_class`` is + ``sig_nonzero``. Empty if no model could be fit. (``fittable`` is added downstream in ``cat_response``.) """ diff --git a/src/tfscreen/analysis/cat_response/cat_response.py b/src/tfscreen/analysis/cat_response/cat_response.py index 4924859..032c100 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -10,7 +10,8 @@ After fitting, a post-hoc pass grades each group's curve against zero on the observed data (a per-point ``sig_nonzero`` test and the per-curve data-based ``nonzero`` test), plus a ROPE-based equivalence rollup and a Benjamini-Hochberg -FDR correction across curves (see :mod:`cat_assess`), yielding ``response_class``. +FDR correction across curves (see :mod:`cat_assess`), yielding the ``fittable`` +bool (distinguishable from zero) plus ``all_equiv_zero``. """ from concurrent.futures import ProcessPoolExecutor @@ -156,7 +157,9 @@ def cat_response(df, (data-based ``nonzero_chi2`` / ``nonzero_df`` / ``nonzero_p`` / ``nonzero_q`` which drive the zero call, reported-only model ``omnibus_W`` / ``omnibus_df`` / ``omnibus_p`` / ``omnibus_q``, - ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, ``response_class``). + ``n_nonzero``, ``any_nonzero``, ``all_equiv_zero``, and ``fittable`` + (bool: distinguishable from zero; with ``all_equiv_zero`` it recovers the + old real / confident_zero / indeterminate three-way)). ``best_model`` follows ``select_by`` (default lowest-AICc; see :func:`cat_fit`), ``shape`` is its qualitative form (flat/linear/step/peak/dip/biphasic), and ``shape_status`` is a @@ -172,7 +175,7 @@ def cat_response(df, ``best_only`` is False. assessment_df : pandas.DataFrame Self-contained per-point best-model assessment at the observed (unique) - x. Group-key columns, then ``model``, ``response_class`` (carried on + x. Group-key columns, then ``model``, ``fittable`` (bool, carried on every point for filtering; ``model`` name + fitted values left intact), ``x``, ``y_obs``, ``y_std``, ``y_model``, ``y_model_std``, ``z`` (= y_obs/y_std), ``sig_nonzero``. @@ -294,7 +297,7 @@ def cat_response(df, else: results_df["all_equiv_zero"] = pd.Series(dtype=bool) - # FDR across curves. nonzero_q (data-based) drives response_class; omnibus_q + # FDR across curves. nonzero_q (data-based) drives fittable; omnibus_q # (model-based) is kept for reference only. results_df["nonzero_q"] = benjamini_hochberg( results_df.get("nonzero_p", pd.Series(np.nan, index=results_df.index)) @@ -303,60 +306,45 @@ def cat_response(df, results_df.get("omnibus_p", pd.Series(np.nan, index=results_df.index)) ) - # Three-valued response class: real / confident_zero / indeterminate. - results_df["response_class"] = _response_class(results_df, alpha) + # fittable (bool): is the curve distinguishable from zero (worth + # interpreting its shape)? The confident_zero-vs-indeterminate split among + # the non-fittable curves is recoverable from all_equiv_zero. + results_df["fittable"] = _fittable(results_df, alpha) - # Surface response_class in the per-point assessment (its own column, so the - # model name and its y_model/y_model_std stay intact) for filtering there. + # Surface fittable in the per-point assessment (its own column, so the model + # name and its y_model/y_model_std stay intact) for filtering there. if len(assessment_df): assessment_df = assessment_df.merge( - results_df[group_cols + ["response_class"]], + results_df[group_cols + ["fittable"]], on=group_cols, how="left", ) else: - assessment_df["response_class"] = pd.Series(dtype=object) + assessment_df["fittable"] = pd.Series(dtype=bool) # Order columns: group keys first. other = [c for c in results_df.columns if c not in group_cols] results_df = results_df[group_cols + other] assess_other = [c for c in assessment_df.columns if c not in group_cols] - # Keep response_class right next to the model column. - if "response_class" in assess_other and "model" in assess_other: - assess_other.remove("response_class") - assess_other.insert(assess_other.index("model") + 1, "response_class") + # Keep fittable right next to the model column. + if "fittable" in assess_other and "model" in assess_other: + assess_other.remove("fittable") + assess_other.insert(assess_other.index("model") + 1, "fittable") assessment_df = assessment_df[group_cols + assess_other] return results_df, predictions_df, assessment_df, resolved_rope -def _response_class(results_df, alpha): +def _fittable(results_df, alpha): """ - Assign each curve to real / confident_zero / indeterminate (data-driven). - - "Distinguishable from zero" is checked first, on the observed error bars - (``nonzero_q``, the BH-adjusted data-based portmanteau test), so a real - signal wins even if it happens to fall inside the equivalence region. When a - curve is *not* distinguishable from zero, the ROPE separates the two - non-signal cases: tight error bars -> confidently flat at zero; wide error - bars -> too noisy to tell. - - - ``real``: ``nonzero_q < alpha`` -- the observed points collectively clear - the zero line. - - ``confident_zero``: not real, and every observed point's CI lies within - [-rope_cutoff, rope_cutoff] (confidently flat at zero). - - ``indeterminate``: not real and not tightly bounded (too noisy to call). + Bool per curve: is it distinguishable from zero (worth interpreting shape)? + + ``True`` iff the observed points collectively clear the zero line + (``nonzero_q < alpha``, the BH-adjusted data-based portmanteau test on the + observed error bars). ``False`` = not distinguishable from zero. The two + ``False`` cases -- "confidently flat at zero" vs "too noisy to tell" -- are + recoverable from the ``all_equiv_zero`` column (True = confidently flat). """ q = results_df.get("nonzero_q", pd.Series(np.nan, index=results_df.index)).to_numpy() - all_equiv = results_df.get( - "all_equiv_zero", pd.Series(False, index=results_df.index) - ).fillna(False).to_numpy(dtype=bool) - - out = np.full(len(results_df), "indeterminate", dtype=object) - # Not distinguishable from zero but tightly bounded -> confident_zero. - out[all_equiv] = "confident_zero" - # Distinguishable from zero wins (real-first precedence). - real = np.isfinite(q) & (q < alpha) - out[real] = "real" - return out + return np.isfinite(q) & (q < alpha) diff --git a/src/tfscreen/analysis/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index e8f743f..50b9be5 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -133,6 +133,7 @@ def extract_epistasis( y_std: Optional[str] = None, group_by: List[str] | str | None=None, scale: str = "add", + scale_constant: float = 1.0, keep_extra: bool = False, logit_eps: float = 1e-9, ) -> pd.DataFrame: @@ -170,6 +171,20 @@ def extract_epistasis( L = logit(Y). Requires an observable in (0, 1) (e.g. theta/occupancy); removes a saturating measurement scale so genuine (within-state) interactions are not masked by the nonlinearity of a bounded readout. + scale_constant : float, default 1.0 + A constant applied to the transformed observable *before* the epistasis + difference-of-differences is taken. Because epistasis on the "add" and + "logit" scales is a linear operator, this simply multiplies the reported + ``ep_obs`` by ``scale_constant`` and ``ep_std`` by ``abs(scale_constant)`` + -- but computing it up front keeps the two consistent (no error-prone + post-hoc rescaling). The main use is unit conversion: for a two-state + binding equilibrium, ``logit(theta) = -dG/RT``, so ``scale_constant`` + carries the observable onto a free-energy scale. Setting it to ``-RT`` + (e.g. ``-0.6159`` for kcal/mol at 310.15 K) reports ``ep_obs`` as an + interaction free energy; the caller owns the sign convention, the + temperature, and the choice of gas constant / units. This is a no-op for + the "mult" scale (the constant cancels in the ratio-of-ratios), so a + value other than 1.0 with ``scale="mult"`` is an error. keep_extra : bool, default False If True, all columns from the original DataFrame are kept in the output. If False, only key identifiers and the calculated epistasis @@ -189,7 +204,8 @@ def extract_epistasis( Raises ------ ValueError - If `scale` is not one of "add" or "mult". + If `scale` is not one of "add", "mult", or "logit", or if + `scale_constant` is not 1.0 when `scale="mult"` (where it has no effect). """ # Determine the epistatic scale @@ -197,7 +213,16 @@ def extract_epistasis( err = ("scale should be 'add' (additive), 'mult' (multiplicative), or " "'logit' (additive on the logit scale)\n") raise ValueError(err) - + + # scale_constant multiplies a per-point transform before the (linear) + # difference-of-differences. "mult" epistasis is a ratio-of-ratios, so the + # constant cancels exactly -- reject it rather than silently ignore it. + if scale == "mult" and scale_constant != 1.0: + err = ("scale_constant has no effect when scale='mult' (it cancels in " + "the ratio-of-ratios). Use scale='add' or 'logit', or leave " + "scale_constant at its default of 1.0.\n") + raise ValueError(err) + # Figure out what columns to extract extract_columns = [y_obs] if y_std is not None: @@ -241,14 +266,18 @@ def extract_epistasis( std_01 = cycles[f"01_{y_std}"] std_11 = cycles[f"11_{y_std}"] - # Additive scale + # Additive scale. scale_constant scales the (identity) transform, so it + # factors straight out of the linear difference-of-differences. if scale == "add": - ep_obs = (obs_11 - obs_10) - (obs_01 - obs_00) + ep_obs = scale_constant * ((obs_11 - obs_10) - (obs_01 - obs_00)) if y_std is not None: - ep_std = np.sqrt(std_11**2 + std_10**2 + std_01**2 + std_00**2) + ep_std = np.abs(scale_constant) * np.sqrt( + std_11**2 + std_10**2 + std_01**2 + std_00**2) # Logit scale: additive epistasis of logit(Y). Removes a saturating [0, 1] # measurement scale so within-state interactions are not masked by it. + # scale_constant multiplies the logit transform up front (e.g. -RT to put + # ep_obs on a free-energy scale, since logit(theta) = -dG/RT). elif scale == "logit": raw = [obs_00, obs_10, obs_01, obs_11] @@ -263,12 +292,14 @@ def extract_epistasis( ) clip = [o.clip(logit_eps, 1.0 - logit_eps) for o in raw] - t_00, t_10, t_01, t_11 = (np.log(c / (1.0 - c)) for c in clip) + t_00, t_10, t_01, t_11 = (scale_constant * np.log(c / (1.0 - c)) + for c in clip) ep_obs = (t_11 - t_10) - (t_01 - t_00) if y_std is not None: - # delta method: d/dy logit(y) = 1 / (y (1 - y)) + # delta method: d/dy [sc * logit(y)] = sc / (y (1 - y)) raw_std = [std_00, std_10, std_01, std_11] - t_std = [s / (c * (1.0 - c)) for s, c in zip(raw_std, clip)] + t_std = [np.abs(scale_constant) * s / (c * (1.0 - c)) + for s, c in zip(raw_std, clip)] ep_std = np.sqrt(sum(s**2 for s in t_std)) # Multiplicative scale diff --git a/src/tfscreen/analysis/scripts/cat_response_cli.py b/src/tfscreen/analysis/scripts/cat_response_cli.py index 6a951d0..2b7ecb0 100644 --- a/src/tfscreen/analysis/scripts/cat_response_cli.py +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -78,13 +78,13 @@ def cat_response(data_file, each group the best model is selected per --select_by (default the 'shape' classifier), then graded against zero on the observed data (per-point sig_nonzero + a per-curve data-based nonzero test with a Benjamini-Hochberg - FDR correction) to assign response_class. + FDR correction) to assign the 'fittable' bool. Writes: - {out_prefix}.csv one row per group; all models' weights and parameter estimates, plus the assessment rollups (nonzero_p/q, omnibus_p/q, - n_nonzero, all_equiv_zero, response_class). + n_nonzero, all_equiv_zero, fittable). - {out_prefix}_{model}.csv one file per model; that model's parameter table and per-group fit statistics. - {out_prefix}_predictions.csv best-model predicted curves (all models diff --git a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py index 4389e29..74fdd69 100644 --- a/src/tfscreen/analysis/scripts/extract_epistasis_cli.py +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -96,6 +96,7 @@ def extract_epistasis(data_file, y_std=None, group_by=None, scale="add", + scale_constant=1.0, keep_extra=False, logit_eps=1e-9): """ @@ -133,6 +134,13 @@ def extract_epistasis(data_file, Epistatic scale. "add" (default): (Y11 - Y10) - (Y01 - Y00). "mult": (Y11 / Y10) / (Y01 / Y00). "logit": additive epistasis of logit(Y) — requires an observable in (0, 1), such as theta/occupancy. + scale_constant : float, optional + Constant applied to the transformed observable before epistasis is + taken; multiplies ep_obs (and abs() multiplies ep_std). Default 1.0. + Mainly a unit conversion for scale="logit": since logit(theta) = -dG/RT, + passing -RT (e.g. -0.6159 for kcal/mol at 310.15 K) reports epistasis as + an interaction free energy. The caller owns the sign, temperature, and + gas constant/units. Has no effect on (and is rejected for) scale="mult". keep_extra : bool, optional If True, retain all columns from the input CSV in the output. If False (default), keep only the identifier columns and the calculated epistasis @@ -167,6 +175,7 @@ def extract_epistasis(data_file, y_std=y_std, group_by=group_by, scale=scale, + scale_constant=scale_constant, keep_extra=keep_extra, logit_eps=logit_eps) diff --git a/src/tfscreen/mle/curve_models/__init__.py b/src/tfscreen/mle/curve_models/__init__.py index 9999a87..261beca 100644 --- a/src/tfscreen/mle/curve_models/__init__.py +++ b/src/tfscreen/mle/curve_models/__init__.py @@ -138,12 +138,15 @@ "bounds":([-inf, 0, -inf, -inf], [ inf, inf, inf, inf])}, - # parallel competing processes + # parallel competing processes. baseline (y at x=0) and amplitude (y at + # x=inf) are left unbounded: pinning them >= 0 assumes a non-negative + # observable and cripples the fit on signed data (e.g. logit epistasis), + # giving a large-negative R2 so it can never be selected. "biphasic_dip": {"model_func":model_biphasic_dip, "guess_func":guess_biphasic_dip, "param_names":['baseline', 'amplitude', 'lnK_dip', 'lnK_rise'], - "bounds":([ 0, 0, -inf, -inf], - [inf, inf, inf, inf])}, + "bounds":([-inf, -inf, -inf, -inf], + [ inf, inf, inf, inf])}, } diff --git a/tests/tfscreen/analysis/cat_response/test_cat_fit.py b/tests/tfscreen/analysis/cat_response/test_cat_fit.py index 9796139..324cd4b 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -258,6 +258,19 @@ def test_adequacy_mode_escalates_off_flagged_pick(): assert adeq_out["best_model"] != "flat" +def test_biphasic_dip_fits_signed_data_and_can_win(): + """biphasic_dip's bounds allow signed data, so a genuinely biphasic curve + (start +, dip, rise) selects biphasic (regression: baseline>=0 crippled it).""" + x = np.logspace(-3, 3, 12) + K1, K2 = 0.3, 50.0 + y = 3.0 / (1.0 + x / K1) + 4.0 * (x / (K2 + x)) # dip-then-rise + ys = np.full_like(x, 0.05) + flat_output, _, _ = cat_fit(x, y, ys, select_by="shape") + assert np.isfinite(flat_output["R2|biphasic_dip"]) + assert flat_output["R2|biphasic_dip"] > 0.9 # not crippled + assert flat_output["shape"] == "biphasic" + + def test_invalid_select_by_raises(): x, y, ys = _hill_data() with pytest.raises(ValueError, match="select_by"): diff --git a/tests/tfscreen/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index d460493..5ab6f61 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -249,19 +249,20 @@ def test_predictions_tagged_with_group_keys(self): class TestAssessmentPass: - def test_assessment_tagged_and_has_response_class(self): + def test_assessment_tagged_and_has_fittable(self): with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit([])): _, _, assess, rope = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", group_by=["titrant_name"]) assert list(assess.columns[:2]) == ["genotype", "titrant_name"] - assert "response_class" in assess.columns + assert "fittable" in assess.columns + assert assess["fittable"].dtype == bool assert "equiv_zero" not in assess.columns # dropped from the output assert np.isfinite(rope) - def test_response_class_column_next_to_model(self): - # response_class is its own column immediately after model; the model + def test_fittable_column_next_to_model(self): + # fittable is its own bool column immediately after model; the model # name and its fitted values are left intact. fit = _capturing_fit([]) @@ -274,12 +275,11 @@ def fake(*a, **k): _, _, assess, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", rope_cutoff=0.5) - assert set(assess["response_class"]) == {"indeterminate"} - # model name preserved (not overloaded), and y_model left intact. + # not distinguishable from zero -> fittable False; model name preserved. + assert set(assess["fittable"]) == {False} assert set(assess["model"]) == {"flat"} - assert "indeterminate" not in set(assess["model"]) cols = list(assess.columns) - assert cols[cols.index("model") + 1] == "response_class" + assert cols[cols.index("model") + 1] == "fittable" def test_rope_defaults_to_median_times_multiplier(self): # Fake assessment always reports y_std=0.1 -> median=0.1 -> rope=0.2. @@ -298,9 +298,10 @@ def test_explicit_rope_cutoff_is_used(self): y_std="theta_std", rope_cutoff=0.75) assert rope == 0.75 - def test_response_class_confident_zero(self): - # Observed y=0 with tiny error -> CI well inside delta -> all_equiv_zero, - # and nonzero_p high (0.5) -> not distinguishable -> confident_zero. + def test_not_fittable_but_confident_zero(self): + # Observed y=0 with tiny error -> all_equiv_zero True; nonzero_p high + # (0.5) -> not distinguishable -> fittable False (recoverable as the old + # "confident_zero" via all_equiv_zero=True). fit = _capturing_fit([]) def fake(*a, **k): @@ -312,11 +313,12 @@ def fake(*a, **k): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", rope_cutoff=0.5) - assert set(results["response_class"]) == {"confident_zero"} + assert set(results["fittable"]) == {False} + assert set(results["all_equiv_zero"]) == {True} - def test_real_wins_over_equivalence(self): - # Real-first precedence: distinguishable from zero (nonzero_p tiny) wins - # even when every observed point sits inside the ROPE. + def test_fittable_true_even_inside_rope(self): + # Distinguishable from zero (nonzero_p tiny) -> fittable True, even when + # every observed point also sits inside the ROPE. fit = _capturing_fit([]) def fake(*a, **k): @@ -329,11 +331,11 @@ def fake(*a, **k): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", rope_cutoff=0.5) - assert set(results["response_class"]) == {"real"} + assert set(results["fittable"]) == {True} - def test_indeterminate_when_noisy_and_not_equiv(self): - # Not distinguishable from zero (nonzero_p high) and error bars too wide - # for the ROPE -> indeterminate (can't tell). + def test_not_fittable_and_not_equiv_is_indeterminate(self): + # Not distinguishable from zero and error bars too wide for the ROPE -> + # fittable False, all_equiv_zero False (the old "indeterminate"). fit = _capturing_fit([]) def fake(*a, **k): @@ -345,15 +347,16 @@ def fake(*a, **k): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", rope_cutoff=0.5) - assert set(results["response_class"]) == {"indeterminate"} + assert set(results["fittable"]) == {False} + assert set(results["all_equiv_zero"]) == {False} - def test_response_class_real_from_low_q(self): + def test_fittable_from_low_q(self): with patch.object(cat_response_mod, "cat_fit", side_effect=_capturing_fit([], nonzero_p=1e-8)): results, _, _, _ = cat_response( _basic_df(), x_obs="titrant_conc", y_obs="theta", y_std="theta_std", rope_cutoff=1e-6) - assert set(results["response_class"]) == {"real"} + assert set(results["fittable"]) == {True} assert (results["nonzero_q"] < 0.05).all() diff --git a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py index d3434c9..047b181 100644 --- a/tests/tfscreen/analysis/scripts/test_cat_response_cli.py +++ b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py @@ -74,12 +74,12 @@ def test_writes_main_permodel_and_predictions(self, tmp_path): assess = pd.read_csv(f"{out_prefix}_assessment.csv") assert "genotype" in assess.columns - assert {"model", "response_class", "x", "y_obs", "y_std", "y_model", + assert {"model", "fittable", "x", "y_obs", "y_std", "y_model", "y_model_std", "z", "sig_nonzero"}.issubset(assess.columns) assert "equiv_zero" not in assess.columns # dropped # Rollups landed on the main table. assert {"nonzero_p", "nonzero_q", "omnibus_p", "omnibus_q", "n_nonzero", - "response_class"}.issubset(main.columns) + "fittable"}.issubset(main.columns) def test_write_all_predictions_flag(self, tmp_path): data = _write(tmp_path, _theta_df()) diff --git a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py index 507cbd6..83ad7c7 100644 --- a/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py +++ b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py @@ -119,6 +119,27 @@ def test_missing_group_by_raises(self, tmp_path): out_prefix=str(tmp_path / "out")) +class TestScaleConstant: + + def test_scale_constant_scales_add_output(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + out_prefix = str(tmp_path / "out") + + extract_epistasis(data, y_obs="y", y_std="y_err", + scale_constant=-2.0, out_prefix=out_prefix) + + out = pd.read_csv(f"{out_prefix}.csv") + # base add ep = 1.0, std = 0.2 -> scaled by -2.0 / abs(-2.0) + assert out["ep_obs"].iloc[0] == pytest.approx(-2.0) + assert out["ep_std"].iloc[0] == pytest.approx(0.4) + + def test_mult_scale_constant_raises(self, tmp_path): + data = _write_csv(tmp_path, _single_cycle_rows()) + with pytest.raises(ValueError, match="no effect when scale='mult'"): + extract_epistasis(data, y_obs="y", scale="mult", + scale_constant=2.0, out_prefix=str(tmp_path / "out")) + + class TestQuantileDefaults: def _quantile_cycle_rows(self): @@ -228,6 +249,7 @@ def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): "--y_std", "y_err", "--group_by", "condition", "--scale", "add", + "--scale_constant", "-2.0", "--keep_extra"] monkeypatch.setattr(sys, "argv", argv) @@ -238,3 +260,5 @@ def test_main_parses_positionals_and_flags(self, tmp_path, monkeypatch): assert "ep_std" in out.columns # keep_extra retained the original observable column. assert "y" in out.columns + # --scale_constant parsed as a float and applied (base ep = 1.0). + assert out["ep_obs"].iloc[0] == pytest.approx(-2.0) diff --git a/tests/tfscreen/analysis/test_extract_epistasis.py b/tests/tfscreen/analysis/test_extract_epistasis.py index dca5594..56e38b0 100644 --- a/tests/tfscreen/analysis/test_extract_epistasis.py +++ b/tests/tfscreen/analysis/test_extract_epistasis.py @@ -331,4 +331,81 @@ def test_logit_custom_eps(self): result = extract_epistasis(df, "theta", scale="logit", logit_eps=eps) # wt (00) clamped to 1 - eps; recompute the expected diff-of-diffs expected = (_logit(0.3) - _logit(0.5)) - (_logit(0.8) - _logit(1.0 - eps)) - assert np.isclose(result.iloc[0]["ep_obs"], expected) \ No newline at end of file + assert np.isclose(result.iloc[0]["ep_obs"], expected) + + +# --- scale_constant ---------------------------------------------------------- + +@pytest.fixture +def add_cycle_df(): + """One complete cycle with a nonzero additive epistasis (ep = 0.1).""" + return pd.DataFrame({ + "genotype": ["wt", "A10G", "P25L", "A10G/P25L"], + "fitness": [1.0, 0.8, 0.5, 0.4], # (0.4-0.5) - (0.8-1.0) = 0.1 + "error": [0.05, 0.04, 0.03, 0.06], + }) + + +class TestScaleConstant: + + def test_default_is_identity(self, add_cycle_df): + """scale_constant=1.0 (default) leaves ep_obs/ep_std unchanged.""" + base = extract_epistasis(add_cycle_df, "fitness", y_std="error", + scale="add").iloc[0] + same = extract_epistasis(add_cycle_df, "fitness", y_std="error", + scale="add", scale_constant=1.0).iloc[0] + assert np.isclose(same["ep_obs"], base["ep_obs"]) + assert np.isclose(same["ep_std"], base["ep_std"]) + + def test_add_scales_obs_and_std(self, add_cycle_df): + """add: ep_obs *= sc (signed); ep_std *= abs(sc).""" + sc = -2.5 + base = extract_epistasis(add_cycle_df, "fitness", y_std="error", + scale="add").iloc[0] + scaled = extract_epistasis(add_cycle_df, "fitness", y_std="error", + scale="add", scale_constant=sc).iloc[0] + assert np.isclose(scaled["ep_obs"], sc * base["ep_obs"]) + assert np.isclose(scaled["ep_std"], abs(sc) * base["ep_std"]) + + def test_logit_scales_obs_and_std(self, theta_cycle_df): + """logit: ep_obs *= sc (signed); ep_std *= abs(sc).""" + sc = -0.6159 + base = extract_epistasis(theta_cycle_df, "theta", y_std="theta_std", + scale="logit").iloc[0] + scaled = extract_epistasis(theta_cycle_df, "theta", y_std="theta_std", + scale="logit", scale_constant=sc).iloc[0] + assert np.isclose(scaled["ep_obs"], sc * base["ep_obs"]) + assert np.isclose(scaled["ep_std"], abs(sc) * base["ep_std"]) + + def test_logit_energy_equals_minus_RT_logit(self, theta_cycle_df): + """The energy use case: ep(sc=-RT) == -RT * ep(logit).""" + RT = 0.6159 + logit_ep = extract_epistasis(theta_cycle_df, "theta", + scale="logit").iloc[0]["ep_obs"] + energy_ep = extract_epistasis(theta_cycle_df, "theta", scale="logit", + scale_constant=-RT).iloc[0]["ep_obs"] + assert np.isclose(energy_ep, -RT * logit_ep) + + def test_scales_without_std(self, add_cycle_df): + """scale_constant works when no y_std is provided.""" + sc = 3.0 + base = extract_epistasis(add_cycle_df, "fitness", + scale="add").iloc[0]["ep_obs"] + scaled = extract_epistasis(add_cycle_df, "fitness", scale="add", + scale_constant=sc).iloc[0]["ep_obs"] + assert np.isclose(scaled, sc * base) + assert "ep_std" not in extract_epistasis( + add_cycle_df, "fitness", scale="add", scale_constant=sc).columns + + def test_mult_rejects_nonunit_constant(self, add_cycle_df): + """scale_constant cancels on the mult scale -> reject != 1.0.""" + with pytest.raises(ValueError, match="no effect when scale='mult'"): + extract_epistasis(add_cycle_df, "fitness", scale="mult", + scale_constant=2.0) + + def test_mult_allows_unit_constant(self, add_cycle_df): + """scale_constant=1.0 is fine with mult (the default path).""" + result = extract_epistasis(add_cycle_df, "fitness", scale="mult", + scale_constant=1.0) + # (0.4/0.5) / (0.8/1.0) = 1.0 + assert np.isclose(result.iloc[0]["ep_obs"], (0.4 / 0.5) / (0.8 / 1.0)) \ No newline at end of file diff --git a/tests/tfscreen/simulate/toy_thermo/test_integration.py b/tests/tfscreen/simulate/toy_thermo/test_integration.py index 1108adb..0ef750a 100644 --- a/tests/tfscreen/simulate/toy_thermo/test_integration.py +++ b/tests/tfscreen/simulate/toy_thermo/test_integration.py @@ -29,7 +29,7 @@ def test_cat_response_consumes_titration_df(): # one row per genotype (wt + 3 singles + 3 doubles) assert len(res) == 7 assert "best_model" in res.columns - assert "response_class" in res.columns + assert "fittable" in res.columns def test_extract_epistasis_consumes_titration_df(): From cbdde604557c1ef69d09ca5697dbb01a4f1ad0fb Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 21 Jul 2026 08:33:06 -0400 Subject: [PATCH 23/26] added toy model version to investigate observables --- examples/toy_thermo/toy_thermo.ipynb | 584 +++++++++++++++++- src/tfscreen/simulate/toy_thermo/__init__.py | 15 + src/tfscreen/simulate/toy_thermo/basis.py | 425 +++++++++++++ .../simulate/toy_thermo/test_basis.py | 248 ++++++++ 4 files changed, 1252 insertions(+), 20 deletions(-) create mode 100644 src/tfscreen/simulate/toy_thermo/basis.py create mode 100644 tests/tfscreen/simulate/toy_thermo/test_basis.py diff --git a/examples/toy_thermo/toy_thermo.ipynb b/examples/toy_thermo/toy_thermo.ipynb index 991727e..beac71e 100644 --- a/examples/toy_thermo/toy_thermo.ipynb +++ b/examples/toy_thermo/toy_thermo.ipynb @@ -322,8 +322,8 @@ ], "source": [ "effects = (MutationEffects()\n", - " .add_mutation(\"A1V\", HD=3.0) # weakens repression\n", - " .add_mutation(\"A2V\", LE=-3.0) # tightens effector binding\n", + " .add_mutation(\"A1V\", HD=0.0) # weakens repression\n", + " .add_mutation(\"A2V\", LE=0.0) # tightens effector binding\n", " .add_epistasis(\"A1V\", \"A2V\")) # NB: zero thermodynamic epistasis\n", "\n", "df = build_titration_df(model, effector, effects=effects,\n", @@ -340,7 +340,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAF6CAYAAADxk+dgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAqvJJREFUeJzs3Xd8U9X7wPHPTbpX2lKgg9KWvSkgCFKgDClbGcpSZIhfUBw/UAQciAMQwYEDRdkIlL2RvTfIni10UVqgdO+0ub8/Qiuhe96O83698mpzx7lP0jR58txzz5FkWZYRBEEQBEGoRFRKByAIgiAIglDaRAIkCIIgCEKlIxIgQRAEQRAqHZEACYIgCIJQ6YgESBAEQRCESkckQIIgCIIgVDoiARIEQRAEodIRCZAgCIIgCJWOSIAEQRAEQah0RAIkCDmQJAlvb+9SP667uzvu7u6lftzywNvbG0mSiqWtkSNHIkkSgYGBxdJecVHidffFF18gSRKHDh3K9z5K/X8IQnERCZBQph06dAhJkvjiiy+UDkUoZ8RrRygMpV83hUlGhcIxUjoAQSirbty4gYWFhdJhCIIgCCVAJECCkIMGDRooHYIgCIJQQsQpMKHEREVFoVar6dOnj8HyixcvIkkSkiTh7+9vsM7b2xtzc3NSUlL44osv6Ny5MwAzZszI3Keo/TY2bNhAp06dqFatGmZmZjg7O9OtWzc2bNhgsF12fRwy+o0EBAQwf/58GjRogKmpKW5ubsyYMQOdTpfleImJiUyePBlXV1fMzMxo0qQJf/75Z4FL7bIss3jxYtq3b4+NjQ0WFhY899xzLF68OMu2ycnJzJs3j+bNm6PRaLC0tMTd3Z1XX32VS5cu5fu5ys6mTZsYOnQoderUwcLCAo1GQ4cOHbI8fwCBgYFIksTIkSPx9/enf//+2NnZYWlpSbdu3XKM5dixY3Tq1AlLS0uqVKnC4MGDCQkJyXeMBXntyLKc778lwJYtW+jatSt2dnaZf8+5c+eSnp6e7/gA/vrrL5o0aYKZmRmurq5MnjyZ5OTkHLcPCgpizJgxuLi4YGJiQo0aNRgzZgzBwcFZts2tH1le/agWLVpE06ZNMTMzw8XFhf/7v/8jLi4u348rNTWV77//npYtW2JpaYm1tTUdOnRg69at+W5j6dKlSJLE0qVL2bZtG88//zwWFha4uLjw2WefZf5tli1bRvPmzTE3N6dmzZp89913WdrKra/Xs6eb8vO6uX37NpMnT6Zly5ZUqVIFMzMz6tWrx5QpU4iPj89yjIznW6vV8sUXX+Du7o6pqSn16tXjt99+y7LtjBkzAOjcuXPmsUWfwJIhKkBCibGzs6N58+YcPXqU9PR01Go1AAcPHszc5uDBg9SpUwfQf2ifOnWKF154AVNTU7y9vQkMDGTZsmV06tTJIBmxtbUtVEwLFizg7bffxsnJif79+1OlShXCw8M5c+YMmzZtYuDAgflq56OPPuLw4cP06dMHHx8fNm/ezBdffEFqairffPNN5nbp6en06dOHgwcP0rRpU4YNG0ZkZCSTJk0qUAdSWZYZPnw4q1evpm7dugwbNgwTExP27t3LmDFjuH79OnPnzs3c/o033mDt2rU0a9aMUaNGYWpqSkhICAcPHuTs2bM0b94838d+1tSpUzExMcHLywsnJycePXrE1q1bGTRoEPPnz+fdd9/Nsk9gYCBt27alcePGjB49mjt37rBlyxY6d+7MjRs3qF69eua2+/fvp2fPnqhUKgYPHoyzszP79++nffv22NnZ5SvGgrx28vu3zHjss2fPxsXFhQEDBqDRaDh69CgfffQRp0+fZt26dfmK76uvvuLzzz+nevXqjB07FmNjY3x9fblx40a229++fRsvLy8ePXpE3759ady4MVevXmXx4sVs27aNY8eOUa9evXwdOzfff/89+/fvZ/DgwfTu3Zt9+/bx448/curUKY4cOYKxsXGu+6ekpNCjRw8OHTqEp6cnY8aMQavVsmPHDl566SV+/vlnJkyYkO94Nm3axJ49e3j55Zdp3749O3bs4Ouvv0aWZTQaDV9//TUvvfQS3t7ebNiwgcmTJ1O9enVGjBhRqMefn9fNxo0bWbRoEZ07d8bb2xudTsepU6f49ttvOXz4cI7P09ChQzlz5gw9e/ZErVazdu1a3nnnHYyNjRk7diygT9YADh8+zBtvvJGZ+BT2/U7IgywIJWjixIkyIJ8+fTpzWd++feV69erJrq6u8tChQzOX79+/XwbkL7/8MnPZwYMHZUCePn16scTTsmVL2cTERH7w4EGWdREREQb3AblTp04Gy9544w0ZkD08POT79+9nLn/06JFsa2srW1tbyykpKZnL//rrLxmQe/bsKaelpWUuv3btmmxmZpbt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", + "image/png": "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du7cydSpU6ldu3aO7VSvXp0JEyYwceJEGjRoQL9+/bCysmL16tU0aNBA786zDLNnzyYwMJBXXnmF0NBQ6tSpw/Hjx1m/fj3lypVj9uzZevXffPNNFi5cyMsvv8wff/xBuXLl2LlzJ3fv3iUgICDbBSiDg4N56qmnGDBgAOXKleOvv/7iwIEDtGzZMsv1nZ40efJk/vnnH7799ls2btxImzZt8PT05PLlyxw7dowjR47w999/ZzvHqzD06tWLatWqsWjRImJjY2nUqBEnTpxg27ZtupsFHpfT+6ZixYr07t2b1atX07RpUzp06MC1a9fYsGEDHTp00I2Q5kfGAojjx4/n+PHjuLq64ubmlqf1k4SBTHX/vShdMtYHadKkSaZ9NWrUyLQezOMiIiJU27ZtlbOzs24dmIw1PdatW6cA9dFHHxkUx48//qh69uypfH19lZ2dnSpTpoxq3ry5mj17tt76OkrlvA6QoWuKKKXU/fv31ejRo5WXl5eytbVVderUUXPnzlWrVq1SgPr666/16ue0hktISIjq2LGjcnd3V9bW1srb21sFBQWpL7/8Ut24cUMppdSdO3fUpEmTVJs2bVTFihWVjY2N8vLyUp07d1abNm3Sa+/OnTvKwsJCtWrVKte+y3D48GHVqVMn5e7urpydnVXbtm3Vn3/+qVu7ZeHChbq6GesADRkyJMu2supjpZTasWOHatOmjbK3t1ceHh6qb9++KiYmJss1bHKS03vHmJ+lUkr98ccfqkePHqpcuXLK2tpaVahQQT311FNqypQp6uLFiwbH9tNPP6k6deooGxsbValSJfX++++rhw8fZtsn0dHRatiwYapixYrKyspKVaxYUQ0bNkxFR0dn2f62bdtUixYtlK2trSpTpox68cUX1bVr13JcBygsLEz99NNPqm7dusrW1lZVrFhRvfPOOyohISFT+9nFmZqaqubMmaNatWqlXFxclK2trapcubLq3Lmzmj17tt4aStnJ6r2UVaxPyu5neuHCBfXss88qZ2dn5ejoqDp06KD279+fbVs5vW/u3bunRo8erfz8/JStra3y9/dXU6ZMUcnJyVn2SU7v2eziXbRokapfv76ytbVVQLa/D0T+aJR64nqAEMXI+++/z+zZs4mJiaFs2bKmDidPPv74Yz777DN+//13unTpYpIYNmzYQI8ePdi4cSNdu3Y1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oxKiZMvXHKB169YxbNgw4uPjebIZjUaDs7MzCxcu5Lnnnst3oOZA5gAJIQpLWmoq29vWp+Kt9O+1GojsUZP+M9eaNC4hipNCuQ3+SXv37qVv376kpqbStGlTBg8eTJUqVQCIjo5myZIl7N+/n/79+7Nz505atGhh7KmEEKLEO3HgD8rf/t/3Fgrqhp4i8vlN1GvZxXSBCVFCGX0J7NNPPyUtLY0vvviCffv28eabb9KtWze6devGG2+8wd69e/nyyy9JTU1lypQpBRmzEEKUONEHt2HxxHi8pYKYf8JNEo8QJZ3RCdCePXuoV68eo0ePzrbOe++9R/369dm9e7dR59i/fz9du3bFzc0NR0dHWrZsyYoVK/LczvXr13nvvffw9/fHzs6OMmXK8NRTTzF79myj4hJCiILm16Q92ieWWtNqwLdxkEniEaKkMzoBSklJoX79+rnWq1evHikpKXluPywsjFatWrFr1y769evHyJEjiYuLo3///nz55ZcGt3P48GHq1avH999/T926dXnvvfcYOHAgjo6OhIaG5jkuIYQoDPVadiGyR03SHkuC7jhD7abPmC4oIUowoydBN23aFEdHR7Zv355jvaCgIO7du8fBgwcNbjs1NZVatWpx6dIlIiIiaNiwIQDx8fE0b96c6OhoTp8+ja+vb47tJCQkUL9+fRITE/nzzz9p0KBBpvNY5WHVVZkELYQobJERmzj110qq/fo3tqlwclgbnhs7x9RhCVEsFMlCiK+++iq7du3KMQHavn07O3fu5NVXX81T29u2bePcuXMMHDhQl/wAuLq6Mn78eJKTk1m8eHGu7fz4449cvHiRGTNmZEp+gDwlP0IIURTqtexC748WcKpx+i9vp407SJNnKQpR4IxOgF555RXeffddunXrxvvvv8/Ro0e5d+8e9+7d4+jRo3zwwQd069aNd999lxEjRuSp7fDwcAA6deqUaV9wcDBAriNPACEhIWg0Gnr37s2pU6f47rvvmDlzJuvXryc5OTlPMQkhRFFq9uF3PLKGStdh/azXTR2OECWOwZfALC0tjT9JHp8G37dvX1atWsWBAwdo0qRJpv3Ozs64u7tz8eLFbNtITk7GwcGBMmXK8NZbbzFx4kS0Wq1uf9WqVVm7dm2O85iSkpJISkrSfZ+QkICPj49cAhNCFInlQ54iYO9dLntCu23H5EGp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", "text/plain": [ "
" ] @@ -402,8 +402,9 @@ " \n", " genotype\n", " best_model\n", - " response_class\n", - " omnibus_q\n", + " shape\n", + " fittable\n", + " nonzero_q\n", " \n", " \n", " \n", @@ -411,28 +412,32 @@ " 0\n", " wt\n", " repressor\n", - " real\n", + " step\n", + " True\n", " 0.0\n", " \n", " \n", " 1\n", " A1V\n", " repressor\n", - " real\n", + " step\n", + " True\n", " 0.0\n", " \n", " \n", " 2\n", " A2V\n", " repressor\n", - " real\n", + " step\n", + " True\n", " 0.0\n", " \n", " \n", " 3\n", " A1V/A2V\n", " repressor\n", - " real\n", + " step\n", + " True\n", " 0.0\n", " \n", " \n", @@ -440,11 +445,11 @@ "" ], "text/plain": [ - " genotype best_model response_class omnibus_q\n", - "0 wt repressor real 0.0\n", - "1 A1V repressor real 0.0\n", - "2 A2V repressor real 0.0\n", - "3 A1V/A2V repressor real 0.0" + " genotype best_model shape fittable nonzero_q\n", + "0 wt repressor step True 0.0\n", + "1 A1V repressor step True 0.0\n", + "2 A2V repressor step True 0.0\n", + "3 A1V/A2V repressor step True 0.0" ] }, "execution_count": 7, @@ -459,7 +464,7 @@ " df, x_obs=\"titrant_conc\", y_obs=\"observable\", y_std=\"observable_std\",\n", " progress=False)\n", "\n", - "results[[\"genotype\", \"best_model\", \"response_class\", \"omnibus_q\"]]" + "results[[\"genotype\", \"best_model\", \"shape\", \"fittable\", \"nonzero_q\"]]" ] }, { @@ -479,13 +484,13 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "f7b22c30", "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAApEAAAF6CAYAAABBQAMXAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAmmNJREFUeJzs3XdYU+fbB/DvScKGMARkDxE3zqIoijjBVetEbZ3VqrW11k7tr44uq9ZW27dDa+tq3VpnwQm4FRcuUGQrIMoKe+V5/6CJxAxCCCTg/bkuLs3Jk5Obw8k5d57JMcYYCCGEEEIIqQWergMghBBCCCGNDyWRhBBCCCGk1iiJJIQQQgghtUZJJCGEEEIIqTVKIgkhhBBCSK0J6vLiiooKxMbG4smTJ8jJyYG1tTWaN2+Otm3bgs/naytGQgghhBCiZ2qdRBYVFWHbtm04cOAAzp07h6KiIrkypqam6N27N0aNGoU33ngDpqamWgmWEEIIIYToB07deSJzc3Px1Vdf4Y8//oBIJAJjDBzHwcHBAc2aNYNQKEReXh6ysrKQkZFRtXOOg1AoxMyZM/HZZ5/BysqqPn8XQgghhBDSQNRKIn/77Td8/vnnyMrKgp2dHSZNmoRBgwahZ8+esLa2liufnZ2NCxcu4MSJE9ixYweePXuGZs2a4auvvsLs2bPr5RchhBBCCCENR62BNW+//TacnJywd+9ePH78GD/88AOGDh2qMIEEABsbGwwfPhzr1q1DWloa9uzZA0dHR7z99ttqB7Zs2TJwHIeIiAi1X6NrERER4DgOy5Yt0+p+N2/eDI7jsHnzZq3uV10eHh7w8PDQyXvXhq6Pk77Q5Dg0lr9xfdLk81tfn/mmKDAwEBzH6ToMvfOyHZfGfK3hOA6BgYG6DqNW6vv8UiuJ3LZtG27evInRo0dDIBAgKSkJHMep/JGcJAKBAGPGjEF0dDS2bt1ab7/IixrjH1tXGmPCTkhDacw3vYZEX+IIUa6p3mfVGljz+uuvK9zu5eWFN954Q+FzL/Z/5DhO6X4UeeeddzBhwgS4ubmp/Rpd6969O2JiYmBra6vV/Y4aNQp+fn5wdHTU6n4JIc/V1+eXEPLcqVOndB2CxmJiYhrdQOGtW7cqHACtLXWa4qdly5b11oxja2vb6C7mpqamaNOmjdb3a2lpCUtLS63vlxDyXH19fgkhz3l5eek6BI01xutDfVfEaX2y8cjISKxbtw4HDhyAWCyWbpc0Lz969AgTJ06Era0tTE1N4e/vj5MnT8rtR1nVb3h4OIYMGQInJycYGRmhefPm6NOnDzZs2ADgeR8lSSzVm9glzSx5eXlYuXIl+vbtCycnJxgaGsLJyQlTpkxBfHy8XCwlJSVYs2YNOnXqBEtLS5iZmcHDwwPjx49HdHS0tJyy/lFxcXGYPn06PD09YWRkBBsbG3Tq1AkLFiyAOoPjlTUTSY7pkydPMHXqVNja2sLExAR+fn5qV5kHBgZi+fLlAIB+/frJdUeorqCgAO+995702Hfs2BF79+5VuN+ysjJ8//336Nq1K8zMzGBhYYE+ffrg0KFDasUlUVFRge+//x6dOnWCiYkJLC0t0a9fPxw+fFjl6w4ePIju3bvD1NQUdnZ2mDFjBp48eSJX7vr16xg7dizc3NxgZGQEOzs7+Pr64uuvv5Yrm5mZiffffx8tW7aEkZERbG1tMWbMGNy5c0eurKQJNDc3F++88w5cXV0hEAiwefNmDBgwADweD8nJyQpjnz9/PjiOw4kTJwBUHcuffvoJQUFBcHV1hZGREezt7TF69GjcuHFDK8dBGcYY/vzzT/j7+0MoFMLU1BSvvPIK/vzzT7X30RCio6PBcRzeeecdme0HDhwAx3EwMjKS+zbu4eEBT09P6eMXP7+SbjvJyclITk6WuZYo+vJ89epVDBo0CBYWFrC0tMSoUaOQlJSkVvySz7iqnxcdPHgQAwYMgLW1NYyNjdGhQwd89913qKysVLjvzZs34/Dhw/D394eFhYXMZ/zZs2dYsGCB9Bplb2+P8ePHKzy3FZk2bRqmT58OAJg+fbrKuMvLy7Fs2TJ4eHjAyMgIrVq1wi+//KJwv7U5/6rfMzZv3oyuXbvC1NRU2q1J0jestLQUixcvhpubG0xMTNCtWzfpPSgvLw/z5s2Dk5MTjI2N0bNnT1y5ckVhbHfu3MH48eNhb28PIyMjeHp6YsGCBcjKylJY/ty5c+jbty/MzMzQrFkzhISEIDU1Va7c//73P3Ach927dyvcz59//gmO47BixQrpttreC65du4Z33nkHHTp0gKWlJUxMTODj44Nvv/0W5eXlcuUl17O8vDzMnTsXjo6OMDMzQ0BAAK5fvw4ASEtLwxtvvAF7e3uYmJhg8ODBiIuLU7qvFzHGsGnTJvTp0wdWVlYwNTWFt7c3Zs+ejZSUFIXHQtE+NDlf/vjjD/j4+MDY2BjOzs54//33kZ+fL/caRd3k8vLysGTJErRr1w7m5uYQCoVo2bIlpk6dKr3Gq3OfDQ8Px4wZM9C6dWuYm5vD3Nwcr7zyijS3eZG69y5FfSLFYjE2btyI7t27w8bGBiYmJnBxccGIESNq3dyuUU2kJHHIycmR2f7uu+/KXAwGDBiA0NBQ6cTjOTk58Pf3h52dHWbOnImnT59i165dCA4Oxt69e/Haa6+pfN+jR49ixIgRsLKywsiRI+Ho6IinT58iOjoa27Ztw1tvvQUPDw8sXboUy5cvh7u7O6ZNmyZ9fefOnQFUVUkvWbIE/fr1w6hRo2BmZobY2Fhs374dR48exfXr1+Hu7i593dSpU7F792507NgR06dPh5GREVJTUxEeHo6oqCh06tRJacxpaWno3r07CgsLMWzYMISEhKCwsBBxcXH45Zdf8N1330Eg0LxCODc3F71794alpSUmT56MzMxM7Nq1C0FBQbh27Ro6dOig8vWS4xMZGYmpU6dKT+oXuyOUl5dj8ODByMnJwZgxY1BUVISdO3di/PjxCAsLw+DBg6VlS0tLERwcjIiICHTu3BlvvvkmysvLcfToUYwcORI//fST3M1eEcYYxo4di4MHD6JVq1aYN28eCgsLsWvXLrz66qv4/vvv8f7778u9bt++fTh27BjGjh2LgQMH4tKlS9i0aRPOnj2LK1euSAeE3bx5E7169QKfz8fIkSPh7u6O3Nxc3Lt3Dxs2bMBnn30m3Wd8fLz0S9DgwYPx2muvITMzU/pep06dQo8ePWTiKC0tRf/+/VFQUIBXX30VAoEAzZs3x+TJk3H69Gn8/fffWLx4scxrKioqsHPnTjg5OWHAgAEAqmY7WLBgAfr06SMd0JaQkIBDhw4hNDQUZ86cga+vr8bHQdXxf/3117Fjxw54e3tj0qRJMDQ0xIkTJ/Dmm2/i3r17+O6772r8OzaEjh07olmzZggPD5fZLnlcVlaG8+fPY9CgQQCAxMREJCcnSxMfRaysrLB06VKsXbsWALBgwQLpcy/eSKKiorBq1Sr069cPs2fPxo0bN3DgwAHcvn0bd+7cgbGxscr4O3fujKVLl8ptT09Px4YNG2BiYiKzfdGiRfj222/h7OyM0aNHw9LSEmfPnsVHH32Ey5cvY8+ePXL72rNnD44fP47hw4fj7bffhkgkAgA8ffoUPXv2lJ7jEyZMQGJiIvbu3YujR4/i2LFj6N27t8r4X3vtNeTm5uLgwYMYOXKk9FqryMSJE3HlyhUMGTIEfD4fu3fvxrx582BgYIBZs2ZJy2l6/q1evRrh4eEYOXIkBg8eLLfwRUhICG7fvo1XX30VxcXF+PvvvzF8+HCcP38eb731FsrKyjBu3DiZ+1NiYqJMa9C5c+cQFBSEsrIyjB07Fh4eHrh48SLWrVuHI0eO4NKlSzItaadOncKQIUPA4/EQEhICJycnnDp1Cv7+/nKfw1mzZmHFihXYuHEjxo8fL/f7/f777xAIBHLnbm3uBb///jsOHz6MgIAADB06FEVFRYiIiMCiRYsQFRWFffv2yb1vWVkZBg0ahJKSEoSEhODJkyfYvXs3Bg4ciAsXLiAoKAiOjo5444038PDhQxw+fBjDhg1DTExMjYuPiMVihISEYO/evXB2dsbEiRMhFAqRlJSE3bt3Y8iQITXWqGl6vnz//fc4deoUQkJCMGzYMJw8eRJr167FpUuXcObMGRgYGKh8z6CgIFy+fBn+/v4IDg6WVhAcOnQIkydPlslDVN1nV65ciYcPH8LPzw+jRo1Cbm4uwsLCMHv2bNy/fx9r1qyRlq3NvUuRRYsWYdWqVfDy8sKkSZNgYWGBx48f49y5czh58mTtxpMwDfTr148BYC1atGBLly5lS5cuZbNmzWIAmEAgYG3atGG2traMx+Oxv/76i/03jRADwCZNmsTEYrF0X9HR0czQ0JDZ2dmxoqIi6falS5cyACw8PFy6bfTo0QwAu3nzplxMz549k3kMgPXt21dh/Lm5uSwrK0tu++nTpxmPx2MzZ86UKctxHOvWrRurqKiQKV9RUcFycnKkj8PDwxkAtnTpUum2H3/8kQFga9eulXs/RTEosmnTJgaAbdq0SWa75Ji+/fbbrLKyUrp948aNDACbPXu2WvtXdKyrc3d3ZwDYyJEjWWlpqXT7yZMnGQAWFBQkU37x4sUMAPv8889l/tYikYi98sorzNDQkD1+/LjGuLZs2SL9O1Z/3+TkZGZra8sEAgGLj4+XbpccJwAsLCxMZl+ffvopA8Deeecd6baFCxcyAOzAgQNy7/3i+dSrVy/G5/Pl9nv//n1mYWHBfHx8ZLZLjllQUJDMeS05DiYmJqxdu3Zy73v48GEGgH344YfSbSUlJezRo0dyZe/cucPMzc3ZwIEDZbbX9jhI4nV3d5fZtmHDBgaATZ8+nZWVlUm3l5aWshEjRjAA7OrVq3Jx6Yrk+pCRkSHd5uPjw/r06cMMDQ3ZokWLpNv/+OMPBoBt3bpVuk3R55cxxcfmxdcAYDt37pR5bvLkyQwA27Fjh0a/T2FhIevWrRvjOI7t3btXuv348ePSc6ugoEC6XSwWszlz5jAAMuUl5wOPx2MnTpyQe5/p06czADLHhzHGjh49ygCwli1bylxflFF2nZLo27cvA8B69OjB8vLypNtjY2OZQCBgrVu3lilf2/NPch0zMzNjt27dUvr+vXv3ljluu3btYgCYlZUVGzduHCsvL5c+t3LlSgaArVmzRrqtsrKSeXl5Kfx8ffTRRwwAmzFjhkz5Fi1aMI7j2NmzZ6XbxWIxmzRpkvT8qW7IkCGM4ziWmJgos/3OnTsMAHvttddkttf2XpCcnCx3PxOLxWzGjBkMADt37pzMc5LrmbLjY2Vlxd5//32Z6/3cuXMZALZv3z65fb34efrpp58YADZgwAC562VRUZFa90pNzxdDQ0MWHR0tcxwkf5fvvvtO5j1ezCtu3bql8O/BWNV1Oz8/X+79lN1nExIS5LaVl5ezQYMGMT6fz5KTk6Xba3Pvkpz31dnY2DAnJydWWFgo93p18xIJjZJIZ2dn6Umr7GfmzJnM1NSU9e/fv+qNAMbn81lSUpLc/t588025C5+qJPL+/fs1/2IqkkhVfHx8mIeHh/RxXl4eA8D8/f1lPiCKqEoi169fX+tYJFQlkWZmZjInKmNVJ55AIGBdu3ZVa//qJpGKTnJ3d3dmY2MjfVxZWcmsra2Zl5eXwuN16NAhBoD99NNPNcbVv39/BoBdvnxZ7rmvv/6aAWBffPGFdJvkOL2YVDHGWH5+PrOysmJCoVB6kZV8EI8dO6YyjuvXr8vdGKqT7Of27dvSbZJjVv3iVN3EiRMZAHbt2jWZ7ePHj1f6RUmRESNGMENDQ5mLZm2PgyTeFy/sHTt2ZGZmZnIXdcaeXzw/+OADteJsCJIbkSRpe/r0KeM4jq1YsYIFBASwHj16SMu+8cYbDABLSUmRbqtLEhkQEKD0uYULF9b6d6msrGSjRo1iANiKFStknnv11VcZAJmbioTkS++YMWOk2yTnw6hRo+TKl5aWMmNjY9asWTOFN5RBgwYxAOzMmTM1xqxuEnn69Gmlz4lEIum22p5/kuvY+++/r/L9IyMjZbZXVlYyAwMDhcc0JSWFAWBTpkyRbjtz5gwDwIYMGSL3Hvn5+czGxoYZGxtLv/hGRkYyAGzEiBFy5ZOSkhifz5e7yR88eJABYP/73/9kti9YsIABYEePHpXZrq17wbVr1xgAtmzZMpntkuuZsuNjbm4ud/5IjtOSJUvk9vXi56lt27aMz+ezBw8eqBWnIpqeL9UrjSQkf5cOHTrIbFeWRE6cOLHG+Gq6zyqzb98+BoBt3rxZuk3dexdjypNIDw8PVlJSUqtYFNGoHVXSjB0UFISwsDAAVSMb4+LikJWVBR6vqqtlamoqYmJipK9zc3OTaSaW6NOnD/744w/cuHEDY8aMUfq+EyZMwP79++Hn54dJkyZhwIAB6NOnj0YDcCIiIrB27VpcvnwZz549Q0VFhfQ5Q0ND6f+FQiGGDh2Kf//9F127dsW4ceMQGBgIX19fldXcEiNGjMCiRYswb948nDp1CsHBwejbty9atGhR65gVadWqFczNzWW2SZpNc3NztfIeQFW1e/X+YxIuLi64ePGi9PH9+/eRk5MDJycnaR+Q6p4+fQoAiI2NrfE9b9y4AVNTU3Tv3l3uuX79+gGoqtZ/UZ8+feS2mZubo3PnzoiIiEBCQgJatmyJ8ePHY+3atRg1ahRCQkIwaNAgBAQEwNnZWea1ly5dAgA8efJEYV84ye8SGxsr02RkbGwMHx8fhb/b5MmTsWPHDmzbtg1du3YFAIhEIhw+fBg+Pj5yXSRu3ryJVatW4dy5c8jIyJDrt/Ts2TO50fvqHgdFioqKcPv2bTg5OWHlypVyz0veX52/ozYG33Xu3LnG7i6ScyI8PBwTJkxAREQEGGPo378/SkpK8PXXXyM/Px8WFhYIDw+Hl5cXXF1d6xwbAHTr1k1um4uLCwBo9Dn85JNP8M8//2DatGn49NNPZZ67dOkSzMzMlPZLNTExUfh3UfQ5io2NRUlJCfr166dw1Gm/fv1w4sQJ3Lx5U+H5pImajpWFhUWdzj9Fv2d1Lza183g82Nvbo6ioSK7JVPKZSktLk26T9ENW1OQn6cd2/Phx3L9/Hz4+PtJ+84qOn7u7O1xdXeX6zg4bNgzOzs7YtGkTli1bBj6fj7KyMmzbtg2urq4IDg6W21dt7gVlZWX4v//7P+zcuROxsbEoKCiQ6Z9f/feVsLa2Vnp8vL295c4fRcdOkYKCAsTExKBly5bw9vZWWVaZupwvqv4ud+/eRVlZmUxOUF3btm3RsWNH7NixA48ePcJrr72GwMBAdO7cWZoHqSs/Px/fffcdDhw4gPj4eBQWFso8X/04qnvvUmbChAn45Zdf0KFDB0yYMAH9+vVDz5495brNqEOjJLJ6wgVU9f2Kjo7GwIEDZQ6cnZ0dIiMjpY+bN2+ucH+S7Xl5eSrfd9y4cThw4AC+//57/Pbbb/j555/BcRz69euHNWvWqOyHU92ePXsQEhICc3NzBAUFwcPDA6amptLO5y8OeNizZw+++eYbbN++XdrXQCgUYvr06fjmm29UDvn38PDApUuXsGzZMvz777/SztJt2rTBF198gXHjxqkVszJCoVDhdoFAINfBvi6UjQ4XCAQyA6iys7MBAHfv3sXdu3eV7u/FD4giIpFI6U1ecoGS9OuqTt3zrEePHoiIiJD+bTdt2gQA8PX1xcqVK6VJieR3Onr0KI4ePar272Rvb690ktfBgwejefPm2LlzJ7777jvw+Xzs3bsXxcXFmDx5skzZCxcuoH///tLXeXt7w9zcHBzH4cCBA4iOjkZpaanGx0GRnJwcMMbw+PFjhV8GlP3Oiqh6vbqmTp1aYxLZvn172NvbS/tBhoeHQygUolu3biguLsby5ctx9uxZeHt74/Hjx5g5c2ad45JQ9DmU9HWu7edw48aN+O677xAYGKiwU312djYqKipq/XdRdD5IPj/KzhVVnzNNqXOs6nL+Kftdanp/VXFV/9JW22Mm+ZzZ29srLN+8eXO5JJLP52PmzJlYvnw5QkNDMXz4cPzzzz/IysrCO++8ozBBqc29YOzYsTh8+DBatWqFkJAQ2Nvbw8DAALm5uVi3bp3C64mq46PusVNEcnzUTYAUqY/zRfJ3yc/PR7NmzRSWEQgEOH36NJYtW4Z9+/bhgw8+AFCV+7zzzjv47LPPauwPClQl9YGBgbh+/Tq6dOmCyZMno1mzZtJ5ubds2SLzN1H33qXMunXr4OnpiU2bNuGrr77CV199BWNjY4wfPx5r1qypVcWcRkmkvb09Hj16JH0cGRmJ8vJy9OrVS6acSCSSST6UjQqVbFdnGpuRI0di5MiRyM/Px/nz57F//3788ccfCA4ORmxsrFrrcy9btgzGxsa4du2a3DefnTt3ypU3NTWVHujExESEh4fjt99+w7p161BcXIz169erfL8OHTpg7969KC8vx7Vr1xAaGooff/xR2sHa39+/xpgbC8nFZMyYMUpHbtdmX5mZmQqfk6zPrujiVZvzrE+fPggNDUVxcTEuX76Mw4cP45dffsGwYcNw584dtGjRQvoe6g4IklC1SgCfz8fEiROxdu1anDx5EkFBQdi2bRt4PB4mTZokU/brr79GaWkpzp49KzfA4dKlSzIzBCj6fZVtV/V5k/zO3bp1w9WrV5WWU0f1Go76FhgYiN27d+Px48eIiIhAQEAA+Hw+/Pz8YGJigvDwcDx+/BgAarzQ6sKpU6cwd+5ctGrVCvv27VPY2iEUCsFxHJ49e1arfSs6HyV/Z2XniqrPWX2qy/lX36u/1PaYST5nyq5lyvYzc+ZMfPXVV/j9998xfPhwbNy4ETweDzNmzKhT/FFRUTh8+DCCgoJw9OhRmSTn0qVLWLduXZ32X1uS4yP5XGqiLueLquskx3GwsLBQ+fpmzZrhp59+wo8//ojY2FicPn0aP/30E5YuXQoDAwMsWrSoxhgOHjyI69ev480338TGjRtlntu5cye2bNki9xp17l3KCAQCfPjhh/jwww+RlpaGyMhIbNq0CVu3bkVGRgaOHTtWY8wSGk3x4+fnBwBISEhAdHQ0li5dCo7j5KrY79y5I22mAICUlBSF05qcPXsWANClSxe1Y7CwsEBwcDA2bNiAadOm4cmTJ7h8+bL0eR6Pp7QGID4+Hm3btpVLINPT05GQkKDyfT09PTFjxgxERkbC3Ny8VlPWGBgYwM/PD8uXL8ePP/4IxhiOHDmi9uvri+Qioo2ay7Zt20IoFOLq1as1fgOtSZcuXVBUVKRwig3JNASKap8l51N1BQUFuHnzJoRCocIPl4mJCQIDA7FmzRosXrwYxcXF0il2JKOuqzfba4OkxvGvv/5CamoqIiMj0a9fP7lv5PHx8bCxsZFLIIuKiqTTayiiyXGQsLCwQNu2bRETE6PVbhH1TZIY7tixA/fu3ZPW4BoZGaFXr144ffq0tKZS3RGIfD5fq7X6ysTExGDs2LEQCoU4cuQIbGxsFJbr0aMHsrKyFE6fUltt2rSBsbExoqKiFE5IrOpz9iJtXkf0+fyT3KcUTYVSWFiIq1evwsTEBK1btwYAadcURZ/H5ORkhdP8AFVN/MOGDcO///6LCxcu4NSpUwgKCqrzvH+SaeyGDRsmV0umKMb6Zm5ujnbt2iExMVHjc7ou54uqv0v79u2VNmW/iOM4tG3bFvPmzZPeO6rnB6o+H5K/yciRI9WKrzpV9y51ODk5YeLEiQgLC0PLli1x8uRJFBcXq/16jZLIefPmAaia/7Br1664fPkyBg4cKNPX5cGDB0hMTJQmnEDVwVu8eLFMzcStW7ewbds22NnZYejQoSrf98yZMwr/AJJveNWn0bCxsZGpLa3O3d0dDx8+lPkGUlJSgrlz58olPk+fPlU4V1pOTg5KS0trnLrj2rVrCpuCJO9d0+sbguRmpexiVhsCgQBz585FcnIyPvzwQ4WJ5J07d5R+K69u6tSpAKqmI6i+n9TUVHz//fcQCAQKV0E6efKk3Depr7/+Grm5uZgyZYq0KejixYsoKSmRe/2Lf5vu3bujR48e2LFjB3bt2iVXXiwWy3TbUFfXrl3Rrl07/PPPP1i/fj0YY3JN2UDV+ZqTkyPTPaCyshIffvihtI+pIuoeB2Xmz5+PoqIizJo1S2EzUGJiotrzIDYUSRK5atUqAJAmkZLnbt68iePHj6NVq1ZwcnJSa582NjZ49uyZwnNFW54+fYrhw4ejqKgI+/fvV9k3bP78+QCAGTNmKJyTMCMjQ6YvuiqGhoaYOHEinj17JjPvIACEhYXh2LFjaNmypVqtJdq8jgD6e/75+/vDy8sLoaGhcnMcf/XVV8jKysLEiROlyUfv3r3h6emJI0eO4Ny5c9KyjDEsXrxYZdI9e/ZsVFRUYNy4cWCMyUyBpCnJuITqsQBVXZBePAcayrx581BZWYm3335bLoEpKSmRdilSRdPzZevWrbh165b0cfW/S/UpAhVJSkpSuE9F93dVnw9lf5PIyEj8/vvvcuXVvXcpUlpaigsXLshtLywsREFBAQwMDGrVn1Oj5mzJHEcWFhZwcHCAs7MzXnnlFZkO9FFRUfDx8cGwYcOk2zp27Ihz587B19cXAwcOlM7DVVFRoXAutBfNnz8faWlp6N27Nzw8PMBxHM6dO4crV67Az89Ppqamf//+2L17N1577TV06dIFfD4fr776Kjp27Ih3330X7777Lrp06YKxY8eioqICJ06cAGMMnTp1kmkefPz4Mbp06YJOnTqhY8eOcHZ2RlZWFg4ePIjy8nJ8+OGHKmPetm0b1q9fj4CAAHh5eUEoFOLevXv4999/YWNjo3KeuoYimfx08eLFuHv3LiwtLWFlZVWrptvqli9fjuvXr+PHH3/E0aNHERAQAHt7ezx+/Bi3b99GdHQ0Ll68qLSPkMTkyZOxf/9+HDx4EB07dsTw4cOl80RmZ2djzZo1CmvThg8fjhEjRkjnb7t06ZJ0IMUXX3whLbdy5UqEh4cjICAAnp6eMDY2xvXr13Hq1Cm0aNECo0aNkpbdsWMH+vXrhwkTJmDt2rXo2rUrTExMkJKSgosXL+Lp06caJRmTJ0+WztllamqqcGDZu+++i+PHj6N3794YP348jI2NERERgcePHyMwMFDp5LDqHgdlZs+ejUuXLmHLli04f/48Bg4cCCcnJzx58gSxsbG4fPkytm/frlfrSrdu3RqOjo5IT09Hs2bN0LFjR+lz/fr1g1gsRlZWFsaOHav2Pvv374+rV69iyJAh6NOnDwwNDREQEICAgACtxb106VIkJCTglVdeQXh4uNx8l8DzAUrBwcH4/PPP8eWXX6Jly5YIDg6Gu7s7srKy8PDhQ5w9exZfffUV2rZtq9Z7r1y5EpGRkfjqq69w4cIF9OjRA0lJSdizZw9MTU2xadMmtW4qko75a9euRU5ODuzs7ABUTZ6tCX09/3g8HjZv3oygoCAMHToU48aNg7u7Oy5evIiIiAh4eXnh22+/lSm/YcMGDB06FAMHDpR2Yzp9+jTS09PRsWNHmSSmOsnfNjk5GQ4ODhgxYkSd4+/evTu6d++O3bt3Iz09HX5+fkhJScGhQ4cwbNiwOndD0sTcuXMRGRmJ3bt3w9vbG6+++iqEQiFSUlJw7Ngx/PHHHzX2idb0fAkKCkLPnj0xYcIE2NnZ4dSpU7h69Sr8/Pzw7rvvqnzPmzdvYvTo0ejevTvatWsHBwcHPH78GAcOHACPx5OZx1jVfXbEiBHw8PDAqlWrcOfOHXTo0AH379/HkSNHMGrUKLm/SW3uXS8qLi6Gv78/WrVqhW7dusHNzQ0FBQU4cuQIMjIy8OGHH8LIyEjl7y1DkyHdiYmJNU7xA0BmDkX8NzQ+NTWVhYSESKdB6NmzJzt+/LjceygaDr9z5042fvx45uXlxUxNTZmlpSXr1KkTW7lypdzUBunp6Wz8+PHS+SpRbeoJsVjMfvvtN9a+fXtmbGzMHBwc2JtvvskyMzPlhsPn5OSwZcuWsYCAAObo6MgMDQ2Zk5MTCw4OZqGhoTLvqWiKkEuXLrHZs2ezDh06MCsrK2ZiYsK8vb3ZO++8o3CKDkVUTfGjbBojVdOSKLJ582bm4+PDjIyMGACZ16ral6LpAxirmkNz/fr1zN/fnwmFQmZkZMTc3NxYcHAw+/XXX2XmaVOlvLycfffdd9LYLCwsWN++fdnBgwflylY/TgcOHGC+vr7MxMSENWvWjE2bNo2lp6fLlA8LC2NTpkxhrVu3ZhYWFszc3Jy1a9eOLV68mD19+lRu/9nZ2ex///sf69ChAzMxMWHm5ubM29ubTZo0ie3fv1+mrLrHPyUlRXp+qpomYu/evaxr167M1NSU2drasvHjx7P4+Hg2depUBkBmPrnaHoea4t21axcbOHAgs7a2ZgYGBszZ2ZkFBgayNWvWKDxOuiaZ4636NDeMMVZWVsbMzc1lpgGqTtkUP/n5+WzWrFnM0dFROh2LpIyy1zD2/Do5derUGmOW/B1V/bzoxIkTbMSIEczOzo4ZGBgwBwcH1rNnT/bll1/KTF1U09Q7jFVNhzR//nzm7u7ODAwMmK2tLRs7dqzMtFXqOHr0qPR8ezFuZdeK6r//i/MiMqb++VfTFCqq3l/V+a/sOnvr1i02duxYZmtrywwMDJi7uzt77733lH4mzpw5wwICApiJiQmzsbFh48aNY8nJySrjYoyx//3vfwwA+/TTT5WWqe29IDMzk82YMYM5OTkxY2Nj5uPjw37++WeWkJCg8JzV5PgoO/+V7UssFrONGzcyPz8/ZmZmxkxNTZm3tzebM2eOzPlcE03Ol99//521b9+eGRkZMUdHR/bee+/JTDel7HdNTU1ln376KfPz82P29vbM0NCQubm5sdGjR7OLFy/KvV7VfTYhIYGNGTOG2dnZMVNTU+br68t27typ8BpTm3vXi+dXWVkZW7lyJRs8eDBzcXFhhoaGrHnz5iwgIIBt3769xqkMX8T9d2DqHcdx6Nu3b62X1CGEEEJeVsOHD8e///6LBw8eKJ2Si9TesmXLsHz5coSHh9duhRYiQ/P19lA1dH/fvn2IiIjAo0ePwBiDi4sLAgMDMWbMGLU7pBJCCCFElqTr06BBgyiBJHpJ4yTy8uXLmDhxIpKTk+Wm8Ni4cSMWL16M7du3o2fPnnUOkhBCCHlZbN++Hffv38fWrVsBQOG66oToA42SyPv372PQoEEoKCiAs7MzQkJCpKuZJCUlYdeuXUhOTkZQUBCuXLmCNm3aaDVoQgghpKnasGEDzp49C3d3d/zxxx9yczAToi806hMZEhKCPXv2YP78+Vi1apVcs3V5eTk+/vhjrFu3DuPGjVM4LQohhBBCCGm8NEoi7ezsYGVlhQcPHihdHYAxhlatWiE3N1flXHaEEEIIIaTx0Wiy8cLCQnTv3l3l8lIcx8HX11ettXUJIYQQQkjjolGfyBYtWqi1bmtWVpa0ryR5TiwWIy0tDRYWFvW+zishhBDysmCMIT8/H05OTrVaeYVoRqMkcsqUKViyZAnu3r2L9u3bKyxz9+5dREREYPny5XUKsClKS0uDq6urrsMghBBCmqTU1FS4uLjoOowmT6M+kZWVlQgJCcHZs2fx+eefY8qUKRAKhQAAkUiEbdu24csvv0Tv3r2xe/du+jbwgry8PFhZWSE1NVV63AghhBBSNyKRCK6ursjNzYWlpaWuw2ny1EoiFa1PDFRN5yNpjrWysgIA5ObmSp93c3MDj8dDfHx83SNtQkQiESwtLZGXl0dJJCGEEKIldH9tWGo1ZyclJSl9TpKD5uTkyD2XnJxMff4IIYQQQpogtZLIxMTE+o6DEEIIIYQ0Imolke7u7vUdByGEEEIIaURoxAshhBBCCKk1jab4qe7SpUsIDw/H48ePAQDOzs7o168f/Pz86hwcIYQQQgjRTxonkSkpKXj99ddx4cIFAM8H2EgG0vj7++Ovv/6Cm5ubFsIkhBBCCCH6RKMkMjc3F/369UNiYiKMjY0RFBQELy8vAEBCQgLCwsJw7tw5DBgwAFevXqW5mgghhBBCmhiNksg1a9YgMTERQ4cOxYYNG+Dk5CTzfEZGBmbNmoV///0Xa9aswRdffKGVYAkhhBBCiH7QaMWaDh064OnTp0hMTISpqanCMkVFRfD09ISdnR3u3LlT50CbEpoMlRBCCNE+ur82LI1qIhMTEzFs2DClCSQAmJqaom/fvjh69KjGwRFCSGOWn1OOgtwKpc+bWwlgYW3QgBERQoj2aJRE8vl8lJeX11iuoqKC1s0mhLy0rp3OQeT+Z0qf7zvaFoFj7BswIkII0R6Nkkhvb29EREQgNzdXumb2i7KzsxEeHo5WrVrVJT5CCGm0uvW3RuuuFigvY9j0RRIAYPoSDxgYVs1iYW5V51nWCCFEZzSqJhw3bhzy8vIwbNgw3L17V+7527dvY/jw4RCJRAgJCalzkIQQ0hhZWBvA0dMEDu7G0m0O7sZw9DSBo6cJNWUTQho1jb4Gv/fee9i1axcuXryITp06oUuXLvD09ARQNcXPzZs3IRaL0blzZ8yfP1+rARNCCCGEEN3TKIk0MTHB6dOnMXfuXOzduxfXrl3DtWvXpM/zeDyEhITg559/hrGxsYo9EUIIqQkN0CGE6CONO+RYW1tj586dSE1NxZkzZ2SWPQwICICrq6vWgiSEkJcZDdAhhOijOvfqdnV1xeuvv652+YMHDyI6OhpLliyp61sTQshLgQboEEL0UYPPv3PgwAEsX768od+WEEIaLRqgQwjRRzSJIyGE1DOx+PnCYMmxhTKPCSGksaI2EEIIqUcxUSKEbsmQPt6+OhVCGwGCpzigrS8ty0YIabyoJpIQQupJTJQIu9c+Qn6O7MhqUXYFdq99hJgokY4iI4SQuqMkkhBC6oFYzBC2NUNlmbCtGdS0TQhptCiJJISQepASWwRRtvK5HYGqGsmU2KIGiogQQrSLkkhCCKkH+SomB68uK6NM7X3SAB1CiD6hgTWEEFIPLNScu/HIH+m4Hp6DFj7m8PIxg6u3KfgCTq4cDdAhhOgbSiIJIaQeuLUxhbmVQOVyhTw+IK4E0hJKkJZQgnMHn8HAiINHWzO06GAGLx9z2DobIvZqPnavfST3eskAnfELXCiRJIQ0OEoiCSGkHjAxYGTCQ0Gu8jJj33WBs5cJEu4UVv3cLkChqBJxNwsQd7MAwBOYW/FRWiRW+V5hWzPQupsFeDz5GkxCCKkvDZ5EMsbAGPXjIYQ0bSd3PkFWehkEhhyMTHgozKuUPvdiM3TnACt0DrACEzM8SS1Fwp0CxN8qRMr9IhTkVip7CynJAB2Pdmb19vsQQsiLOEYZXYMTiUSwtLREXl4ehEJqgiKkqZHMDwkAIe+7wqOdKVbOug8AmPSRK7w6mqtVa1hRJkbkP09x7lBWjWVHz3OGTy/LugVOSCNH99eGVafR2ffu3cOcOXPQpk0bmJubw8zMDK1bt8acOXNw584dbcVICCGNRnZGGQ6uTwMA9BzWDG1ekW1mdm9jpnazs8CQBy8fc7XKqjuQhxBCtEXjJPLnn39Gly5d8Pvvv+PBgwcoKipCcXEx4uLisGHDBnTr1g0//vijNmMlhBC9Vl4mxp4fH6G0WAy31iYYMN6+zvt0a2MKoY3qBFFoI4BbG9M6vxchhNSGRklkaGgo3n33XVRUVGD06NE4dOgQbt++jdu3b+Pw4cMYO3YsKisr8f777yM0NFTbMRNCiF4K25qBjOQSmAr5GPOOi8KpemqLx+MQPMVBZZngKQ40qIYQ0uA0av9YtWoVOI7Dzp07MW7cOJnn2rdvj2HDhmHv3r0YP348Vq1ahSFDhmglWEIIaQj5OeUqp+YxtxLAwtpAZlv02VxcD88FOGDMPGcIbQwUv1gDbX2FGL/ABaFbMuTW4TYy5cGtFdVCEkIankZJ5LVr19C9e3e5BLK6sWPHokePHrh27ZrGwRFCiC5cO52DyP3PlD7fd7QtAsc8b6rOfFSCo5vSq54bZYcWHdTrx1gbbX2F8GxvJh2gM/59F4TvzsTTx2U4uCENEz90BcdRbSQhpOFolERyHAcvL68ay3l5eeHevXuavAUhhOhMt/7WaN3VAuVlDJu+SAIATF/iAQPDqiTNvNoglrISMfase4TyUoYWHcwQMMq23uKq3mTt1cEcNvaG+H1JIuJuFiDqRA66D7apt/cmhJAXadQnsmPHjoiLi6uxXFxcHHx8fDR5C0II0RkLawM4eprAwd1Yus3B3RiOniZw9DSRNmUzxnB4YxqepZXBwlqA0fOcG7RvYnM3YwyaUFUjenz7E2SmljTYexNCiEZJ5MKFCxEVFYWdO3cqLbNr1y5ERUXh/fff1zg4QgjRZ1dP5eDORRE4XtXqM2ZC2cad/JxypCcWIyP5eXKXkVyC9MRipCcWIz+nvM4xdA+yQctO5qgsZ9j3f49RUaZ6dRtCCNEWjZqzu3Xrhvfffx9vvPEG9u7diylTpsDT0xMAkJiYiG3btuGff/7B+++/D19fX6SkpMi83s3Nre6RE0KIDqUlFOPYticAgIETmsOttfzgFkV9KyXN44B830pNcByHkbOd8Nun8ch8VIoTOzMxpIbR3IQQog0aJZGShJExhn/++Qf//POPXBnGGNauXYu1a9fKbOc4DhUVykc9EkKIvisurMSeHx+hsoKhzSsW6DlUcV9ESd9KZcy1NEG4uaUAI2c7YfvqVFw5lo2WHc3g3Vn5+xJCiDZodAVzdaVRgISQlxNjDAd/e4zcp+WwtjfAyLeclF4PLawN5KYCqi/enS3QPcgGV45l4+D6NMz51gvmlvLN67WduogQQpTRKIlMSkrSchiEENI4XDiahfvXC8A34DBuvguMzfi6Dklq0AR7JN0rRGZqKQ6uT8Okj2S/8Nd26iJCCFGFFlslhBA1JccW4tSuTABA8GQHOHqaNMj7SmoQy8uYdFtGconMlEMW1gYQGPIwZp4zNnyeiIfRBbhyLBs9gptJX1ObqYsIIaQmdMUghBAlxOLnSdv96yIc//sJmBjw8bdEt/5WDRZHbQbo2LsaY/Ck5gjdkoETOzPh0c4Mzd2qpiqSNK+XlTwfwe3gbgxDY40m6iCEvOTqlESePXsWP/30Ey5cuICnT5/ijTfewB9//AEAOHHiBMLDwzF//nw4ONBIQUJI4xITJULolgzp4/0/pwEALKwFGD7DsUH7hdd2gI7vIGvE3yrAgxsF2PfzY8z60hMGhpQoEkK0S+OryldffYXAwEDs3bsXaWlpKC8vB2PPv7VbWlpi5cqV2L9/v1YCJYSQhhITJcLutY/k1qkGgPycCsTfLmjQeCSTnyv7eXEwDMdxeHWWE8ws+Xj6qBQntj9p0HgJIS8HjZLI0NBQLFmyBM7Ozti9ezeePJG/QHXv3h12dnY4cuRInYMkhJCGIhYzhG3NUFkmbGuGTFO3PjKzFOC1Oc4AgKgTOXhwPV/HERFCmhqNksh169bByMgIoaGhGDt2LOzs7BSW69Spk1rLIxJCiL5IiS2CKFv1XLai7AqkxBY1UESaa9nRHD2Cq+awPLghTeX0PoQQUlsaJZFRUVHo3r072rdvr7KcnZ0dMjJUf6Ovb1FRURg6dCisrKxgZmYGPz8/7N69W+P95eTkwNnZGRzHITg4WIuREkL0Qb6aiZa65XRt4AR7NHczQlF+JQ6sfwym5zWohJDGQ6MksrCwUK3BMnl5eRCLdbeOa3h4OPz9/XHu3DmMHz8ec+bMQUZGBkJCQrBmzRqN9vnOO+8gLy9Py5ESQvSFhZrT3KhbTtcEBjyMmecCgQGH+FuFuHwsW6YpPjm2UO+b5gkh+kmjJLJ58+Z4+PBhjeXu378PV1dXTd6izioqKjBr1izweDycOXMGGzZswJo1axAdHY1WrVph8eLFSE5OrtU+9+3bh+3bt2PlypX1FDUhRNfc2phCaKM6QRTaCODWRn6tbH1l52KEwW80BwCc2P4EP33w/Pq9fXUq1r0Xh5goka7CI4Q0Uholkb1798bNmzdx/vx5pWWOHDmChw8fol+/fhoHVxenT59GfHw8Jk2ahM6dO0u3W1paYvHixSgrK8OWLVvU3t/Tp08xd+5cTJ48GcOGDauHiAkh+oDH4xA8RXVLS/AUB/B4jWvp11cGWMPJ0xhiMVAkqpR5TpRdgd1rH1EiSQipFY2SyA8++AAcx2H06NE4cOAAKipk+waFhYVh5syZMDAwwLvvvquVQGsrIiICADB48GC554KCggAAkZGRau9vzpw54PP5WLdunVbiI4Tor7a+QjR3M5LbLrQRYPwCF7T1FeogqrphDBApmLKousYw6pwQoj806tTTtWtXrFmzBgsXLsSYMWNgYmICjuOwb98+7N+/H/n5+WCM4ccff0S7du20HbNaJKPCvb295Z5zcHCAubm52iPH//rrL+zfvx8HDhyAtbV1rftElpaWorS0VPpYJKJv+4Tos6L8CmQ+KpXZNukjV3h1NG90NZASKbFFNY7Olow692hn1kBREUIaM40nG3/vvffw77//wtfXF8XFxWCMIT8/HyKRCD4+Pjh06BDeeecdbcZaK5JEz9LSUuHzQqFQrWQwLS0N8+fPx8SJEzFy5EiNYlmxYgUsLS2lP7rqJ0oIUU9MVD6YGHBwf14b6d7GrNEmkEDTG3VOCNG9Og0vDAoKQlBQELKyspCYmAixWAxXV1c4OjpqKz6dkzTL//jjjxrvY9GiRVi4cKH0sUgkokSSED1291LVF8w2rwiRkfxUx9FoR1MbdU4I0T2tXC2aNWuGZs2aaWNXWiOpgVRW2ygSiWBtba1yH1u2bEFoaCj27NkDW1tbjWMxMjKCkZF8/ypCiP4pyKtA0r2qicTb+logYl/TSCIlo85VTaTe2EadE0J0S+PmbH0n6QupqN9jRkYGCgoKFPaXrO7GjRsAgHHjxoHjOOmPp6cnAODYsWPgOE5m9DchpHGLuSICY4CzlzGs7Ax1HY7WNNVR54QQ3VGrJvKHH37AvHnzYGio+QW1rKwM//d//yfTrFuf+vbtixUrVuD48eOYMGGCzHPHjh2TllGlZ8+eKCgokNteUFCAXbt2wcXFBUFBQXBzc9Ne4IQQnbp7qWrgW3s/xf2pG7O2vkKMX+CC0C0ZyH9hpPbI2Y6NctQ5IUR3OMZYjfM58Hg8uLu749NPP8WkSZNgYWGh9hvk5eXhr7/+wqpVq/Do0SNUVlbW/CItqKioQOvWrfH48WNcunRJWluYl5eH7t27IykpCffv34eHhwcAID09HXl5eXB0dFQ6GEciKSkJnp6eCAoKQlhYWK1jE4lEsLS0RF5eHoRCumgToi/yc8rx/btxAAMW/OgNEzM+VrwZCwBY9EcbGBo3jcabkqJKrJx1HwBgZWeA3Kfl6DmsGQZPaq7jyAipG7q/Niy1roj//PMPeDwe5s6dCwcHB7zxxhvYtGkTYmNj8WIOyhhDTEwM/vzzT0ycOBFOTk6YP38+DAwM8M8//9TLL6GIQCDAxo0bIRaLERAQgLfeegsffPABOnXqhAcPHuCbb76RJpBA1eCXtm3bNmiMhBD9cveyCGCAq7cJLJsZ6DqcelO9yXrw61WJ45Xj2RBllesqJEJII6RWc/bIkSMxZMgQ/Pjjj/jpp5+wfft27NixA0BVLaWlpSWEQiFEIhFyc3OliSVjDG5ubnj33Xfx7rvv1qk5XBP9+vXDuXPnsHTpUuzatQvl5eXw8fHBypUrERIS0qCxEEL0373/mrK9OpkjPbEY5WXPvyRnJJfAwLAq+TK3EsDCumkkmS06mMG9jSmSY4sQsf8pXp3lpOuQCCGNhFrN2dWJxWIcPHgQBw4cQEREBFJTU+XKuLq6ol+/fnjttdfw6quvgsdrGk1A2kLV7YTon7yscqydHwdwgF+wNS6F5igt23e0LQLH2DdgdNpVViKWaaZ/klqCP5clgeOAt1d5wdaJZpMgjRPdXxtWraf44fF4GDVqFEaNGgUAyMrKwpMnT5CXlwcrKyvY29vr3XQ/hBBSE8mAGvfWpug1zBYd/a2UljVvYnMpunqbonU3C9y/lo/wPZkY9x7NY0sIqVmdr4T6OEckIYTUlmSC8fZ+QlhYGzSZ5mp19R9vh/vX83HvSj4exxfD2ctE1yERQvQctTMTQl56OZllSEsoAccBbbu/nE1g9i7G6NS7amaK07szdRwNIaQxoCSSEPLSu3u5qinbo50pzC2bVlN1bQSOsQNfwCHhTiES7sjPkUsIIdVREkkIeek15QnGq8vPKUd6YjEykkuk2zKSS5CeWIz0xGLwBRxeGVi1HOypXZlyU7gRQkh1L+9XbkIIAZCVUYqMpBJwvKq1spuya6dzELn/mcy2TV8kSf/fd7Qt+rxqixsRuUhLKEFMVD7avaTN+4SQmlESSQh5qUlqIVt0MIOpRdO+JHbrb43WXZUnyuZWAphZCtBzqA0i9z/D6d2ZaNPNAjw+radNCJHXtK+YhBBSA2lTdo+mX+Om7qjznkOaIepEDrLSy3DzbC66Blo3QHSEkMaG+kQSQl5aTx+XIjO1FDw+0Ma36SeR6jIy5aPPSFsAQOS+pygvE+s4IkKIPqIkkhDy0pLUQnr5mMPEjK/jaPTLKwOsYdnMAKLsCkSdUL56DyHk5UVJJCHkpcQYk5lgnMgSGPIQOMYOAHDu0DOUFFXqOCJCiL6hPpGEkJdSZmopnqWVgS/g0Lpb0x6VramOfSxx4WgWnj4uxYUjWeg/vmq98PycchTkVih9nbmV4KVb8YeQlxElkYSQl5KkKbtlJ3MYm1JTtiI8Hof+4+2w64dHuBSWhe6DbWBuJVA4VVB1fUfbInCMfQNGSgjRBUoiCSEvHcaYdJUaaspWrXU3Czh7meBxfDHOHHiKodMcpVMFlZcx6TyT05d4wMCwaiogcyu6tRDyMqA+kYSQl05GcgmyM8ogMOBUzptIAI7jMHBCVa3itdM5yMksg4W1ARw9TeDgbiwt5+BuDEdPEzh6mlBTNiEvCUoiCSEvHUlTtndncxga02WwJh7tzODV0QziSiB871Ndh0MI0RN09SSEvFSqRmX/15Tds2mvla1NA/4bVHP7Qp7M2tuEkJeX1pPIyMhIrFu3DgcOHIBYTBPUEkL0S1pCCXKflsPAiEOrzua6DqfRcPQ0qeo/yoDTuzN1HQ4hRA9olERu3rwZXbt2xblz52S2v/vuu+jfvz8WLlyIMWPGIDg4GJWVNLcYIUR/SOaGbN3VAgZG1BhTG/3G2YHjAXE3C5AcW6jrcAghOqbRFXTv3r2Ij4+Hr6+vdNvVq1fx888/w9jYGCNHjoSzszNOnTqFnTt3ai1YQgipCyZ+Piq73UuwVra2NXMwkq6jfWpXJhhjOo6IEKJLGiWRd+7cgY+PD4yMjKTbdu7cCY7jsG3bNuzfvx9XrlyBsbEx/vzzT60FSwghdfHoYTFEWRUwNObBuxM1ZWui72hbCAw4pD4oRtzNAun25NhCiMWUVBLyMtEoiczKyoKLi4vMtjNnzkAoFOK1114DADg4OKBPnz54+PBhnYMkhBBtkNRCtulmAYEhNWVrwsLaAD2CbQAA//zyWLp9++pUrHsvDjFRIl2FRghpYBpdRcvLy2X6OpaWliI6Ohq9evUCj/d8l3Z2dsjMpA7YhBDdY2KGe5KmbJpgvE7snKtaoV4cOynKrsDutY8okSTkJaHRsgJOTk64e/eu9HFkZCTKy8vRq1cvmXIikQiWljSFBiGkYaha0zk9sRj5ORUwMuXBy8esgSNrOsRiVuPo7LCtGWjdzQI8HtdAURFCdEGjJDIwMBBbtmzBt99+iyFDhmDp0qXgOA7BwcEy5e7cuSPX7E0IIfWlpjWdAaDtKxYQGFBTtqZSYosgylacqEuIsiuQElsEj3aUrBPSlGl0JV28eDHMzc3x2WefoWvXrrh8+TIGDhyIbt26Scs8ePAAiYmJ8PPz01qwhBCiSrf+1njrK09MX+Ih3TZ9iQdmfuEBE7Oqy117P2odqYt8JTW9mpYjhDReGtVEtmzZEhcuXMCaNWuQmZmJ7t2746OPPpIpc+rUKXTq1AnDhg3TSqCEEFITC2sDWFgboKzkeWc9B3djPI4vRnGhGCbmfHi2p9qxurCwUu+2oW45QkjjpfGnvH379iqn75k7dy7mzp2r6e4JIURr7vw3wXibVyzAF1A/vbpwa2MKoY1AZZO20EYAtzamDRgVIUQXqGMQIaRJE1cyxFzJBwB06EmjsuuKx+MQPMVBZZngKQ40qIaQlwAlkYSQJi0ppgjFBZUwFfLh0ZaasrWhra8Q4xe4wMJavjFrwAR7tPWlZJ2Ql4FazdktWrQAx3E4efIkPD090aJFC7XfgOM4xMfHaxwgIYTUhWTOwra+QvD4VDumLW19hfBsb4aVs+4DADzbmyLxbhHibuTDf3gzcBwda0KaOrWSyKSkJHAch/LyculjddGFhBCiSw+u/9eUTROMa131Juuh0xyx/rMEpNwvRuzVfKqNJOQloFYSmZiYCABwdnaWeUwIIfqupEgMcysa6FHfhDYG6Dm0Gc4eeIaTOzLRqgsNYiKkqVMriXR3d1f5mBBC9IlYzGQet/Wl1VMagv/wZrgenoPsJ2W4ejIbPYKb6TokQkg9ooE1hJAmJSZKhF8+lu2HffeyiNZzbgBGJnz0G2sPAIj85xlKCit1HBEhpD5pNYlkjGHLli14//33sXbtWhQWFmpz94QQolJMlAi71z5Cfo7sHIZFokrsXvuIEskG0KWvFeycjVBcUIkzB1UvQUkIadw0SiLXrFkDGxsbhIeHy2wfNWoUZsyYgR9//BEffPAB/P39UVxcrJVACSFEFbGYIWxrhsoyYVsz5Jq6iXbx+BwGTaqqjbxyLBs5mWU6jogQUl80SiJDQ0PB5/MREBAg3RYeHo5Dhw7Bzs4O7733Hjp27Ijbt29j8+bN2oqVEEKUSoktUrmKCgCIsiuQElvUQBG9vFp2MkeLDmaorGA4tStT1+EQQuqJRknkgwcP0L59e/D5fOm2vXv3guM47NixA99//z3OnDkDoVCIv//+W2vBEkKIMvm5qhPI2pYjyuXnlCM9sRgZySXSbRnJJUhPLEZ6YjEKciswaFJzgAPuXhLhURwl7oQ0RRqtnZ2VlYXevXvLbDt37hxsbW3Rr18/AICFhQX8/f1x48aNukdJCCE1sLBS73Kmbjmi3LXTOYjcL9vfcdMXSdL/9x1ti8Ax9ugcYIWbkbk4/vcTTF/qQfMGE9LEaHQ1FYvFKCl5/g20sLAQ9+7dw4gRI2TKWVtbIzs7u24REkKIGtzamEJoI1DZpC20ofkitaFbf2u07mqh9Hnz/xL1fmPtcPdSHlLjihETlY923WkCckKaEo2as93c3GRqGI8fP47Kykr4+/vLlMvJyYGNjU3dIiSEEDXweByCpzioLBM8xYHmi9QCC2sDOHqaKP2xsDYA8HwCcgA4ufMJKitoUBMhTYlGSWRwcDBSUlLw9ttv4+DBg1i0aBE4jsOwYcNkyt28eRNubm5aCZQQQmrS1leIfuPs5LYLbQQYv8CFluLTAf/htjC3EiDnSTmiTlDLFCFNiUbN2YsWLcLevXvx22+/Yf369WCM4Y033kCbNm2kZa5fv460tDSEhIRoLVhCCKmtSR+5wqujOdVA6oihMQ/9xtrh8MZ0RP7zFJ0CrGBixkd+TjkKVAxyMrcSSGs0CSH6SaMk0sHBAdevX8eGDRvw5MkTdO/eHZMnT5Ypc/fuXYwcORKjR4/WSqCEEKKO5BjZkcDubcwogdSxzn2tcDksG5mPSnHmn6cIesNB4eCc6iSDcwgh+otjjFEnlQYmEolgaWmJvLw8CIXUvEaItlRWMKx8Kxblpc8va4v+aANDY1rhVdce3irA3ytTwOMD73zXEgIDDgW5FSgvY9KR3dOXeMDAsCrhp5pIogm6vzYsurISQpqMtMRilJcymJjzay5MGlTLjubw8jGDuBI4uTNTOjjHwd1YWsbB3VhucA4hRH9pZcK0vLw8iEQiKKvUpME1hJCGkHSvqinbrZUJ7l8v0HE05EWDJjVH/OIE3LssQmpcEVy9abolQhozjWsic3Jy8O6778LBwQE2Njbw8PCAp6en3E+LFi20GS8hhCiVHFsIADQXpJ5q7maMLn2tAADH/3qitOKBENI4aFQTmZeXBz8/Pzx8+BB8Ph8mJiYoKiqCo6MjMjIywBgDx3FUA0kIaTCVFQwp9yU1kZRE6qt+Y+1w52IeHj0sxr0r+fDuZK7rkAghGtKoJnL16tWIi4vDlClTkJeXh7Fjx4LjODx+/Bj5+fn49ddfYWVlhb59+yIxMVHbMRNCiJz0pKr+kEYmPFRWPq/hqr6mc35OuQ4jJEDVROW9htkCAE7tfIKKcrGOIyKEaEqjmshDhw7B1tYWv/76K4yNjWXWQzU1NcXs2bPRqVMn9O7dG7169cJbb72ltYAJIUSRpP+m9iktFmPzl8nS7YrWdCa61WtYM1w7nYOczHJcP52r63AIIRrSKIlMSEhAnz59YGxcNapOkkRWVlaCz68aFenn54eePXvijz/+0GkSGRUVhaVLl+LChQsoLy+Hj48PFi5ciPHjx9f4WsYYwsLCcOjQIZw/fx7JyckoLy+Ht7c3QkJCsHDhQukxIIToVvK9qv6QPYfawKeXpcIykjWdiW4ZGvPQf5wdDv2ejnOHn0q3J8cW0sTwhDQiGl9Rra2tpf83Na3qf5STkwNbW1vpdjc3Nxw5cqQO4dVNeHg4goKCYGxsjAkTJsDCwgL79u1DSEgIUlNT8cEHH6h8fWlpKYYOHQojIyMEBgYiKCgIJSUlOHbsGD777DMcOHAAERER0t+fEKIb4kqGlAfFAICO/lZw8KAvd/quU4AVIv95irxnz1et2b46FUIbAYKnONASlYQ0Ahr1iXRycsLjx4+ljyUDaG7duiVTLiEhAQKBbr75V1RUYNasWeDxeDhz5gw2bNiANWvWIDo6Gq1atcLixYuRnJysch98Ph9fffUV0tPTERYWhtWrV+Onn37C3bt3MWLECERFReHnn39uoN+IEKJMelIJykrEMDbjobmbka7DIWq4fy1fJoGUEGVXYPfaR4iJEukgKkJIbWiURPr4+OD+/fvSx3369AFjDEuXLkV+fj4A4K+//sLly5fRrl077URaS6dPn0Z8fDwmTZqEzp07S7dbWlpi8eLFKCsrw5YtW1Tuw8DAAJ999plMratk+6JFiwAAkZGRWo+dEFI7STFVTdnurU3BUVOo3hOLGcK2ZqgsE7Y1A2IxTQFEiD7TKIkMDg5GZmYmwsPDAQA9e/aEv78/zp8/DxsbGzRr1gxTp04Fx3H4+OOPtRqwuiIiIgAAgwcPlnsuKCgIQN0SQAODqtUUdFXTSgh5TrJetns7Mx1HQtSRElsEUbZ8LWR1ouwKpMQWqSxDCNEtjZLIiRMn4uzZs2jVqpV02/79+zF8+HAAVX0jrays8P3332PEiBHaibSW4uLiAADe3t5yzzk4OMDc3FxaRhN//vknAMVJKiGk4Ygrn88P6dGW+ic3Bvm5qhPI2pYjhOiGRtVo5ubm8Pf3l9lmZ2eHQ4cOoaioCHl5eWjevDl4PN0tzZ2XlwegqvlaEaFQKC1TW6GhoVi/fj3atm2LN998s8bypaWlKC0tlT4WiaivDyHakpFcgtJiMYxMeWjuRgNqGgMLNUfJq1uOEKIbWs/yTE1N4ejoqNMEsj5FRUUhJCQElpaW2LNnD4yMau7Ev2LFClhaWkp/XF1dGyBSQl4Okvkh3duY0tQwjYRbG1MIbVQniEIbAS1fSYie0yjT4/P5atXAzZo1S2d9BiU1kMpqG0UikdJaSmWuXr2KwYMHg8fj4dixY2jfvr1ar1u0aBHy8vKkP6mpqbV6X0KIcsmSQTVtqD9kY8HjcQie4qCyTMAoO/pSQIie0yiJZIyBMfVGzalbTtskfSEV9XvMyMhAQUGBwv6Syly9ehWDBg2CWCzGsWPH4Ovrq/ZrjYyMIBQKZX4IIXUnFjMk/zf4wqMd1Vo1Jm19hRi/wAUW1rIVDbyq9Spw50Iejc4mRM/Va5tzUVGRdBRzQ+vbty8A4Pjx43LPHTt2TKZMTSQJZGVlJcLCwtCjRw/tBUoI0dgTSX9IEx4c3Kk/ZGPT1leIt1d5SR9P+sgVc75tAQMjDkkxRbh4NEuH0RFCalJvSWRubi7OnTsHR0fH+noLlQYMGIAWLVpg+/btuHnzpnR7Xl4evvnmGxgaGmLKlCnS7enp6YiNjZVr/r527RoGDRqEiooKhIaGomfPng31KxBCaiDpD+nWmvpDNlbV/27ubcxg52SMIf81dZ/ek4m0xGJdhUYIqYHaHRZbtGgh83jv3r3SuRhfVFFRgYyMDFRWVmL27Nl1ClBTAoEAGzduRFBQEAICAmSWPUxOTsZ3330HDw8PaflFixZhy5Yt2LRpE6ZNmwYAyM7OxqBBg5Cbm4vg4GCcOHECJ06ckHkfKysrLFiwoOF+MUKIlLQ/JE3t06R07muFuJsFiInKx/6fH+Otr1rA0LhpDtYkpDFTO4lMSkqS/p/jOBQUFKCgoEBpeUNDQ7z22mv45ptv6hRgXfTr1w/nzp3D0qVLsWvXLpSXl8PHxwcrV65ESEhIja8XiUTIyckBAISFhSEsLEyujLu7OyWRhOiATH/ItjSopinhOA7D33TEo4fFyEovw/G/n2D4m7pp1SKEKKd2EpmYmAigaqBMixYtMHbsWKxevVphWUNDQ9jZ2enFai7du3dHaGhojeU2b96MzZs3y2zz8PDQ2cAgQohqmSmlKCkSw9CYB0cP6g/Z1JhaCPDaHCdsW5GCa6dz4N3ZHK27Weg6LEJINWpnee7u7tL/T506FX369JHZRgghDUmyXrZba1Pw+NQfsilq0cEcPYc1w8WjWTj0exrmtGgBC2vdDNYkhMjTqKpw06ZN2o6DEEJq5XlTNvWHbMr6j7ND4p1CZCSX4OD6NLz+sRu4aoNx8nPKUaBieURzKwElnoTUE622NzPGsHXrVty8eRPu7u6YNWsWzMyorxIhRLtYtf6QNKimaRMY8DB6njM2fJaA+NuFuHw8G37BzaTPXzudg8j9z5S+vu9oWwSOsW+IUAl56WiURK5ZswZff/019u3bh379+km3jxo1CocPH5Y+3rx5My5evAgTE5O6R0oIIf/JfFSK4oLK//pD0vWlMZLUIJaXPe93npFcAgPDqlrG6jWIds5GGPx6c/y7OQMnd2TCs52ZdJ30bv2t0bqrBcrLGDZ9kQQAmL7EQ2Y/hJD6odGnKzQ0FHw+HwEBAdJt4eHhOHToEOzt7TFp0iSEh4fj1q1b2Lx5M+bOnau1gAkhJOleVX9I11Ym4AuoP2RjpKgGUZIEAvI1iK8MtEbczQLE3SzA/p8fY+aXnjAw5MHC2gAW1gYoKxFLyzq4G9OUQIQ0AI2SyAcPHqB9+/bg8/nSbXv37gXHcdixYwf69euH/Px8uLm54e+//6YkkhCiVZJJxmlqn8ZLUoOozIs1iBzHYeRbTvj103hkPirFyZ2Z0knJCSG6odFXtaysLDg5OclsO3fuHGxtbaXN2xYWFvD395dODUQIIdpA/SGbBgtrAzh6mij9UTQYxsxSgJGzq+49V45l42G08rmKCSH1T6MkUiwWo6SkRPq4sLAQ9+7dg7+/v0w5a2trZGdn1y1CQgipRtIf0sCIg5Mn9Yd82Xh3tkD3wTYAgAPrH6NQpHxkNiGkfmmURLq5ueHGjRvSx8ePH0dlZaVcEpmTkwMbG5u6RUgIIdVIaiFdvU2pP+RLauBEe9i5GKEwrxKHfk+jRSEI0RGNksjg4GCkpKTg7bffxsGDB7Fo0SJwHIdhw4bJlLt58ybc3Ny0EighhADPB9V4tKOm7JeVgSEPo992Bl/A4cH1Alw7laPrkAh5KWmURC5atAgODg747bffMHr0aDx48ACvv/462rRpIy1z/fp1pKWloVevXloLlhDycmOM1ssmVRzcjTFgQtXo7WN/P0Hmo+ddrJJjCyEWU+0kIfVNo9HZDg4OuH79OjZs2IAnT56ge/fumDx5skyZu3fvYuTIkRg9erRWAiWEkKePS1GUXwmBIQenFtQf8mXnF2SDhzcLkHCnUGZ6oO2rUyG0ESB4igPa+gp1FyAhTRzHqDNJgxOJRLC0tEReXh6EQrrAEaKuqBPZ+HdzBlp0MMPkRe66DofogevhOTi8MV3p8+MXuFAi+RKh+2vDotlYCSGNhmR+SPc21B+SAGIxQ+T+pyrLhG3NoKZtQuoJJZGEkEaBMYbkGMmgGuoPSYCU2CKIslVP8SPKrkDKf/1oCSHapVYSyefzIRAI8ODBA+ljdX8EAlq3lBBSd8/SylAoqoTAgINTC2Ndh0P0QH6uenNEqluOEFI7amV4jDGZebhq042SulwSQrQhKeb5etkCA2pEIYCFlXqVFOqWI4TUjlqfLLFYrPIxIYTUt2Rpf0hqyiZV3NqYQmgjUNmkbW4lgBv1oSWkXtDXM0KI3mOMSWsiPWi9bPIfHo9D8BQH7F77SGkZsZgh71k5rO0NVe4rP6ccBSqavc2tBArX8ybkZUZJJCFE72Wll6EwrxJ8Aw7OXjQ/JHmura8Q4xe4IHRLBvJznieB5lYCgAEFeRXY/GUSpnzmjmYORkr3c+10DiL3P1P6fN/RtggcY6/V2Alp7OqcRF66dAkRERF49OgRGGNwcXFBYGAgevbsqY34CCFEOrWPa0sTCAypPySR1dZXCM/2Zlg56z4AYNJHrvDqaI7CvAps/SYZz9LKsPnLZExZ5A47F8WJZLf+1mjd1QLlZUw6cfn0JR4wMKxan92c+lUSIkfjT0V8fDymTJmCS5cuAXg+gIbjqj5wPXr0wNatW9GyZUsthEkIeZlJpvZxp6UOiRI8Hif9v3sbM/B4HCysDTDtfx7YuiIZmaml2Px1EqYsckdzN/nR/RbWBrCwNkBZyfM+/w7uxjA0pi8thCijURKZlpaGPn36ICMjA6ampggODoanpycAICkpCWFhYbh06RICAgIQFRUFZ2dnrQZNCHl5VPWHlKyXTf0hSe2YWQow9TN3/PVtCtKTSrDl62S88akbnDypWwQhdaXRV6wlS5YgIyMDY8aMQWJiIvbu3YvVq1dj9erV2LNnDxITEzF27FhkZGRg6dKl2o6ZEPISyc4oQ0FuBfgGHFxa0o2f1J6phQBTFrvD2csExQWV2PpNMh7F0QTkhNSVRklkaGgonJyc8Pfff8POzk7ueVtbW/z1119wcnLCv//+W+cgCSEvL0ktpIsX9YckmjM242PyIje4tTZFaZEY275NQXJsoa7DIqRR0+iKnJ2djd69e8PQUPmUCYaGhujduzdycnI0Do4QQp73h6SmbFI3RiZ8vP6xGzzbm6KsRIy/V6Ug4U6BrsMipNHSKIl0cXFBYWHN3+CKioqoPyQhRGOy/SFpUA2pO0NjHiZ+6IaWHc1QXsqw/btUxN3M13VYhDRKGiWR48aNQ0REBB4/fqy0zOPHj3H69GmMHTtW4+AIIS+3nCflyM+pAF9A/SGJ9hgY8hCy0BWtu5qjspxh1w+PEHuVEklCakujJPLzzz+Hj48P+vfvjyNHjsg9f/ToUQwYMAAdO3akgTWEkBrl55QjPbFY7if6bC4AwMHdCAZG1B+SyJOcOxnJJdJtGckl0nMoP6dc4esEBjyMe88V7XoIUVnBsOfHVNy9LIJYzKRlkmMLZR4TQmRxTDLBYy30798fZWVluHDhAjiOg5WVFTw8PABUTfGTm5sLAOjZs6dcv0mO43Dq1Kk6B96YiUQiWFpaIi8vD0KhUNfhEKJzEfsyVa4W4tbaBNOXeDZgRKSxqOncqWmlGXElw4H1abh9Pg8AYGzGQ0nh87kihTYCBE9xQFtf9a7VtHyibtH9tWFplETyeJrXCHAch8rKSo1f3xTQSU6ILMmNt/pqIdM+d8eeHx+hMK8SY991Rns/S90GSfSSNpI2sZhh24pkJN1TPu3P+AUuaiWSdU1qSd3Q/bVhaTTZeHh4uLbjIIS8xBStFmJsxkdhXiV4fKBVFwsdRkf0meTcqavsjDKVz4dtzUDrbhYyK+MoQssnkpeJRmdz3759tR0HIYTISImtqhVy9jKh/pCkXqXEFkGUrbw2EwBE2RVIiS2CRzvVswTQ8onkZUJnNSFEL6U8qEoiab1sUt/yVTSHa1KOkJdFnerVGWMIDQ3FhQsX8PTpU/To0QMzZswAADx9+hQ5OTnw8vICn8/XSrCEkJeHpCaS1ssm9c1CzSbm0uKXuz8/IS/SOImMjo5GSEgI4uLiwBgDx3EoLy+XJpEnTpzA5MmTceDAAYwYMUJrARNCXg6i7Arw+ICrNyWRpH65tTGF0EZQY5P20T8z8PRRGfqNs4OxKVWOEKJRc/ajR48wcOBAPHjwAEOGDMGqVavw4iDv1157DQYGBjh48KBWAiWEvHycWphQXzJS73g8DsFTHFSWcWtdNdn9lePZ+PmjeNy5mCd339MmZXOn1jT/JSENSaOayG+++QZZWVlYu3Yt5s+fDwD4+OOPZcqYmpqiU6dOiIqKqnuUhJCXknsbqoUkDaOtrxDjF7ggdEsG8nOe10hWnycy4U4Bjm7KQHZGGfb932PcjMzF0GmOsHEwVLFnzVw7nUNTBRG9p1ESGRYWhjZt2kgTSGU8PDxoOiBCiNpeXB3EjZJI0oDa+grh2d4MK2fdBwBM+sgVXh3NpdP6tOhgjrkrWuD8kSycPfQM8bcL8cun8ejzqi38RzSDwEB7teY0VRBpDDQ649PS0uDj41NjOY7jIBKJNHkLQshLJiZKhF8+jpfZdmRjOmKi6BpCGk71eSDd25jJzQspMOSh72g7vP2tF7x8zFBZzhCx7yl+/TQBCXcKpOXqunyihbUBHD1N4OBuLN3m4G4MR08TOHqa0Ko3RC9o9FXGzMwMT58+rbFcYmIibGxsNHkLQshLJCZKhN1rH8ltz8+pwO61j9ReLYSQhmLjYIjXP3HD3UsiHPvrCbIzyrBtRQo69BSiRQczhO99fo/cvjq11ssnagstw0jqk0ZJpI+PD65du4Znz57B1tZWYZnk5GRER0dj0KBBdQqQENK0icUMYVszVJZRd7UQQhoSx3Ho0NMSLTuZI3zPU0SdyMadiyLcuShfey7K1s0XIupbSeqTRknkG2+8gTNnzmDmzJnYvn07TE1l+y2VlZXh7bffRnl5Od544w2tBEoIaZq0uVoIIbpgbMrHkKkO8OktxKblSRCrmE6yob8QaatvJdVoEkU0SiKnT5+Ov//+G4cOHUKbNm0QHBwMoGruyPnz5+PQoUNISUnBwIEDERISotWACSFNC60WQpqKilKmMoEEGv4LkbaWYdRWjSYlo02LRkkkn8/H4cOHMXv2bOzcuRMbN24EANy4cQM3btwAAIwZMwabNm3SXqSEkCZJ3dVC1C1HiK6o+0XnwY18uLYyBV/QeLpnaKtGk5rXmxaNr8rm5ub4+++/8fnnn+Pff/9FQkICxGIxXF1dMWTIEHTu3FmLYRJF6BsdaQrUWS1EaCOg6X5IvZJcT8vLno+izkgukUmSarqeqvtF5+K/2bh1Lg8delmic4CVzAjsF704yrv6lEMNSVs1mjR1UdNS579WmzZt0KZNG23EQmpJ35oXKKklmpCsFqJodLZE8BQHGlRD6pWi66kkyQHUu56q84XI0JiDwIBDoagSl8OycTksG83djNApwAo+vSxhbvn8thwTJULolueDznQ5yltbtJWMEv1AKX8jpm/NC/qW1JLGo62vEP3G2SF8j+zUYY39hkkaD8n1VBl1rqfqfCF6bY4zWnWxQPytAkSfzcX96wV4klKK4389wYntT+DdyRyd+lhBLGbY93+P5V6vq1HehChCSWQjpm/NC/qW1JLGpbJCdjLmF1cLIaQ+Sa6ndaXO8okA0KqrBVp1tUBxQSXuXMpD9Jk8PI4vxoMbBXhwowCo4bSnaa+IPqAkkmgtGdW3pJZqNBuXhNuFMo8VrRZCSGNQ0/KJ1ZmY8+E70Aa+A23wLK0U0Wdzce10DooLxHJlq6vtKG996VtJmhZKIone0bcpKUj9Ky6sxOP4Yl2HQYjW1LR8oiK2TkYYENIcdi5G+OeXtBrLR5/Lg7EZH/YuRuDxle+/KfatJPqBkkjSZNEowMYj8U4hGAOaORoiK71M1+EQolNCNVtIbkbm4mZkLgyMODi1MIGzlwlcWlb9SFpZlC0pSn0riTbQXZQ0WTQKsPGIv10AAGjRwYySSPLSU2eUt5EJD04tjJGWUILSYjGSY4qQHFMkfV7YTABnLxMk3ClUug+g9n0rqVmcVNfk76ZRUVEYOnQorKysYGZmBj8/P+zevbtW+ygtLcUXX3wBb29vGBsbw8nJCW+99RYyMzPrKWqiT/JzypGeWKz0Jz+nXNchNmqMMcT/1x/Ssz0ta0iIZJS3KiNnO2HKYg98sqE13l7phVdnOaJrPys0dzMCxwGirArEXMlHaVHNfSuTYlQnmhIxUSL88nG89PH21alY914cYqLk1wqvyYvJaPXHpPFo0jWR4eHhCAoKgrGxMSZMmAALCwvs27cPISEhSE1NxQcffFDjPsRiMUaOHIljx47Bz88PY8aMQVxcHDZu3IhTp07h0qVLsLOza4DfhugK9a2sX9kZZch7Vg6+gINrK5pQnBBA/VHeHI+DnYsR7FyM0CXQGgBQWlyJtIQSXDudg7uXak7w/lqRAmt7Q1jZGcDS1gBWdgawsjOE1X//t7ASIPZavtaaxamPZtNRpyQyPj4e69evx4ULF/D06VOMHDkSq1atAgBcvnwZ0dHRCAkJgaWlpVaCrY2KigrMmjULPB4PZ86cka6gs2TJEnTv3h2LFy/G2LFj4e7urnI/W7ZswbFjxzBx4kT8/fff4LiqavvffvsNc+fOxf/+9z+sX7++vn8dlbTVvEDNFIpR38r69fBWVS2IW2sTGBo1+cYRQtRWm1He1RmZ8OHZ3gwcB7WSSMaA7CdlyH6iuCsJp8bHMnRzBrw6msHQiK+yHPXRbFo0vvtt2bIFc+bMQWlpKQCA4zg8e/a8tqaoqAhz586FoaEhpk2bVudAa+v06dOIj4/H9OnTZZZgtLS0xOLFizFt2jRs2bIFS5YsUbmf33//HQCwYsUKaQIJALNnz8bq1avx999/Y+3atTAxMamX36Mm2vpGp81vhk0tGaW+lfUr4b/+kHYuRshILpFur+2Sc4ToA20sn1idJqO8JdRdUnTa5x4QZZUj91k5cp9K/i1D7tNy5GWVg6luEQdQtW74ihn3YWDEwcScDxMzPkzM+TA158P4v8fGpjycP5Klcj80/2XjolESeenSJcycOROmpqb48ssv0bdvX/To0UOmTN++fWFpaYnDhw/rJImMiIgAAAwePFjuuaCgIABAZGSkyn2UlJTg8uXLaN26tVyNJcdxGDRoENavX4+rV6+iT58+2gm8FrT1jU6b3wwpGSW1UVnBkHivqibyyrEcXDmWI32utkvOEaIPtLF8oraou6Sotb0hrO0NoahdTlzJEHUyB2FbMxQ8K6+8lKG8tAKiLOWJqyq1nf+S6JZGSeSqVavAGMPRo0fRu3dvhWV4PB46d+6Me/fu1SlATcXFxQEAvL295Z5zcHCAubm5tIwy8fHxEIvFCvdRfd9xcXENnkSKxazGD3XY1gy06mIOvkB5jZm6+1Hnm6G+JqNEf6XGFaG8lMHUgo/XP3IFp+Qcoy4DpLHQxvKJ2qRu30pleHwOzV2N1HqviR+4ws7FCEX5lSgurERxQbWfwkqkJRYj9X7N88Hmq1gkgugXjc7m8+fPo3v37koTSAkHBwdcv35do8DqKi8vDwCU9scUCoXSMnXZR/VyypSWlkqb/QFAJKrqo5KVlYWyMs2mM3kcV6ayiQKoSt6+mhoLcACPB/D4Vd9Mefyqztg8XlUSWZyvelScKLsCm79+CKENH3wDDgIBB74BwBdw4As4CAyq9n/hsOoRfv9uTkMz99Iak9H46BKE/Snfj0eSjAbPEMKrk7HKfVRXWvy8Leb2lXS4tjHUqEazvPT5ccrKyoKBEdWK1tXtS1VN2S6tBDC0LFJarrQSKFU+tokQvWKgPIes9bmsjeuOnScQ8okVNn5a9cbDZ1v+dx0sk+mGpoypLYOZFQ+Fucrbtc2teLByKUElrxRGloCRJWAlfZYDIMDjOCO1kkgxV4hnzzSb9SI/P1+j1xHNaJRE5ubmws3NrcZyxcXFGidJTcmKFSuwfPlyue2HDh3SuC9lcaYtgDbqFWaAuLLqB2DPN9ZCamw5gLpNZVOQK8avH6SDb1gBjl8BnqASHL8SnEDy/wpw/EoUproA4EPx4rEMx//OhP3Dq+DUuJYWP20G0cMWAKq+SR9ZnweeYSmELRNgYqe6b86LxJU8AL0AAAcPHQSPr0ZHof9UlhpAXGao9HmeYRn4Ri/fVEHPrnUCYIEnhbewf/9TXYdDiN6py3VH2X6i7obiWmzt9mPg1AzIldxzql98q+4lAqe7OHBA9TWVMYBn6PvftVDx9Z1nVIpLt46Au12r8KSKi2nlq4akURLZrFkzJCcn11ju4cOHcHBQPddVfZHUHiqrJRSJRLC2tq7zPqqXU2bRokVYuHChzOtcXV3x6quvwsJCxVdWFR7HleFATG6N5YbOFMLe3QBMLEkkGcT//Z+JGTKSy3FmT0GN+2nXyxjmlnxUVjBUlDNUVgCV5QwVFQyV5VVzkj17XFlz4GIBKkvq0pzDQVxqDIMn/WDnagBzKx7MLPkws+TB3IoHEwuetJYxProEYZHyNZriMiPk3msLv1rWaJaXMmw4V5XojHx1ZK1qBK6EFiAqTHlNm2+wKboPMVd7f01BcYEYf56pqgUZNakXzCxVj+ok5GVUl+uOtvcTH12Cs/sKUJj3PAE1t+Kj92hzeHXqq94+WipuaarCYfDr9vDqNLrWsUnk5+djwYIFGr+e1I5Gd3M/Pz8cPnwYd+/eRfv27RWWOX/+PO7evYs33nijTgFqqnp/xW7dusk8l5GRgYKCAnTv3l3lPlq0aAEej6e076SqfpfVGRkZwchIvk9Js2bNpE3itWVjw3B6e0GNo+669XVW2XTbuhPDzVNxNe5nzFxPlftJuleILV/X/MVi5Gwn2DoaorRYjJJiMUqLK1FaLEZpUdW/6UklMqsuKJN6vxyp9+Vr7jgeYG4pgIU1H5mpqmvBLxwogm8/1cenupKiSgBVF+GCp8a1GujTZ7gluvSuUDlN0Ms2+vhOXB7AgOZuRnD3aq7rcAjRS1WzQlRdd5o1a6bxrBDa2I/tAKBTz8paTzn04j6EQpHGfTRrYmiovMWHaJ9GZ+O8efNQWVmJMWPG4ObNm3LPx8TEYMaMGeA4Dm+//XZdY9RI375V34qOHz8u99yxY8dkyihjYmKC7t274/79+3I1r4wxnDhxAmZmZnjllVe0FLX61FnRIHiKQ40fbm3tRzKVhCpCGwE69raEi7cpvDqao30PIboGWqPnkGYIHGOPoDccEDhavYnbu/azQvfBNmjrawFnL2NYWAvA8QAmBvJzKpCWUIqK8pr7et46lwemxkoJdV2pwcLaAI6eJnBwf17z6eBuDEdPEzh6mrx0CSQAxN/6b6lDn5erBpYQdUhWynpx2itdr5RVlymHJNr6CvH2Ki/p40kfueK9dd40aLIR0qgmcsCAAVi4cCG+//57dOvWDV5eXuA4DseOHUPHjh1x7949iMVifPzxx/Dz89N2zGrH2KJFC2zfvh3z58+XzhWZl5eHb775BoaGhpgyZYq0fHp6OvLy8uDo6CjTPP3WW2/h0qVLWLRokcxk4+vXr0dCQgLeeustnc0RWddRd9rcj7pTSaibjNZUMzpshqPcvsRihsK8CoiyK3DnYh4uhWbXGPfB9WkI3ZIBR09jOLUwgXOLqn+t7Aykf2uaHFf7qi912LIjTeVByIv0aaqg+qCNZJTonsad07777ju0bt0ay5Ytw8OHDwFUJWLp6emwtbXF0qVLMW/ePK0FWlsCgQAbN25EUFAQAgICZJY9TE5OxnfffQcPDw9p+UWLFmHLli3YtGmTzLyWU6dOxa5du7Bjxw4kJiaib9++ePjwIfbv3w9PT0989dVXDf/LVaPpigb1sR9dJ6M8HiedGLy8VKxWEskTVDXzJMcUyTSjm5jz4eRpDEdPY1w7natyHzQ5bu09fVyK/JwKCAw4uNFSh4TI0bepgghRpE5n4axZszBz5kzcuHEDCQkJEIvFcHV1ha+vLwQC3Z/g/fr1w7lz57B06VLs2rUL5eXl8PHxwcqVKxESEqLWPng8Hg4ePIhvv/0W27Ztww8//AAbGxu8+eab+Oqrr/Ri3WxtfaPTVjOFPiSj6tZovvt9S2SllyEtsQRpCcVIS6hqPiouqET87UJpbZkqNDlu7UmOq3tbUwgMaeUfQl4k+UJMiD6rc6bHcRy6du2Krl27aiMerevevTtCQ0NrLLd582Zs3rxZ4XNGRkZYunQpli5dquXomiZ9SEbVrdEUGPDQ3M0Yzd2M0aWvFQCgolyMzNRSpCUU484lkVoDfWhy3NqR9If0ov6QhNQrbS/DSEh1uq8uJESJuiajmtZoCgx4cGphAqcWJrB1MlJr1HmhqAKMMZn11YliFWViaWLuRf0hCalXTb1vJdEttZLIM2fO1OlNAgIC6vR6QjRV1xpNdZrFAeDYtie4GZkL30E28OllqfE0HC+DlAdFqChnsLAWwM5ZveXUCCGa0VbfSqrRJIqodfYEBgZqXMPCcRwqKqipj+hOXWo01WkW9+xgitQHxXiSUoojf6Tj5I4n6BJohVcG2sCmueycZeJq0wklxxZqNAiqsYu/VdUf0svHjGpuCaln2upbqa0aTUpGmxa1ksiAgAC62JOXljrN4sWFlbgZmYuoE9nIySzHxX+zcTE0G96dzNF9sA28fMwQey0foVsypK/fvjpVaxPsNibxt2l+SEIaG23VaFLzetOi1l89IiKinsMgmqBvdA2npmZxEzM+eg5tBr9gGzy8VYArx3PwMLoAcTerfswtBSjIk6+Rf9nmmizIrcCTlFKAq6qJJIQ0Dtqq0aSpi5oW+ms1YvrWvNDUk1p1msU5Hgfvzhbw7myBrIxSRJ3IwY3IHIUJZHUvy1yTklpIRw9jmFrQ5YeQlw1NXdS00FW8EdO35gV9S2p1rZmDEYInO8DLxwzbV6eqLPuyzDWZcPt5f0hCCCGNW52SyGfPnmHjxo2IiIjAo0ePwBiDi4sL+vXrhxkzZsDenvo11Cd9a17Qt6RWX5QUidUq19TnmmRihvg7ND8kIYQ0FRonkQcPHsT06dORl5cHxp7XGMXExODkyZP49ttvsWnTJowaNUorgZL6o61kVN+SWn2p0bRQM151yzVWT1JKUZhXCQMjDq601CEhhDR6Gt21Ll++jHHjxqGiogKvvPIKpkyZAk9PTwBAUlIStm7diqioKISEhODs2bPo0aOHVoMmTZu+TUlRV+rMNck34ODgYVzjviSJsTL63NQv6Q/p2c4MfEHT7vtJCCEvA42SyC+++AKVlZVYvXo1PvjgA7nn582bhx9++AEffPABvvzySxw5cqTOgRJSW/oyClCduSYryxn++jYZkz5yUzngRFFiXJ0+N/VL1sumqX0IIaRp4Fj1tmg1WVtbw83NDdHR0SrLderUCSkpKcjJydE4wKZIJBLB0tISeXl5EAqb/rQuTUVZiRgr3owFACz6o02tV6WJiRIpnGvSd7ANLhzJQnFBJWydDPHGp+6wbKa4NrF6E72kVnX6Eg+9H3RUXirGyrfuo7KCYd5qL9g60Uo1hBDto/trw9KoKqa8vBw+Pj41luvQoQPi4+M1eQtC9Ia2+laqmmuydVcL/PVtMp6lleHP5YmY/Km7wkRL0tRfVvJ8sI6Du7HeL7OYFFOIygoGy2YGaOZoWPMLCCGE6D2Nksg2bdogNVX1lCUA8PjxY7Ru3VqTtyBEb2izb6WyuSbtnI0wY6kntn2bjKz0Mvy5PAmvf+wGZy+Tuv8CekDSlO3VkZY6JISQpkKjJHL27NmYM2cOIiMj0bdvX4VlIiMjcfbsWfz66691CpAQXWuovpWWtgaYvsQD21enIC2hBFu/SUbI+y5o0aHx9yF8Pj9k4/9dCCGEVNHo7jdr1izExsZi2LBhmDNnjszo7MTERGzbtg2//vorFixYgLfeekurARPS0BpyhQUzoQBTFrtj1w+pSLxbhO2rUzF6njPadW+8fXtEWeV4+rgUHAd4tqdJxgkhpKlQK4nk8/lKn/vhhx/www8/KHxu7dq1WLduHSoqmvYkyoRok5EJH5M+csP+nx8jJiofe398hGEzHNGtv7WuQ9OIZGofpxYmMDFXfi0hhBDSuKjVG58xpvGPWKzeah2EkOcEBjyMne+Crv2swBhw5I90nDv0DBpMpqBz1ftDEkIIaTrUSiLFYnGdfgghtcfjcRj+piN6v9oMAHBqVyaO//0ETMwgFj9PJpNjC2Ue6xOxmCHhDvWHJISQpqhpr7NGiJ7QdJogjuMwIKQ5TC0EOP73E1wKzUZ6UgmyM8qkZbavToXQRoDgKQ5o66tffSczkkpQXFAJIxNekxlpTgghpAolkYQ0gLpOE9RzaDOYWvBxYH0akmOK5J4XZVdg99pHGL/ARa8SyYe3/lvqsD0tdUgIIU0NJZGENABtTBPk42+JY389QXFBpdIyYVsz0Lqbhcx8lLr0fGof6g9JCCFNjcZJZHl5OX788Ufs2bMH9+/fh0gkUliO4zganU1eetqYJigltkhlAglU1UimxBbBo53uk7bS4kqkxlXVmnp1pP6QhBDS1GiURJaWlmLAgAG4ePFijaNFG+NoUkL0UX6uel/G1C1X35JiiiCuBKybG8DanpY6JISQpkajBXfXrVuHCxcuYPDgwbh//z6mTJkCjuNQWlqKO3fu4JNPPoGRkRE+//xzGp1NiJZYqLkyjrrl6lv8f/0haVQ2IYQ0TRrdbfbs2QMLCwvs3LkTlpaW0rVwDQwM0K5dO6xYsQK9evXCa6+9Bh8fH4wdO1arQRPyMnJrYwqhjQCibOU1jUIbAdzamDZgVMrF01KHhBDSpGlUE/ngwQP06NEDlpaWACBNIisrn/fXGjFiBLp06YKffvpJC2ESQng8DsFTHFSWCZ7i0KCDavJzypGeWCz3c/+aCNkZZeB4gGc7/UhqCSGEaJdGNZHl5eWws7OTPjYxqZr/TSQSwdr6+dJsrVu3RmhoaB1DJIRItPUVYvwCF4RuyUB+jnyNpKVtw6zxLaFo6qLqLKwFMDKlpQ4JIaQp0qgm0sHBAenp6dLHjo6OAICYmBiZcmlpaTK1k4SQumvrK8Tbq7ykjyd95Ir2flXTBx3emA5xZcMNZuvW3xpvfeWJ6Us8pNumL/GAZ/uq2scOfvozZyUhhBDt0iiJbNu2LR4+fCh93KtXLzDGsGrVKulAmsjISJw9exatW7fWTqSEEKnqTdbubcwQPMURxmY8ZCSV4FJYVoPFYWFtAEdPEzi4G0u32bsYIS2xBADQrodlg8VCCCGkYWmURAYFBeHRo0e4cuUKACAwMBDt2rXD4cOH4ezsjG7dumHQoEFgjOHtt9/WasCEEHnmlgIMntQcABC+9ylyMstqeEX9SUssQWmRGMZmPDh6Gtf8AkIIIY2SRknkpEmTsG3bNunAGh6PhwMHDqBDhw548uQJbty4AcYY5s+fj2nTpmkzXkKIEp37WsGjrSkqyhiObkrX2RytiXerRmW36GCuNyvnEEII0T6NBtbY2tri9ddfl9nWsmVLREdH4/79+8jOzkarVq3QrFkzrQRJCKkZx3EY/qYjfl2UgPhbhbh9QYSO/g3fnJx4j5Y6JISQl4FGNZGqtG7dGj179qQEkhAdaOZohIDXbAEAx7ZloCi/4VevSUsoBkDzQxJCSFOnURL5wQcf4N69e9qOhRCiBf7DbWHvYoSi/Eqc2P6kwd+fiQFbJ8MGn26IEEJIw9Ioifzhhx/g4+ODHj164LfffkNeXp624yKEaIgv4DB8piPAATfP5CHhTkG9v6dYLNv/0rMDNWUTQkhTp1ESuXDhQtjb2yMqKgrz5s2Do6MjXn/9dZw8eVLb8RFCqpGsEJORXCLdlpFcIl0pJj+nHADg6m0K34FVE/8f+TMd5WX1t4Z9TJQIv3wcL7PtzgURYqJE9faehBBCdI9jGg7hrKysxNGjR7Fp0yb8+++/KC8vB8dxcHFxwbRp0zB16lS0aNFC2/E2CSKRCJaWlsjLy4NQSJMxE/VF7MtUuUJM39G2CBxjDwAoLarEzx/HIz+nAr1fbYYBIc2l5fJzylGQq7y/pLmVABbWNTdHx0SJsHvtI6XPj1/ggra+dI4TQhoG3V8blsZJZHXPnj3Dtm3bsHnzZty+fbtqxxyHgIAAzJgxA5MnT65zoE0JneREU7VN/mKv5mPXD6ng8YG3vmqB5m5V8zbWJhlVRixmWPdeHETZyuMR2gjw3jpvmuqHENIg6P7asLSSRFZ3/fp1bNq0CTt27EB2djZ4PB4qKhp+hKg+o5OcNKTda1MRE5UPZy9jzFjmCR6Pkyaj5WUMm75IAlC1XKGBYVWyp05NZNK9Qmz5OrnG95/6mTs82lEfSUJI/aP7a8PSaJ5IVbp27YrS0lLk5eXhr7/+0tmEx4SQKkOmOiDhTiEex5cg6kQOegTZwMLaABbWBigred5X0sHdGIbG6neTzldRI6pJOUIIIY2L1uaJTEtLw7fffos2bdqgd+/e+Pvvv8Hn8zF06FBtvQUhRAMW1gYYOKGqafr07kzkZZVrZb8CA/WaqC2stP5dlRBCiB6o09W9rKwM//zzDzZv3oyTJ09CLBaDMYZWrVph+vTpmDJlChwdHbUVKyFEQ936W+PWuTykxhXj303pmPCBKzhOs36KjDFEn83Dsb8yaiwrtBHArY2pRu9DCCFEv2mURF65cgWbN2/Grl27kJubC8YYzM3NMX78eEyfPh3+/v7ajpMQUgccr2ruyPWLE/DgRgFiruSjXY/a9xfKySzDkT/SkXCnamlDS1sB8p4pb64OnuJAg2oIIaSJ0iiJ9PPzA8dxYIyhT58+mD59OsaPHw9TU6pxIERf2bsYo/ertjjzzzOEbs1Aiw5m4PHVS/DElQyXwrIQvvcpKsoYBAYc+o6xQ88hzfDgRj5Ct2QgP+d5Mim0ESB4igNN70MIIU2YRkmkk5MTpk6diunTp6Nly5bajokQUk/6vGqLu5dEyEovw4mdTxD0ukONr0lPLMbhjelIT6qa4NyjnSlGvOkEGwdDAEBbXyE825th5az7AIBJH7nCq6M51UASQkgTp1ESeebMGRgYGMDV1VXb8RBC6pHAkIfhbzpiy1fJuH46F+1U1BSWl4oRvu8pLoVmgYkBYzMeBr/ugM4BlnL9KasnjO5tzCiBJISQl4BGSaS3tzf8/Pxw/vx5bcdDCKlnHm3N0CXQCjcicvHvlnTp9uTYQmkNYsKdAhz5Ix05mVUjudv7CRE8xQHmljTSmhBCSBWN7ggWFhbw9PTUdiyEkAYyaFJz3LsiQnbG8+l+tq9OhbmVAM0cDZEcUwSgqm/jsOmOaNXVQlehEkII0VMaJZHt2rVDamqqtmMhhDSQpHuFKC0Sy20vyK2QLqvoO9gaA8bbw8iE39DhEUIIaQQ0mmx81qxZOH/+PKKiorQdDyGknonFDGFbVc/xaCbkI3iyAyWQhBBClNIoiZw+fTrefvttDB48GN988w3u37+P0tJSbcdGCKkHKbFFEGWrXoqwUFSJlNiiBoqIEEJIY6RRczaf/7x24vPPP8fnn3+utCzHcaiooLVzCdEXtOY1IYQQbdCoJpIxpvaPWCzf76qhiEQiLFy4EO7u7jAyMoKHhwc++ugjFBQUqL2PuLg4fPPNNwgICICTkxMMDQ3h6uqKKVOmIDY2th6jJ6R+qLuWtTrl8nPKkZ5YjIzkEum2jOQSpCcWIz2xGPk52lmnmxBCiP7hGGNM10HUh8LCQvTu3Rs3b97E4MGD0aVLF9y4cQPHjx+Hr68vzpw5A2Nj4xr3M2HCBOzatQsdOnRA7969IRQKcfv2bYSGhsLExARhYWEICAioVWwikQiWlpbIy8uDUEgrepCGJRYzrHsvTmWTttBGgPfWedc432PEvkxE7n+m9Pm+o20ROMZe41gJIaQ26P7asJrspG+rVq3CzZs38cknn+Dbb7+Vbv/000+xcuVK/PDDD1i0aFGN+wkODsYnn3yCLl26yGzfuXMnJk6ciLlz5+Lu3btaj5+Q+sLjcQie4oDdax8pLaPumtfd+lujtYrpf8zVrPUkhBDS+DTJmkjGGFxcXCASiZCRkQEzMzPpc4WFhXBwcIC9vT3i4+Pr9D6tW7fGgwcP8PTpU9ja2qr9OvqmRPRBTJSI1rwmhDQpdH9tWBr1iZSIj4/Hxx9/jN69e6N169b4+OOPpc9dvnwZGzZsQF5eXp2DrK24uDikpaXB399fJoEEADMzM/j7+yMhIaHOc10aGBgAAAQCqm0hjU9bXyHeXuUlfTzpI1e8t86bEkhCCCFq0Tj72bJlC+bMmSOd2ofjODx79rxvVFFREebOnQtDQ0NMmzatzoHWRlxcHICq5RkV8fb2xrFjxxAXF6fx+t9XrlzB3bt34evrCysrK5VlS0tLZaZAEolEGr0nIdpGa14TQgjRlEY1kZcuXcLMmTNhaGiIVatW4fLly3ixVbxv376wtLTE4cOHtRJobUhqPy0tLRU+L6ni1rSWNC8vD1OnTgWPx8OqVatqLL9ixQpYWlpKfzRNXAkhhBBC9IVGNZGrVq0CYwxHjx5F7969FZbh8Xjo3Lkz7t27p3FwH3zwQa0mMX/vvfeU1j5qS3FxMUaNGoXY2Fh8/fXXCAwMrPE1ixYtwsKFC6WPRSIRJZKEEEIIadQ0SiLPnz+P7t27K00gJRwcHHD9+nWNAgOA9evXo7CwUO3yY8eOhbe3t7QGUllNo6Q5WVlNpTIlJSUYOXIkwsPDsWjRIixevFit1xkZGcHIyKhW70UIIYQQos80SiJzc3Ph5uZWY7ni4mKUlZVp8hYAUKtJwauT1EZK+ka+qKY+k4oUFxdj5MiROHHiBD7++GN88803GsVGCCGEENIUaNQnslmzZkhOTq6x3MOHD+Hg4KDJW9SJt7c3nJyccP78ebmazMLCQpw/fx6enp5qNylXTyA//PBDrFy5sj7CJoQQQghpNDRKIv38/HD16lWVk2yfP38ed+/erbHJuz5wHIeZM2eioKAAX375pcxzX375JQoKCjBr1iyZ7UVFRYiNjUVKSorMdkkT9okTJ7Bw4UKsXr263uMnpL7RcoWEEELqSqPJxk+dOoVBgwahVatW2LlzJzp37gwej4dp06bhzz//RExMDF577TU8fPgQ58+fh5+fX33ErlJhYSH8/f0RHR2NwYMHo2vXrrh+/bp02cPIyEiYmJhIy0dERKBfv37o27cvIiIipNunTZuGLVu2wMHBAbNnz1b4XtOmTYOHh4fasdFkqETXaLlCQkhTRPfXhqVRn8gBAwZg4cKF+P7779GtWzd4eXmB4zgcO3YMHTt2xL179yAWi/Hxxx/rJIEEqiYVj4yMxLJly7Bv3z6Eh4fD0dERH3zwAZYuXSqTQKqSlJQEAMjIyMDy5csVlgkMDKxVEkmIrtFyhYQQQuqqTsse/v7771i2bBnS09Nlttva2mLp0qWYN29enQNsiuibEiGEEKJ9dH9tWHVeO5sxhhs3biAhIQFisRiurq7w9fWlpQBVoJOcEEII0T66vzasOmd6HMeha9eu6Nq1qzbiIYQQQgghjYBGo7NVKS4uRnR0NLKysrS9a0IIIYQQoic0SiLPnj2LhQsXIjo6Wmb733//DXt7e3Tt2hWOjo744osvtBIkIYQQQgjRLxr1iZw8eTJ27dqFtLQ02NraAgBSU1Ph7e2NsrIyWFlZITc3FxzH4fTp0+jbt6/WA2/MqM8GIYQQon10f21YGtVEXr58GZ06dZImkACwbds2lJWVYdmyZcjOzkZkZCQA4JdfftFOpIQQQgghRG9olEQ+e/YMLi4uMttOnz4NQ0NDLFy4EADQp08f+Pn54caNG3WPkhBCCCGE6BWNksiCggKZyboZY4iKisIrr7wCc3Nz6XYPDw+kpaXVPUpCCCGEEKJXNEoibWxspCu5AMCNGzeQn5+PXr16yZQrLy+HoaFhnQIkhBBCCCH6R6N5In19fREaGoqLFy+iZ8+eWLduHTiOQ//+/WXKxcXFwdHRUSuBNiWSsUwikUjHkRBCCCFNh+S+Wsd1VIiaNBqdffLkSQQFBQGAdBRUixYtcP/+ffB4VZWbz549g4ODAyZMmIC//vpLu1E3co8ePYKrq6uuwyCEEEKapNTUVLmxG0T7NKqJHDhwIP78808sX74cmZmZ6Nu3L3755RdpAglUjdYWi8U0vY8CTk5OSE1NhYWFBTiOk2739fVFVFSUTNkXtyl7LBKJ4OrqitTUVK1Pa6AorrqWV1VG2XO1PT7V/19fx6e2x0bd19T2+NC5o/o5fTw++nLuKNpe0/GizxZ9tlRt1+XxYYwhPz8fTk5OWtkfUU3jZQ+nTp2KqVOnKn1+zpw5mDFjhsxAG1KFx+Mp/IbE5/PlPkgvbqvpsVAo1PrFSlFcdS2vqoyy52p7fBSV1/bxqe2xUfc1tT0+dO6ofk4fj4++nDuKttd0vOizRZ8tVdt1fXwsLS21ti+iWp3XzlbGxMREZgQ3qdm8efNq3FbT4/pQ2/dQp7yqMsqeq+3x0cdjo+5rant86NxR/Zw+Hh99OXcUba/peOnjuaPua+izpVmZxvTZIg1Hoz6R1aWlpeHMmTN49OgRGGNwcXFBQEAAnJ2dtRUjUQPN0q8aHR/l6NioRsdHNTo+ytGxUY2OT+OncU1kVlYW5s+fj927d0MsFss8x+PxMHbsWPz0008yq9qQ+mNkZISlS5fCyMhI16HoJTo+ytGxUY2Oj2p0fJSjY6MaHZ/GT6OayNzcXPTq1Qv3798HAHTp0gWenp4AgKSkJFy/fh0A0KpVK1y8eBFWVlbai5gQQgghhOicRpONf/HFF4iNjUXPnj0RHR2Nq1evYs+ePdizZw+ioqJw69Yt+Pv748GDB/jiiy+0HTMhhBBCCNExjWoiPTw8UFBQgISEBKX9GPLy8uDl5QVzc3OZ1W0IIYQQQkjjp1FNZEZGBgIDA1V2hLW0tERgYCCePHmicXCEEEIIIUQ/aZRENm/eXK0lhRhjsLe31+QtSAPLz8/HnDlz4OTkBDMzM3Tp0gV79+7VdVh6w8PDAxzHyf2sXr1a16HphZSUFIwfPx7W1tYwMzODr68vHj9+rOuw9MKyZcvkzptXXnlF12Hpnblz54LjOPzf//2frkPRGz/88APat28Pc3NzWFlZoX///rh8+bKuw9IbK1aswCuvvAILCws0b94c48ePp5bPBqZREjl8+HBEREQgLy9PaZnc3FyEh4dj2LBhGgdHGs7777+PiIgI7N69G7dv38b48eMxYcIE3Lp1S9eh6YWoqCikp6dLf7Zv3w4AGD16tI4j072srCz07t0bVlZWOHnyJG7duoUlS5bQiMtqOnXqJHP+HDt2TNch6ZUjR47g4sWLtMrIC9zd3fH9998jOjoaFy5cQMuWLREUFISsrCxdh6YXIiMj8e677+Ly5csICwtDdnY2hgwZgoqKCl2H9vJgGsjKymItW7Zk/v7+7NatW3LP3759m/Xu3Zu1bNmSPXv2TJO3IA2sffv2bMWKFTLbbGxs2KZNm3QTkJ6bNGkSCwgI0HUYeuGjjz6iY6HC0qVLWbdu3XQdht7KyMhgLi4u7NatW8zd3Z399NNPug5Jb+Xl5TEALCIiQteh6KWEhAQGgEVHR+s6lJeGWjWRM2bMkPn58MMP0b59e1y4cAGdO3dG165dMWbMGIwZMwZdu3ZF586dceHCBXTo0AEff/xx/WbBjdhff/2F2bNn45VXXoGRkRE4jsPmzZtVviYqKgpDhw6FlZUVzMzM4Ofnh927d9c5ll69euHgwYPIyMgAYwx79uxBaWmpTtc+16fjU11eXh7++ecfTJ8+Xav7rQ19OjaHDx+WXgPs7e3h6+uL/fv313m/daFPxwcAYmJi4OjoiJYtW2L69OnIyMjQyn41oW/HZvr06Zg/fz58fHy0sr+60rfjI1FWVoYNGzbA2tpap8dKX48PAGnrqI2Njdb3TZRQJ9PkOE7jHx6PV895cOPl7u7OADBbW1vp/1XV/J0+fZoZGBgwCwsLNmvWLLZw4ULp67777rs6xVJcXMwmTpzIADCBQMCEQiE7duxYnfZZV/p0fKr79ddfmbm5OSsoKNDaPmtLn46NkZERMzIyYsuWLWPXr19nq1evZjwej0VGRtZpv3WhT8fn33//Zbt372a3bt1ioaGhzNfXl7Vt25aVlJTUab+a0qdj89NPP7GAgABWWVkpjU3XNZH6dHwYY+zMmTPMzMyM8Xg85uTkxK5evVrnfdaFvh0fiYqKCjZgwAA2dOhQre2T1EytJHLz5s11+iGKnThxgiUlJTHGGFuxYoXKD2N5eTnz8vJiRkZG7MaNG9Ltubm5rFWrVszQ0FC6L4lPPvmEAVD5I/Htt9+ydu3asdDQUHbz5k325ZdfMisrK3b37l2t/97q0qfjU52vry+bMWOGVn5HTenTsTEwMGC9e/eWef2rr77KXn/9de38shrQp+PzoszMTGZsbMz27t1b599TE/pybGJiYpi9vT1LTEyUvlYfkkh9OT4SRUVFLC4ujl26dIm9+eabrEWLFuzp06da/Z1rQ9+OD2OMicViNnPmTObt7c0yMzO19ruSmmnUJ5JoX00fxmPHjjEAbPr06XLPbd68mQFgy5cvl9memZnJYmJiVP4wVnWRMjAwYGFhYTKvHzhwIJs3b552fsE60uXxqe7OnTsMADt79qxWfi9t0PWxcXV1ZW+++abM6z/55BPWp0+fuv9yWqDr46NImzZt2OrVqzX+nbRFl8dm06ZNjOM4xufzpT8AGI/HY506ddL2r6oRfTx3WrZsyVatWqXx76RN+nB8xGIxmzNnDvPw8GApKSla+92IejReO5s0rIiICADA4MGD5Z4LCgoCUDVSrTo7OzvY2dnVuO/y8nKUl5eDz+fLbOfz+XLrouur+jw+1f3555/w9vZG7969NQtUB+r72PTq1QsPHz6U2fbgwQO4u7trEG3Da6hzRyInJwfJycnw8PDQ6PUNqT6PzWuvvSY31VFQUBCmTZum0/7GtdHQ5w5QNXVeaWmpxq9vSPV9fBhjmDdvHo4ePYrIyEi4urrWLWBSa3VOIsvKynDt2jXpnHDOzs7o1q0bDA0N6xwceS4uLg4A4O3tLfecg4MDzM3NpWVqSygUok+fPvjwww/xf//3f3BycsKhQ4dw4sQJ/Pvvv3WKu6HU5/GRqKiowF9//YUFCxbUaT8Nrb6Pzfvvvw9/f3+sWbMGI0eOxMmTJ3H48GG5m4O+qu/j89FHH2HEiBFwc3PDo0ePsHjxYjg7O2Po0KEa77Oh1OexsbKygpWVlcw2AwMD6QCkxqC+z51PPvkEr776KlxcXJCdnY1ffvkFjx49wpgxYzTeZ0Oq7+Mzb9487NixA4cPH4aJiYl0wJqNjQ3lIA1E4ySyoqICy5cvx08//YT8/HyZ5ywsLDB//nwsWbIEAgFVdmqDZNSZpaWlwueFQqHKeTtrsnPnTnzyyScYN24c8vLy0LJlS2zevFn6bVHf1ffxAYCjR4/i2bNnmDJlSp3209Dq+9j06NEDe/bswWeffYb//e9/aNWqFfbs2YNevXppvM+GVN/HJzU1FRMmTMCzZ8/QvHlz9O3bF9u2bYOpqanG+2woDfG5aszq+/ikpaVhwoQJyMzMhI2NDXx9fXH27Fm0bdtW4302pPo+Pr/++isAoE+fPjLbw8PDERgYqPF+ifo0yvDEYjFeffVVHDt2DIwxWFtbw9PTEwCQmJiInJwcfP3117h27RoOHz4MHk+jOc1JA3JycsK2bdt0HYZeGzlyJCorK3Udhl4aNWoURo0apesw9NLOnTt1HUKjQauNyKJrsmpMjZXzSP3SKLvbuHEjwsLC4O7ujr179yIrKwtXr17F1atXkZWVhX379sHd3R1hYWH4448/tB3zS0nyTU7ZtzaRSKT0297LgI6PcnRsVKPjoxwdG9Xo+KhGx6fp0yiJ3Lp1K0xMTHD69GmFy76NGjUKp06dgpGREbZs2VLnIMnzPiWK+o9kZGSgoKBAYb+TlwUdH+Xo2KhGx0c5Ojaq0fFRjY5P06dREnnnzh0EBgaqHF3o6emJ/v37486dO5rGRqqRrBxz/Phxueck6/DqcnUZXaPjoxwdG9Xo+ChHx0Y1Oj6q0fFp+jRKIktLS9WqgrawsGg0UxHouwEDBqBFixbYvn07bt68Kd2el5eHb775BoaGho1uwIc20fFRjo6NanR8lKNjoxodH9Xo+DR9HNOgZ2qrVq1QXl6Ohw8fys0tKFFZWYmWLVtCIBDUeWqVpmrjxo04d+4cAOD27du4fv06/P39pdNb9O7dGzNnzpSWDw8PR1BQEIyNjfH/7d1/TFX1/wfw56HL5YfgFOTqgABNpkzkh8xoIXpnMH4qya8YrF0Zm/bL1arJYpMfW5uVzUbR/FEwa0UaVE4cMsxxTefEagIupTlAUBQLWIlyBYHX9w92z8frBeQCX019Pv5i9/U+7/f7vM7mXp7zPu+TkZEBV1dX/PDDD2hvb8fHH3+Md95556Gcx/8X5md8zM3EmJ/xMTcTY34mxvyQhansUP7GG2+InZ2dvP766zI4OGgVHxgYkNdee03s7Oxky5Yt09gL/fFmMBgm/LSTwWCwOqa+vl5iY2Nl9uzZ4uTkJM8++6zs37//wU/+AWB+xsfcTIz5GR9zMzHmZ2LMD91tSnciOzs7ERQUhH/++Qeenp7IyMhQt/hpbW3FgQMHcPXqVbi5uaGhoQFeXl62DkFERERE/2FTKiIB4Ndff0VaWho6OjqgKIpFTETg4+ODyspKq89aEREREdGjb8pFJDD6ycOKigoYjUaLzx7q9XqkpaXxs0NEREREj6lpFZFERERE9GTi9wiJiIiIyGYsIomIiIjIZiwiiYiIiMhmLCKJiIiIyGYsIomIiIjIZiwiiYiIiMhmLCKJ6JF06dIlKIoCPz+/BzKeiGDHjh0IDAyEk5MTFEWx+NDCwMAA8vLy4O/vDwcHhwc6NyKih4FFJBEBAPz8/KAoCi5dujSj/RYWFkJRFBQWFs5ovw/arl27sHXrVly5cgVxcXEwGAwwGAxqfNu2bdi+fTv6+vqQlJQEg8GA1NTUBzrHxyXXRPRo0DzsCRARPQq+//57AEBFRQWio6PHjZ84cQL+/v4PdG5ERA8D70QSEU1CR0cHAIxbIN4vTkT0uGERSfSE27dvHxRFQXt7OwBg4cKF6no/RVFgNBot2p85cwbp6enw9PSEVquFTqfDunXrcPToUau+FUVBUVERAKCoqMii340bN6rtzp8/j4KCAkRERMDLywtarRbu7u6IiopS7/DNpKGhIXz55ZfQ6/Vwc3ODg4MDFi5ciFdffRWXL1+2aKvX66EoCtra2gBY5qewsFBdBmD+guzd57hv3z6Lvn7//XdkZWXBx8cHDg4OcHNzQ0xMDKqrqyeca1lZGaKiojBv3jw4ODjA29sbUVFR+Oyzz9R2k801APT29iIvLw/Lli2Ds7MzXF1dERYWho8++ggmk8lqDkajEYqiQK/Xo7+/H/n5+QgICICzszPXfRI9wfg4m+gJt3jxYhgMBlRWVuLWrVtISUmBi4uLGl+wYIH69xdffIFXXnkFIyMjCA0NhV6vR3t7Ow4fPozDhw+jsLAQBQUFanuDwYCGhgY0NjYiODgYISEhamzVqlXq3zt37kRpaSmWLl2K5cuXY86cOejo6EBdXR2OHTuG06dPY+fOnTNyvn19fVi/fj2MRiNcXFwQFhYGDw8PnDt3Drt370ZFRQWOHj2K0NBQAEBsbCz8/PzGzE9ISAhSU1PR3d2Nr776Sj3nu3NrVlxcjLfffhsjIyMICQlBeHg4urq6YDQaUVtbi6KiIuTn51vM9d9//0ViYiJOnjwJe3t7PP/88/D09ERXVxeamppw7NgxbNmyxaZct7a2Yu3atWhvb4eHhwfi4+Nx584d1NXVITc3FwcOHMDPP/+MuXPnWuXu9u3b0Ov1OH/+PFavXo3g4GD09PRM84oQ0SNLiIhExNfXVwBIW1vbmPGmpibRaDSiKIp8/fXXFrHq6mrRarUCQGpray1iBQUFAkAKCgrGHdtoNEpLS4vV783NzeLt7S0ApL6+3iLW1tYmAMTX13dS52eWmZkpACQxMVGuX79uEfvkk08EgPj7+8vQ0JBF7H75ASDj/ZNaU1MjiqLIvHnz5Pjx4xaxpqYm9RyNRqNFLDk5WQBIaGio1bh37tyRgwcPWvw2mVyHh4cLAFm/fr3cvHlT/f2vv/6SFStWCADJzMy0OKaurk49v6CgILl27dq4/RPRk4OPs4loUoqLizE0NIQNGzbg5ZdftojFxcVh06ZNAIAdO3bY3PeaNWuwaNEiq9+XLFmCbdu2AQAqKyunMGtLFy5cwHfffQdPT0+Ul5dDp9NZxN966y3Ex8fj4sWLOHLkyLTHMysoKICIYPfu3Vi9erVFbPny5epd1rsfTzc2NuLHH3+Eo6MjqqqqrB4bazQaJCUl2TSPkydPor6+Hs7Ozti7dy9mzZqlxjw8PLB3714AwP79+3HlypUx+ygpKbG4O01ETy4+ziaiSTGvjbx3fZ1ZTk4OSkpKcOLECQwPD+Opp56yqf+bN2/iyJEjOHv2LLq7uzE4OAgAuHbtGgDgzz//nPLczaqrqyEiiIuLg6ur65ht9Ho9qqurcerUKSQmJk57zO7ubpw5cwZOTk5Yt27duGMCwKlTp9TfampqAAAJCQnw8vKa9jyA/13D2NhYzJ8/3yoeFhaG4OBgNDY24vjx48jKyrKI63Q6REZGzshciOjRxyKSiCals7MTwOiLJWN55plnAIyum+vp6bG6yzeRqqoqZGdnT7i+7saNGzbMdmytra0AgNLSUpSWlk7Y9u+//572eADQ1tYGEYHJZIKDg8OkxzS/6LR06dIZmQdw/2sIjF7HxsZGte3d+BINEd2NRSQRPVSdnZ146aWXYDKZsHXrVmRlZcHPzw8uLi6ws7NDbW0tYmJi1Lefp2NkZATA6AsxwcHBE7YNDw+f9nh3j+ni4oKUlJQZ6fNhcXJyethTIKL/EBaRRDQpXl5eaGlpQWtrKwIDA63i5rt8jo6OcHNzm3S/VVVVMJlM2LBhAz788EOr+MWLF6c+6Xs8/fTTAICIiAiUlJTMWL+TGVNRFJSVlcHObnJL0X18fAAAzc3NMzYX82Nx87Uaizk2U4/QiejxxRdriAgAoNVqAYzuSzgW87q9e/c+NCsrKwMAREZGQqP53/9P79dvb28vAMDX19cqJiIoLy+//+QnKS4uDgBw6NAh3L59e8b6nYinpyeCgoLQ19enrnOcjNjYWACj6zivXr06qWMmew1rampw/fp1q/jZs2fR0NAAOzs7qxeAiIjuxSKSiAAA3t7eAIA//vhjzPibb74JjUaDgwcP4ptvvrGI1dbWYs+ePQCAd99916Z+AwICAIy+fW1+iQYAhoeHkZ+fb/GyyXSFhoYiJSUFly9fRnJy8pjfCb916xa+/fbbMYusqXr//fcBANnZ2aiqqrKKiwjq6+tRW1ur/hYSEoKkpCSYTCYkJSWpX8QxGxoawqFDhyx+u1+uV61ahfDwcJhMJmzevBn9/f1qrLu7G5s3bwYAZGRkqHdQiYjG9TD3FyKi/46SkhIBIC4uLpKcnCw5OTmSk5Mjzc3Naps9e/aInZ2dAJAVK1ZIZmamREREiKIoAkAKCwut+u3q6pJZs2YJAImIiJCNGzdKTk6OlJWVicjofodhYWHq2AkJCZKeni6+vr5ib28vubm5AkDWrFlj0e9U94m8ceOGvPDCCwJAtFqtrFy5UtLT0yUtLU1Wrlyp7nd54cIFi+Oms0+kiEhxcbFoNBoBIIsXL5aEhATJzMyU6Oho0el0AkByc3Mtjunt7ZXnnntOnater5fMzExZu3ateHh4WI13v1yLiLS0tKjnotPpJDU1VZKSkmT27Nnqde3t7bXo17xP5L3XgIiebCwiiUhERIaHh2X79u2ybNkycXR0VIuiuro6i3anT5+W1NRUWbBggWg0GnF3d5eEhASrTcbv9ssvv0hUVJTMnTtXLUINBoMa7+vrk7y8PFmyZIk4OjqKTqeTF198UX777bdxC5ipFpHmcy0vL5f4+HiZP3++2Nvbi7u7uwQGBkp2drb89NNPMjg4aHHMdItIEZFz587Jpk2bxN/fXxwdHcXZ2VkWLVokMTEx8umnn0pnZ6fVMQMDA7Jr1y6JjIyUOXPmiFarFW9vb4mOjpbPP//cqv39ci0i0tPTI++9954EBASo8wgNDZUPPvhA+vv7rfpkEUlEY1FEZuCVRyIiIiJ6onBNJBERERHZjEUkEREREdmMRSQRERER2YxFJBERERHZjEUkEREREdmMRSQRERER2YxFJBERERHZjEUkEREREdmMRSQRERER2YxFJBERERHZjEUkEREREdmMRSQRERER2YxFJBERERHZ7P8AwtWgPRol5w8AAAAASUVORK5CYII=", + "image/png": "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mh9d1kYbuhRLYVsfxqL7sRS29WEsuh9LYVsf0n2SksuvvvoK69evx6lTp1CtWjXUqFEDpUuXBvB+KKKIiAhkZGSgevXqGDlypFYD1pS/vz9mzJiBAwcOoGvXrkrT9u/fr6iTHVNTU3h5eeH06dOIjo5WGYro4MGDMDc3R61atQC8PzrZv39/tW2tX78eb968QZ8+fQDgoySPRERERPlNUnJpamqKI0eOYMiQIdi0aRPOnz+vdOpbT08PXbp0we+//55vjzxs1KgR3NzcsGbNGowcORLVq1cH8P7mm59++glGRkbo1auXov6jR4+QmJiI4sWLK93AM3DgQJw+fRoTJ07EP//8ozjyuHjxYty9excDBw5UDDVUvXp1LF26VG08hw4dQlxcXJbTiYiIiAoDyY9/tLW1xbp16xATE4OjR48qPf7Rz88Pzs7OWgtSCgMDAyxduhSBgYHw8/NTevxjdHQ05syZA1dXV0X9iRMnYuXKlVixYoXi6CIA9O7dG+vXr8fatWsRFRUFf39/3L59G1u2bEHp0qUxffr0T79yRERERDoqz88Wd3Z2Ro8ePTSuv337dly6dCnHJ+NoQ4MGDXD8+HEEBwdj/fr1SE1NhYeHB2bNmqXxtaB6enrYvn07Zs6cidWrV2PevHmws7ND//79MX369GyfK05ERET0uclzcplb27Ztw6pVqz5JcgkAXl5e2Lt3b471QkJCEBISonaasbExgoODERwcLDmOe/fuSZ6XiIiIqKDg7VlEREREpDVMLomIiIhIa5hcEhEREZHWMLkkIiIiIq1hcklEREREWsPkkoiIiIi0hsklEREREWkNk0siIiIi0homl0RERESkNZ88uRRCQAjxqRdLRERERJ/AJ08uQ0JCkJGR8akXS0RERESfQJ6Sy//++w+DBw9GhQoVYGFhAXNzc5QvXx6DBw/G1atXtRUjERERERUQkpPL33//HTVq1MBff/2FW7du4fXr13jz5g0iIyOxZMkSeHp64rffftNmrERERESk4yQll3v37sWIESOQlpaGDh06YMeOHbhy5QquXLmCnTt3IigoCOnp6Rg9ejT27t2r7ZiJiIiISEcZSJlp9uzZkMlkWLduHTp16qQ0rXLlymjZsiU2bdqEzp07Y/bs2WjevLlWgiUiIiIi3SbpyOX58+fh5eWlklhmFhQUhDp16uD8+fOSgyMiIiKigkVScimTyVCmTJkc65UpUwYymUzKIoiIiIioAJKUXFatWhWRkZE51ouMjISHh4eURRARERFRASQpuRwzZgzOnj2LdevWZVln/fr1OHv2LEaPHi05OCIiIiIqWCTd0OPp6YnRo0fjiy++wKZNm9CrVy+ULl0aABAVFYXVq1dj69atGD16NGrXro379+8rzV+qVKm8R05EREREOkdScilPJIUQ2Lp1K7Zu3apSRwiBX3/9Fb/++qtSuUwmQ1pampTFEhEREZGOk5RcOjs780YdIiIiIlIhKbm8d++elsMgIiIiosIgT88WJyIiIiLKjMklEREREWlNnpLLY8eOoXPnzihZsiSMjY3Rv39/xbSDBw9i0qRJiIuLy3OQRERERFQwSE4up0+fjoCAAGzatAkPHz5EamoqhBCK6dbW1pg1axa2bNmilUCJiIiISPdJSi737t2LKVOmoESJEtiwYQMeP36sUsfLywsODg7YtWtXnoMkIiIiooJB0t3i8+fPh7GxMfbu3YvKlStnWa9atWoaPSaSiIiIiAoHSUcuz549Cy8vr2wTSwBwcHDgNZdEREREnxFJyeWrV6/g6OiYY73ExERkZGRIWQQRERERFUCSkstixYrh9u3bOda7efMmnJ2dpSyCiIiIiAogScll/fr1ERERgRMnTmRZZ9euXbh9+zYaNGggOTgiIiIiKlgkJZdjx46FTCZDhw4dsG3bNqSlpSlN37dvHwYMGABDQ0OMGDFCK4ESERERke6TlFzWrFkTc+fORXx8PDp27AgbGxvIZDJs3rwZNjY2aNmyJZ48eYK5c+eiUqVK2o6ZiIiIiHSU5EHUv/rqK+zZswe1a9fGmzdvIIRAcnIykpKS4OHhgR07dmD48OHajJWIiIiIdJykcS7lAgMDERgYiISEBERFRSEjIwPOzs4oXry4tuIjIiIiogIkT8mlXJEiRVCkSBFtNEVEREREBZjk0+JERERERB/SKLmcN28e3r17l6cFvXv3Dr/88kue2iAiIiIi3aZRcjl27FiUL18eixcvRnJycq4WkJiYiN9//x3u7u74+uuvJQVJRERERAWDRsnl1q1boaenhyFDhsDR0RFffPEFVqxYgRs3bkAIoVRXCIHr169j+fLl6NatG5ycnDBy5EgYGhpi69atH2UliIiIiEg3aHRDT9u2bdG8eXP89ttvWLBgAdasWYO1a9cCAPT09GBtbQ0rKyskJSXhxYsXioRTCIFSpUphxIgRGDFiBIyMjD7emhARERFRvtP4bnEjIyOMGzcOY8aMwfbt27Ft2zaEhYUhJiYGz549w7NnzxR1nZ2d0aBBA7Rr1w5t2rSBnh7vGyIiIiL6HOR6KCI9PT20b98e7du3BwAkJCTg8ePHSExMhI2NDYoWLcphiYiIiIg+U3ke55JjXBIRERGRHM9XExEREZHWMLkkIiIiIq1hcklEREREWlPok8uzZ8+iRYsWsLGxgbm5Oby9vbFhw4ZctZGSkoLvv/8e7u7uMDExgZOTEwYOHIgnT56o1I2IiMDkyZPh7e2NokWLwtjYGG5ubhg6dChiY2O1tVpEREREOinPN/TostDQUAQGBsLExARdu3aFpaUlNm/ejC5duiAmJgZjx47NsY2MjAy0bdsW+/fvh7e3Nzp27IjIyEgsXboUhw8fxunTp+Hg4KCoP3jwYJw5cwZeXl7o2rUrjI2NcebMGSxatAgbN27EsWPHUKFChY+52kRERET5ptAml2lpafjyyy+hp6eHo0ePonr16gCAKVOmwMvLC5MmTUJQUBBcXFyybWflypXYv38/unXrhn/++QcymQwA8Oeff2LIkCH47rvvsHjxYkX9Hj164O+//0bZsmWV2pk1axa++eYbjB07Frt379buyhIRERHpiEJ7WvzIkSO4c+cOunfvrkgsAcDa2hqTJk3Cu3fvsHLlyhzb+euvvwAAM2bMUCSWADBo0CC4ubnhn3/+wZs3bxTlI0aMUEksAWDcuHEwNTVFeHh4HtaKiIiISLcV2uQyLCwMANC0aVOVaYGBgQCQY6L39u1bnDlzBuXLl1c5wimTydCkSRO8evUK586dyzEemUwGQ0NDGBgU2oPFRERERIU3uYyMjAQAuLu7q0xzdHSEhYWFok5W7ty5g4yMDLVtZG47p3YAYNOmTUhKSlKb7H4oJSUFSUlJSn9EREREBUGhTS4TExMBvD8Nro6VlZWiTl7ayFwvKzExMRg5ciRMTU3xww8/ZFsXeH8K3traWvHn7Oyc4zxEREREuqDQJpe6IiEhAS1atMCTJ0+wZMkSlC9fPsd5Jk6ciMTERMVfTEzMJ4iUiIiIKO8K7QWA8qONWR1VTEpKgq2tbZ7byFzvQwkJCWjUqBGuXbuGRYsW4YsvvtAodmNjYxgbG2tUl4iIiEiXFNojl9ldDxkXF4eXL19meS2lnJubG/T09LK8pjK76zrlieWlS5ewcOFCDBo0KLerQERERFTgFNrk0t/fHwBw4MABlWn79+9XqpMVU1NTeHl54ebNm4iOjlaaJoTAwYMHYW5ujlq1ailNy5xYLliwAEOHDs3LqhAREREVGIU2uWzUqBHc3NywZs0aREREKMoTExPx008/wcjICL169VKUP3r0CDdu3FA5BT5w4EAA76+DFEIoyhcvXoy7d++iR48eMDU1VZQ/e/YMjRs3xqVLlzB//nwMHz78I60hERERke7R+jWX4eHhiIiIgIuLC9q0aQM9vfzJXw0MDLB06VIEBgbCz89P6fGP0dHRmDNnDlxdXRX1J06ciJUrV2LFihXo06ePorx3795Yv3491q5di6ioKPj7++P27dvYsmULSpcujenTpystt0OHDoiIiECFChXw7NkzTJ06VSW2UaNGwcbG5uOsOBEREVE+kpRchoSE4LfffsNvv/2G+vXrK8pHjBiBP/74Q/G6UaNG2Lt3L/T19fMeqQQNGjTA8ePHERwcjPXr1yM1NRUeHh6YNWsWunTpolEbenp62L59O2bOnInVq1dj3rx5sLOzQ//+/TF9+nSl54oDwL179wAAN27cwLRp09S22adPHyaXREREVChJSi43bdqEO3fuoHbt2oqyc+fO4ffff4epqSkCAwNx7tw5HD58GOvWrUOPHj20FnBueXl5Ye/evTnWCwkJQUhIiNppxsbGCA4ORnBwcI7tyJNLIiIios+RpHPWV69ehYeHh9JwOevWrYNMJsPq1auxZcsW/PvvvzAxMcHy5cu1FiwRERER6TZJyWVCQgJKliypVHb06FFYWVmhXbt2AN4/YtHX1xe3b9/Oc5BEREREVDBISi5TU1ORnp6ueJ2SkoJLly6hXr16SjfwODg44MmTJ3mPkoiIiIgKBEnJpZOTE65du6Z4HR4ejtTUVNSrV0+pXlJSUpZPryEiIiKiwkdSchkQEICbN29i5syZuHTpEoKDgyGTydCsWTOlelevXlU5fU5EREREhZek5HLSpEmwsLDAt99+i5o1a+LMmTNo3LgxPD09FXVu3bqFqKgoeHt7ay1YIiIiItJtkoYiKlu2LE6ePIm5c+fiyZMn8PLywtdff61U5/Dhw6hWrRpatmyplUCJ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4H+/aXNq99mzZwAAFxcXlWlCCKxZsybn4DXUvHlzAMCOHTvw9u1brbWbHScnJ1StWhXJycmK6yg10axZMwDvrxN9+PChRvNo+h7u27cPjx8/Vpl+8eJFREREQE9PT+XGIyKizJhcElGOSpYsCQC4du2a2ulfffUVDAwMsG3bNvz9999K0w4cOIDFixcDAMaNG5erditWrAjg/d3g8pt3ACA9PR1TpkxRusklr2rUqIGOHTsiJiYGHTp0UPsc9VevXuGff/5Rm3xJNX36dABA3759sXPnTpXpQgicOXMGBw4cUJRVr14dbdu2xZs3b9C2bVvFE4Dk0tLSsGPHDqWynPq6fv36qFOnDt68eYNBgwbh9evXimnx8fEYNGgQAKBr166KI65ERGrl5zhIRFQwLFy4UAAQFhYWokOHDqJ///6if//+4saNG4o6ixcvFnp6egKAqFmzpujevbvw8fERMplMABBTp05VaTcuLk6Ym5sLAMLHx0f06dNH9O/fXyxfvlwI8X68Rk9PT8WyW7ZsKTp37ixcXFyEoaGhmDBhggAg/P39ldqVOs5lUlKSaNSokQAgjIyMRO3atUXnzp1Fp06dRO3atRXjdV6/fl1pvryMcymEEPPnzxcGBgYCgChbtqxo2bKl6N69u2jSpIkoWrSoACAmTJigNM+zZ8+Et7e3ItaAgADRvXt30bBhQ+Hg4KCyvJz6Wggh7ty5o1iXokWLiqCgING2bVthZWWleF+fPXum1K58nMsP3wMi+nwxuSSiHKWnp4sZM2aIypUrCxMTE0WyFBoaqlTv9OnTIigoSDg6OgoDAwNRpEgR0bJlS5XB0zM7evSoaNy4sbC1tVUkp71791ZMT05OFpMmTRLly5cXJiYmomjRoqJdu3bi3LlzWSY2UpNL+bquWbNGtGjRQhQrVkwYGhqKIkWKiCpVqoi+ffuKrVu3infv3inNk9fkUgghrly5IgYOHCjc3d2FiYmJMDMzE25ubiIwMFD89ttvIjY2VmWelJQUsWjRIuHr6ytsbGyEkZGRKFmypGjSpIn4/fffVern1NdCCJGQkCAmTpwoKlasqIijRo0aYubMmeL169cqbTK5JKIPyYTQwm2WRERERETgNZdEREREpEVMLomIiIhIa5hcEhEREZHWMLkkIiIiIq1hcklEREREWsPkkoiIiIi0hsklEREREWkNk0siIiIi0homl0RERESkNUwuiYiIiEhrmFwSERERkdYwuSQiIiIirWFySURERERa83/vrkylovv27AAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -537,7 +542,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "04fe6860", "metadata": {}, "outputs": [ @@ -623,7 +628,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "21596f1e", "metadata": {}, "outputs": [ @@ -674,7 +679,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "id": "77e62a5d", "metadata": {}, "outputs": [ @@ -748,6 +753,537 @@ "and ligand transitions in `[E]` to hunt for a two-lobed `ε(E)`." ] }, + { + "cell_type": "markdown", + "id": "411379d5", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 9. From curves to mechanism: the basis-curve decomposition\n", + "\n", + "Section 8 showed that logit epistasis separates into an *ensemble* (redistribution)\n", + "part and a *within-state* (direct) part. We can make that exact and turn it into a\n", + "lookup table. Two facts do all the work:\n", + "\n", + "**(a) A single genotype's curve can never peak.** Because\n", + "$\\operatorname{logit}\\theta = -g_{HD} + \\ln[L_\\text{free}] + \\text{const}$ and free\n", + "$[L]$ is *strictly monotone-decreasing* in effector (mass balance), every single\n", + "genotype's $\\theta$ and $\\operatorname{logit}\\theta$ curve is monotone. **All peaks\n", + "live in the epistasis second difference** — exactly where you see them in the real\n", + "data.\n", + "\n", + "**(b) Additive epistasis is a combination of three basis curves.** Using\n", + "$\\operatorname{Cov}_p(a,b)=\\sum_{j" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from tfscreen.simulate.toy_thermo import (\n", + " basis_curves, epistasis_coeffs, predict_epistasis, exact_epistasis,\n", + " classify_shape, plot_basis, plot_epistasis_decomposition,\n", + ")\n", + "\n", + "# The basis curves depend only on the *backdrop* (the wt ensemble), not on any\n", + "# mutation. Compare a mixed apo state (ln_K_conf=0) with an H-dominant one (+4).\n", + "backdrops = {\n", + " \"mixed (ln_K_conf=0)\": ThermoModel(0.0, 18.0, 14.0, 1e-6, 1e-9),\n", + " \"H-dominant (ln_K_conf=+4)\": ThermoModel(4.0, 18.0, 14.0, 1e-6, 1e-9),\n", + "}\n", + "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", + "for ax, (label, mdl) in zip(axes, backdrops.items()):\n", + " plot_basis(mdl, effector, ax=ax)\n", + " ax.set_title(label)\n", + "fig.suptitle(\"p_L*p_LE and p_H*p_LE (the peak generators) turn on then off at the \"\n", + " \"ligand transition\")\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "3fcd7882", + "metadata": {}, + "source": [ + "### The peak recipe\n", + "\n", + "A double-mutant epistasis curve **peaks** iff it puts weight on a $p_\\cdot p_{LE}$\n", + "basis curve — i.e. **one mutation contrasts an occupied apo state ($L$ or $H$)\n", + "against $LE$, and its partner contrasts the same pair.** Then:\n", + "\n", + "- **peak location** = the ligand transition midpoint (where $p_{LE}\\!\\approx\\!0.5$).\n", + " To leading order this is the *wt* transition (set by `ln_K_eff`/`protein_total`),\n", + " because the basis curves are the wt ensemble occupancies. It is **not** strictly\n", + " mutation-independent, though: a mutation that perturbs $g_{LE}$ shifts *its own*\n", + " genotype's transition ($\\ln K_{eff}\\!\\to\\!\\ln K_{eff}-\\Delta g_{LE}$), so the exact\n", + " peak is a blend across the wt/A/B/AB quartet — it drifts toward the $LE$-perturbed\n", + " members as $|\\Delta g_{LE}|$ grows, and eventually splits into two lobes\n", + " (*biphasic*). The non-$LE$ partner sets amplitude/sign, not location.\n", + "- **peak sign** = $-(\\Delta_A)(\\Delta_B)$ across that state pair.\n", + "\n", + "`epistasis_coeffs` reads the coefficients straight off the two ddG vectors, and\n", + "`plot_epistasis_decomposition` overlays the exact curve, the second-order\n", + "prediction, and each contribution — so you can see which basis curve makes the\n", + "peak and how far second order drifts (the closed form is exact only in the\n", + "no-depletion limit; here it runs ~15 % high in magnitude but keeps the shape)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8f43d8e1", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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OmNJRERERERERETqRGcsiYiIiIiIiIhInWhiSURERERERERE6kQTSyIiIiIiIiIiUieaWBIRERERERERkTrRxNIFori4mLvuuouMjAzi4uLo2bMnCxcubOphRZzs7Gwsy6rx9dhjjzX10CLOvn37mDBhAikpKcTFxdGnTx8OHjzY1MOKKLNmzapxLPXu3buphxXx7r77bizL4qmnnmrqoUSc3/zmN1x++eXEx8eTnJzMtddey/r165t6WBHn4Ycfpnfv3iQkJNCqVSsmTJjAnj17mnpYcoFQTWWf6ir7VFedneqqc6ea6sxUV9nTEHWVJpYuED/84Q/Jzc1lwYIFbN26lQkTJjBx4kS2bNnS1EOLKBs2bODw4cPhr/nz5wMwbty4Jh5ZZDl27BgDBgwgOTmZ119/nS1btjBjxgx91GctunfvXu2YWr58eVMPKaItXryYd955h4yMjKYeSkTKysriiSeeYPPmzaxbt47OnTszYsQIjh071tRDiyirV6/mu9/9LuvXr2fZsmUUFBQwcuRI/H5/Uw9NLgCqqexTXWWP6ir7VFfZp5rq7FRX2dMQdZVljDGNOEY5T7p168bXv/51fvrTn4aXtWjRgl//+tdMmTKl6QYW4W6//XYOHDjA6tWrm3ooEeW+++5j/fr1yuUsZs2axeLFi9m4cWNTD6VZOHLkCL1792bJkiXceOON3HvvvXznO99p6mFFtKKiIpKSksjNzWXw4MFNPZyItXv3bjp27MjmzZu58sorm3o40syppqo71VW1U11lj+oq+1RT1Y3qKnvqUlfpjKV6eP755/n2t79N7969iYqKwrIs5s2bd8ZtNmzYwKhRo0hOTiYuLo5rrrmGBQsW1Hss/fr14z//+Q95eXkYY3jppZeoqKiIiF+YSMrpVIWFhbz88stMnTq1Qfutq0jKadGiRVx11VWMHz+e9PR0+vTpw7///e9699sQIikngG3bttGmTRs6d+7M1KlTycvLa5B+G0KkZTV16lS+973vccUVVzRIfw0l0nKqUllZyTPPPENKSkpEZBapOUHo9RwgNTW1wfuW8yOSjq9IrqkgsrI6leqq01NdZV+k1lWRllOk1lQQeVlVUV1lX53qKiN1lpWVZQCTlpYW/v4vf/nLadu/+eabxu12m4SEBPM///M/5kc/+lF4u8cff7xeY/F6veZrX/uaAYzL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l58DERERqZ3qKtVVqqtEvqCJJZGLRCAQMA8//LC5/PLLTXR0dPgP+qpVq6q1e/fdd80tt9xiWrdubVwul2nRooUZPXq0WbFixWn7XrNmjRk2bJhJSUkJF1CTJ08Ory8uLjYPPPCAufTSS010dLRJT083N910k9m4ceNp//jWtQCq2tf58+ebUaNGmVatWhm3221atGhhunXrZqZOnWpefvllU1lZWW2b+hZAxhizdetW861vfcvk5OSY6OhoExsbazp27GhGjBhhfv/735uDBw/W2KaiosLMmTPHDBw40CQnJxuPx2Patm1rhg8fbv7whz/UaH+2rI0x5tixY+b+++83Xbt2DY+jZ8+e5le/+pUpKyur0acKIBERkXOjukp1leoqkS9YxjTARwaIiIiIiIiIiMhFR/dYEhERERERERGROtHEkoiIiIiIiIiI1IkmlkREREREREREpE40sSQiIiIiIiIiInWiiSUREREREREREakTTSyJiIiIiIiIiEidaGJJRERERERERETqRBNLIiIiIiIiIiJSJ5pYEhERERERERGROtHEkoiIiIiIiIiI1IkmlkREREREREREpE40sSQiIiIiIiIiInXy/wHcWDYd8gsblwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "mdl = backdrops[\"mixed (ln_K_conf=0)\"]\n", + "\n", + "# Four mechanisms, one per panel. (ddg_A, ddg_B, epi)\n", + "demo = [\n", + " (\"cross g_H x g_LE (redistribution -> PEAK)\", {\"H\": 2.0}, {\"LE\": 2.0}, None),\n", + " (\"same g_LE x g_LE (redistribution -> dip)\", {\"LE\": 2.0}, {\"LE\": 2.0}, None),\n", + " (\"direct e on LE (within-state -> step)\", {}, {}, {\"LE\": 2.0}),\n", + " (\"cross + e_HD offset (peak masked by flat)\", {\"H\": 2.0}, {\"LE\": 2.0}, {\"HD\": 2.0}),\n", + "]\n", + "fig, axes = plt.subplots(2, 2, figsize=(12, 8))\n", + "for ax, (title, a, b, epi) in zip(axes.ravel(), demo):\n", + " plot_epistasis_decomposition(mdl, effector, a, b, epi=epi, ax=ax)\n", + " ax.set_title(title + f\"\\n(exact shape: {classify_shape(exact_epistasis(mdl, effector, a, b, epi))})\")\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "add89f65", + "metadata": {}, + "source": [ + "### The screen: mechanism → shape, without enumerating billions\n", + "\n", + "The naive double-mutant space is enormous ($\\sim3.5\\times10^9$), but the shape of\n", + "the epistasis curve is fixed by (i) which **basis curve** the pair activates and\n", + "(ii) the **backdrop** (which apo state is populated). So we scan a handful of\n", + "mechanistic *channels* across a few backdrops — a couple dozen curves — and compare\n", + "each channel's noise-free **intrinsic** shape with the shape `cat_response`\n", + "**observes** once a realistic measurement noise floor is added (the same classifier\n", + "you run on real data). The genuine `build_titration_df → extract_epistasis`\n", + "pipeline, and the logit-error caveat, follow below." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d1dfabbe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INTRINSIC shape (noise-free ground truth)\n", + "\n", + "backdrop H_dom(+4) L_dom(-4) mixed(0)\n", + "genotype \n", + "redist_cross_H_LE peak flat peak\n", + "redist_cross_L_LE flat peak peak\n", + "redist_cross_H_L step step step\n", + "redist_same_LE dip dip dip\n", + "redist_same_H dip step step\n", + "direct_e_LE step step step\n", + "direct_e_H step step step\n", + "direct_e_HD flat flat flat\n", + "\n", + "cat_response OBSERVED shape (select_by='aicc', noise sigma=0.05)\n", + "\n", + "backdrop H_dom(+4) L_dom(-4) mixed(0)\n", + "genotype \n", + "redist_cross_H_LE peak flat peak\n", + "redist_cross_L_LE flat peak peak\n", + "redist_cross_H_L linear step step\n", + "redist_same_LE dip dip dip\n", + "redist_same_H dip flat dip\n", + "direct_e_LE step step step\n", + "direct_e_H step linear step\n", + "direct_e_HD flat flat flat\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "from tfscreen.analysis.cat_response.cat_response import cat_response\n", + "\n", + "# Mechanistic channels: (name, ddg_A, ddg_B, epi). Names encode the mechanism.\n", + "channels = [\n", + " (\"redist_cross_H_LE\", {\"H\": 2.0}, {\"LE\": 2.0}, None),\n", + " (\"redist_cross_L_LE\", {\"L\": 2.0}, {\"LE\": 2.0}, None),\n", + " (\"redist_cross_H_L\", {\"H\": 2.0}, {\"L\": 2.0}, None),\n", + " (\"redist_same_LE\", {\"LE\": 2.0}, {\"LE\": 2.0}, None),\n", + " (\"redist_same_H\", {\"H\": 2.0}, {\"H\": 2.0}, None),\n", + " (\"direct_e_LE\", {}, {}, {\"LE\": 2.0}),\n", + " (\"direct_e_H\", {}, {}, {\"H\": 2.0}),\n", + " (\"direct_e_HD\", {}, {}, {\"HD\": 2.0}),\n", + "]\n", + "screen_backdrops = {\"L_dom(-4)\": -4.0, \"mixed(0)\": 0.0, \"H_dom(+4)\": 4.0}\n", + "\n", + "\n", + "def intrinsic_shape(y, tol=1e-2):\n", + " \"\"\"Ground-truth label of a noise-free epistasis curve: flat/step/peak/dip.\n", + "\n", + " Judged by the *span* (E-dependence), so a constant offset -- e.g. a within-HD\n", + " interaction -- reads 'flat' regardless of its level.\n", + " \"\"\"\n", + " y = np.asarray(y, dtype=float)\n", + " if float(y.max() - y.min()) < tol:\n", + " return \"flat\" # no E-dependence, any offset\n", + " yc = y - 0.5 * (y[0] + y[-1]) # center on the endpoints\n", + " i = int(np.argmax(np.abs(yc)))\n", + " interior = 3 <= i <= len(y) - 4 and abs(yc[i]) > 1.3 * max(abs(yc[0]), abs(yc[-1]))\n", + " if not interior:\n", + " return \"step\"\n", + " return \"peak\" if yc[i] > 0 else \"dip\"\n", + "\n", + "\n", + "SIGMA = 0.05 # realistic measurement noise\n", + "rng = np.random.default_rng(0)\n", + "truth_rows, obs_rows = [], []\n", + "for bname, lnc in screen_backdrops.items():\n", + " mdl_b = ThermoModel(lnc, 18.0, 14.0, 1e-6, 1e-9)\n", + " for cname, a, b, epi in channels:\n", + " eps = exact_epistasis(mdl_b, effector, a, b, epi)\n", + " truth_rows.append({\"genotype\": cname, \"backdrop\": bname,\n", + " \"shape_true\": intrinsic_shape(eps)})\n", + " obs_rows.append(pd.DataFrame({\n", + " \"genotype\": cname, \"backdrop\": bname, \"titrant_conc\": effector,\n", + " \"ep_obs\": eps + rng.normal(0.0, SIGMA, effector.size), \"ep_std\": SIGMA}))\n", + "\n", + "# Ground truth: mechanism -> intrinsic shape (noise-free, deterministic).\n", + "truth = (pd.DataFrame(truth_rows)\n", + " .pivot(index=\"genotype\", columns=\"backdrop\", values=\"shape_true\")\n", + " .reindex([c[0] for c in channels]))\n", + "\n", + "# Observed: what cat_response reliably calls each curve at SIGMA noise. We use the\n", + "# conservative 'aicc' selector (penalizes extra peak params) so the table reflects\n", + "# shapes you can trust; 'shape' mode is the liberal, exploratory alternative and\n", + "# will over-call structure on low-amplitude curves.\n", + "obs_res, _, _, _ = cat_response(\n", + " pd.concat(obs_rows, ignore_index=True), x_obs=\"titrant_conc\",\n", + " y_obs=\"ep_obs\", y_std=\"ep_std\", group_by=[\"backdrop\"],\n", + " select_by=\"aicc\", progress=False)\n", + "observed = (obs_res.pivot(index=\"genotype\", columns=\"backdrop\", values=\"shape\")\n", + " .reindex([c[0] for c in channels]))\n", + "\n", + "print(\"INTRINSIC shape (noise-free ground truth)\\n\")\n", + "print(truth.to_string())\n", + "print(f\"\\ncat_response OBSERVED shape (select_by='aicc', noise sigma={SIGMA})\\n\")\n", + "print(observed.to_string())" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "7a1cb61f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "median ep_std (logit-propagated): 1.34 <- why the screen above uses clean fixed-error curves for shape calls\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAr0AAAF6CAYAAAAUIeBJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAl4tJREFUeJzs3XdYU9cbB/BvEvYIyJY9HDgQRVEUVBAVV92z1tlfl23VVmvraLVDrVZbW2ttrVWx7lVxIyLgBFHciiAKgsjeyEzO7w+a1JgAIQmE8X6eJ49y78m97z25uXlzcu45HMYYAyGEEEIIIc0YV90BEEIIIYQQUt8o6SWEEEIIIc0eJb2EEEIIIaTZo6SXEEIIIYQ0e5T0EkIIIYSQZo+SXkIIIYQQ0uxR0ksIIYQQQpo9SnoJIYQQQkizR0kvIYQQQghp9ijprcaKFSvA4XAQHh6u7lAUkpiYCA6Hg5kzZzbofsPDw8HhcLBixQqJ5b6+vuBwOA0aiyJ27NgBDoeDHTt21Ns+Zs6cCQ6Hg8TERPEyRV4vWdtRlabwenE4HPj6+qo7jDqr6Rz75Zdf0KlTJ+jp6YHD4WDDhg0NHh9p2urzulBf6vu6e/PmTfB4POzZs6detq+o6j4vW0ocirzu586dA4fDwalTpxTaZ70kvbdu3cL777+Pjh07gs/nQ0tLC1ZWVhg0aBDWr1+PzMzM+tgtIUROinypa4gvBK9qCom3Ku3btw/z5s2DtrY25s2bh+XLl8PLy0vdYTW4xvplpikmk3WlrsaS+vbpp5/C1dUVkydPVncoRA41XQMGDhwIHx8fLFq0CAKBoM7b1lAyNglCoRCLFi3C+vXrwePx0K9fPwwePBj6+vrIyMjA1atXsXDhQixfvhyPHj2CjY2NKnevUh999BEmT54Me3t7dYeiEBsbGzx8+BBGRkbqDgUAsHPnTrx8+VLdYTQKq1evxhdffNGoz/+m8Ho9fPgQenp66g5DZU6cOCH+19raWs3RkKaqKVxfXjdmzBh4eXmhdevWKt/2+fPnER4ejr/++gtcLv243Zgo+rovWrQII0eOxL59+zB16tQ6PVelSe/SpUuxfv16eHh4YP/+/WjTpo1UmZiYGHz++ecoKSlR5a5VzszMDGZmZuoOQ2GamppwdXVVdxhiTfXLQ31o3bp1vVzcVakpvF6N6fxWhdTUVABQWcKbmJgIHR0dWFlZqWR7pGloCteX1xkZGdVbA83mzZuhq6uL8ePH18v2ieIUfd2HDBkCMzMz/P7773VOelX2tScuLg4//PADzM3NcebMGZkJLwB4eHggJCQEjo6OUuvu3LmDyZMno3Xr1tDS0oKDgwM+/vhjZGdnS5R79SeYx48fY8yYMWjVqhX09fUxcOBA3L59W2rbNTWXOzo6SsUj6+dfRfYLABkZGfjkk0/Qpk0baGtrw8zMDOPGjcO9e/dklpdF9NPakydPsHbtWrRt2xY6OjpwcnLCN998g4qKimrr6FWin4xLS0vxxRdfwN7eHjo6OujQoQM2btwIxpjM/QcFBcHf3x+tWrWCjo4OOnfujHXr1sn984Ksn6pf/bn87Nmz6NOnD/T09GBqaooZM2ZIve4i8p4nygoKCkLPnj2hp6cHc3NzzJ49G+np6RJlavs5UNZ5V9efSe/fv48RI0bA0NAQRkZGGDZsWJ3Ondf5+vri66+/BgD4+fmBw+GAw+FIvAdef71mzpyJWbNmAQBmzZolfs6rZV49t5YtWwYXFxdoamqK+4nFxcVh0aJF8PDwgKmpKXR0dNCuXTt88cUXKCoqkoiRw+EgIiJC/H/R49V6ru49nZWVhfnz58PJyQna2tqwsLDAxIkTZdaZ6LV4+vQpfvnlF7i6ukJbWxsODg74+uuvIRQK61S3r8rJycH7778PS0tL6OnpwdPTE//8849UOdH7ICwsTOp4lREeHg5bW1sMGTIEe/bsUXnLvTzvw+zsbNja2sLQ0BCPHz+WeP7r60T9CgEgIiJCoh5EXWpevWYcP34c3t7eMDQ0FJ+75eXl2LhxIwICAmBnZyd+/ceOHYubN29WeyxBQUEYPHiw+Lx0dHTEtGnTxOeMo6MjAgMDAQBOTk7iuBTthqHIZ939+/cxfPhwGBsbw8DAAIMHD8aNGzektl3d9eXw4cPo378/LCwsoKOjA2trawwcOBCHDx8W162TkxMAIDAwUKL+RZ+Dqamp4m43FhYW0NbWhqOjI+bMmYOMjAypWPLz8/HVV1+hY8eOMDAwAJ/PR5s2bTBjxgwkJSWJy1XXdSomJgbjx4+Hvb09tLW1YW5uDk9PT6xcuVKues7NzUVQUBACAgLA5/Ol1oeFhWHo0KGwtraGtrY2LC0t0bdvX2zZskWq7JMnT/Duu+9KXFd8fX0lYlb0/JNFFXmDKty7dw8TJ04Uv95OTk6YP39+tZ+3ERER6NevH/T19WFqaopJkyYhOTm51hwAgFzXAKCqUW/06NG4dOmS1HWlNipr6Q0MDIRAIMB7770Hc3Pz2nesIbnrY8eOYeLEieByuRg1ahTs7Ozw4MED/PrrrwgODkZUVBRatWol8ZzExER4eXmhU6dOmD17NhISEhAUFAQ/Pz88fPgQlpaWqjo8hfebkJAAX19fpKSkYPDgwRg9ejQyMjJw+PBhBAcHIzQ0FL169ZJ73/Pnz8fly5cxceJEGBgY4Pjx41i+fDnu3LmDQ4cOyb2diRMn4ubNmxg3bhyAqgvi3LlzkZiYiPXr10uUXbx4Mb7//nvY2Nhg7NixMDIywsWLF/HZZ58hKioKBw8elHu/shw7dgwnT57EG2+8gT59+uDChQvYuXMnEhIScOnSJamydTlPZs6cicDAQGzfvr1O/dREr8/48eMxcOBAREZGYvv27bh48SKuXbsmdS7Wl3v37sHb2xtFRUUYO3Ys2rZti2vXrsHb2xvu7u4KbVNUDxEREZgxY4Y4YTA2Nq72OaNHj0ZeXh6CgoIwatQodO3atdqy48aNw+3btzFkyBAYGxuLP0iPHDmCv/76C35+fvD19YVQKERkZCTWrFmDiIgIXLhwAZqamgCA5cuXY8eOHUhKSsLy5cvF265pvwCQmZmJ3r17i993kydPxtOnT3Ho0CGcPHkSwcHB8PHxkXreZ599hoiICIwYMQIBAQE4evQoVqxYgfLycrk/YF/18uVL+Pr64u7du+jduzf69++P5ORkTJo0CYMHD5Yo27Vr12qPVxl9+vTBpEmTcPToUQQHB8PQ0BDjxo3DtGnTxF92FCXv+9DU1BQ7d+7EoEGD8Oabb+Ly5cvi1/jtt9/G8+fPsWPHDrRp0wYaGhpYvnw5vv76azg4OEi8X19/3Q8ePIizZ89ixIgRmDNnDgoKCgBUfdGYP38++vbti2HDhqFVq1Z48uQJjh07htOnT+PChQvw9PSU2NaCBQvw448/wsTEBKNHj4aFhQWSk5Nx7tw5dO/eHZ07d8b8+fOxY8cO3L59G/PmzRO/V2Q13qiq7l715MkTeHt7w8PDAx988AGSkpJw8OBB9OvXD+fPn6/1M2Tz5s2YM2cOWrdujTFjxsDU1BRpaWm4du0a/vnnH4wbNw5du3bFvHnz8PPPP8Pd3R2jR48WP190nBcuXMD69evh7++PXr16QVNTEzdv3sTmzZsRHByMmJgYccsdYwwBAQGIioqCt7c3hgwZAi6Xi6SkJBw7dgzTpk2Dg4NDtTHfunULffr0AY/Hw6hRo+Dg4IC8vDw8ePAAW7ZswdKlS2ut6wsXLqCiokJm33jRZ46xsTFGjRqF1q1bIzMzE7dv38bff/+Nd999V1z20qVLGD58OAoLCxEQEIDJkycjNzcXN2/exM8//yw+VxU5/2RRdd6gqEuXLiEgIADl5eUYP348HB0dcfXqVfz88884ceIEIiMjJX4RP3v2LIYPHw4ej4dJkybB2toaYWFh8PHxkesz09HRUe5rQO/evbF161acP3++2kZWmZiK+Pn5MQAsNDS0zs/NyspifD6f2djYsMTERIl1e/fuZQDYRx99JF729OlTBoABYN9//71E+WXLljEAbPXq1RLLAbD+/fvL3L+DgwNzcHCQWLZ8+XIGgIWFhSm13z59+jAej8fOnDkjsfzRo0fM0NCQubm5yYzpdTNmzGAAmLm5OUtOThYvLysrY/369WMA2KFDh6RinTFjhsR2+vfvzwCw9u3bs7y8PPHyvLw81r59e8bhcFh0dLR4+dmzZxkAFhAQwIqKisTLhUIhe//996X2GxYWxgCw5cuXy9zvq7Zv384AMA0NDXbp0iXx8srKSubr68sAsKtXr4qX1/U8ebXetm/f/nqVyiSKCYDUa/bFF19Uey6+Xs8iss47UUxPnz6tdTuietu1a5fE8sWLF4vjfHU78pJ1fsva76tEdVNdXYqe07VrV5adnS21PiUlhZWVlUkt//rrr2Ueo6wYXiWrbmfNmsUAsMWLF0ssP3nyJAPA2rRpwwQCgXi56LVwcnJiqamp4uWZmZnM2NiYGRoayoy5NqL6feeddySWnzlzRvy6vV6PtR2vogoLC1lgYCAbNGgQ4/F4DACzs7NjixcvZg8ePKjz9hR5H4reO4sWLWKMMbZp0yYGgE2ZMkVq+zVdq0XnIJfLZSEhIVLrS0tLWUpKitTye/fuMQMDAzZw4ECJ5cePH2cAmJubG8vKypJYV1FRwdLS0sR/y3rf1pUyn3VffPGFRHnRufT6Z4isOD08PJiWlhZLT0+XGdPr+6vuepaens4KCwullgcGBjIA7LvvvhMvu3PnDgPARo8eLVW+tLRUYjuyri2ffvopA8COHj1aY8w1+eyzzxgAmefK2LFjGQB269atGrdfWlrKbGxsGJfLZadPn5Yq++rncV3Pv+o+L+uaN4SFhbHly5fL/Xj92iMrDoFAwFxcXGR+Forqdfbs2eJllZWVzMHBgXE4HHbx4kWJ8tOnTxefx6+q7jOlpmuAyO3btxkANn369BrLvU5lV9gOHTowAOzhw4dS62S9IK9+2P74448MANu5c6fMbXt4eDAzMzPx36I3ppOTk8QH2Kvrxo4dK7FclUmvvPuNiYmROjFeJXpT3717V+b6V4kuZK9eVEQuXrzIALARI0ZIxSNvEsUYY3///bfURXfkyJEMAEtKSpIqn5eXxzgcDhs3bpx4mSJJr6yTVrTul19+ES+r63nCGGOpqans4cOHEgl+TUT7ff3ixFhV8mBsbMz4fL749a/PpDcpKYkBYF26dKk2lsaY9AYFBdUpluzsbAaAzZw5s9YYXvV63ZaVlTEdHR1mamrKiouLpcoPGjSIAWAXLlwQLxO9Ftu2bZMqL1p3586dOh0PY4w5OTkxLS0t9uLFC6l1/v7+DZr0vio1NZWtW7eOde3aVfwh1KNHD/bzzz+zjIwMubahyPuwvLyceXp6Mi6Xy3755Remq6vLHB0dWX5+vtTz5Ul6x4wZI1esr3rjjTeYlpYWKy8vFy8bOnQoA8DOnz9f6/NVkfQq+llnbGwsM9kUnUvXr1+vMU4PDw+mr6/PcnJyaoyvtutZdYRCIePz+czX11e8TJT0yvpi87qakt7g4OA6xfKqKVOmVPseFiW9jx49qnEb+/fvVyi5ep2s80/W56UieYPoei7v4/X3l6w4Lly4wACwoUOHSsVQWFjITExMmI6OjrhRIDw8nAFgI0eOlCr/7Nkz8RfuVymT9KalpTEAbMCAATWWe51Kb2SrTnh4uLgP4atE/aEiIyMBAFFRUUhISJAqV1paiqysLGRlZUk0pXft2lXqbkxbW1sAQF5enoqilybvfkXHlZ6eLnP8u9jYWPG/nTt3lmvfffv2lVrWu3dvaGho1KnPkKztiJa9up3IyEjo6+tj27ZtMrejq6srPg5Fde/eXWpZTfVZl/NE0Zs6ZNWPgYEBunbtivDwcDx58qRuP6koQNRHXNbP8a/G0tj07NlT5nLGGLZv344dO3bg3r17yM/Pl+gzK7qRS1GxsbEoLS2Fn5+fzFEd/Pz8EBISglu3bkm9vvKeg/IoKCjA06dP0bFjR5k3kfXt2xehoaF12qYsR48exa1btySW+fr61tjPtHXr1liwYAEWLFiABw8e4O+//8bvv/+OefPmYfv27XJdQxR5H2pqamLv3r3o2rUr5s6dCx6Ph927d8vsZymP6s4xoOpn8bVr1+LSpUtIS0uTut8hKytLfE24du0atLW10b9/f4XiqCtFP+u6desGAwMDqfKic+nmzZsyz2GRyZMnY9GiRejcuTPefPNN+Pn5wcfHR6H6P3LkCP744w/ExMQgNzdX4r6OV9/DHTp0QJcuXbB3716kpKRg9OjR8PX1lfn5KcvEiROxYcMGjBkzBpMmTcKgQYPQr1+/Oo1KIep3Kqvr1uTJk3HkyBF4eXnhzTffhL+/P/r27St1A/u1a9cAQKpbUnXqcv7JokjesGLFCpWPsSu6Fsi6nhgYGKBHjx44e/YsHj16BDc3txo/r+zs7GBvb4+nT5+qLD4TExMAVfVZFypLei0tLfHw4UOkpqZK3VX96guyb98+TJkyRWJ9Tk4OAGDTpk017qO4uFjihJT1hhX1FVZk/DZ5ybtf0XGdPHkSJ0+erHZ7xcXFcu9bVj9lHo8HU1NT5OfnK7Ud0bJXt5OTk4PKykqZX1pE6hK/LHWtz7qeJ4qorj+4rDqqL6J9WFhY1BhLY1NdXHPnzsWvv/4KOzs7jBw5Eq1bt4a2tjYA4Ouvv0ZZWZlS+xX166xu/6IPGlG5V6nyWiLafn2/bkePHhXfXPUqeW6uev78OU6dOoUzZ84gLy8PHA4HnTp1kmu/ir4PnZ2d4e7ujsuXL6N79+7o06ePXPuTpbo6vHLlCgYMGACgKklp27YtDAwMwOFwcPToUdy+fVviPMvPz4eNjU2DDWWlaN0pez1auHAhTE1NsXnzZqxfvx7r1q2DhoYGhg8fjp9++knc774269evx8KFC2Fubo7BgwfD1tYWurq6AIANGzZI1K2GhgbOnz+PFStW4PDhw1iwYAEAwNzcHB999BGWLl0KHo9X7b569eqF8PBwrFq1Cnv27MH27dsBAJ6enlizZg38/PxqjVcUW2lpqdS6CRMm4OjRo/jxxx/x+++/Y9OmTeBwOPDz88P69evFfUhFdStPsl3X80+W+sgbFFHX66k81z1VJr2iEcDqOmylypLePn36IDw8HGFhYeIXXV6iD5y7d+/K3eJZVxwOB5WVlTLX5efn18twKaLj2rhxIz766COVbDM9PR3t27eXWCYQCJCdnV2nD9P09HSpYalEIxO8Whd8Ph8cDqfO36bqQ0OcJyKvj9Lw+nJRHYk+LGWdW8omxqJ9yLoruqYY1U3WDVIZGRnYtGkTunTpgqtXr0pcqNLS0mr8UiUv0flRXb2kpaVJlKsvou3X9+u2Y8eOOk0UUlBQgMOHD2PXrl0IDw+HUChEu3bt8O233+Ktt96S+6YsRd+HP/74Iy5fvgxTU1Ncu3YNv/32G+bMmSP3819V3U14K1euRFlZGS5evCjV4hQZGSk1wo6xsTHS0tIgFAobJPFVtO7kvR5Vh8PhYPbs2Zg9ezays7Nx8eJF7N27FwcOHEB8fDzu3LlTYwIKVF3jvv32W7Ru3Rq3bt2SSG4YY1i7dq3Uc0xNTbFx40b88ssviI2Nxfnz57Fx40YsX74cmpqaWLx4cY377Nu3L06fPo2SkhJERUXh+PHj+O233zB8+HDcu3cPzs7ONT5fdFO9KJF83ahRozBq1CgUFhbi8uXL4ptthwwZgtjYWBgbG4tbiZ8/f17jvoC6n3+yKJI3hIeH1+lXP0dHx1pv7K7r9bShrnsiotdUnoETXqWyd/mMGTPA5XKxZcuWOidIorsQr169qqpwpLRq1UrmSZuYmFhvXSHq47guXrwotezq1auorKxEt27dlNqOaNmr2+nVqxeys7MRHx+vQLSq1RDniYis+ikqKsKtW7fA5/PFF9uaLoh1HaLmdaLRGV4fweLVWBQl+oCrSyumIs8RefLkCRhjGDhwoNQ3c1l1rcj+XF1doaOjg+joaJnDc4k+FGobAUJZfD4fTk5OePz4sfiD4VXVHW99KC8vR1BQECZOnAhLS0vMnj0bt2/fxvvvv4/IyEg8evQIy5Ytq9MoBIq8D2/evIklS5agffv2uHv3LpycnLBw4ULcv39fqiyXy1X4l7qEhASYmJhIJRwvX75ETEyMVPmePXuirKxMPDxeTZQ5/0UUvYbdvHlTalg/QPY1uzampqYYPXo09u/fjwEDBuDBgwfiYZ9qOsasrCzk5+ejd+/eUq15169fr3HsfQ6Hgw4dOuDDDz9ESEgIgKpRLOSlq6sLX19frF+/HkuWLEFJSYl4OzVxc3MDADx69KjGcoaGhhgyZAi2bNmCmTNnIj09HVFRUQD+60pz9uzZWvdX1/NPFkXOEVEXUnkf8nxZFp1TspLp4uJiXL9+Hbq6uuJGONHn1eXLl6XKp6Sk4NmzZ3IfjzzXANFrKnqN5d52nUrXoF27dli0aBEyMjIwdOjQasdOk5Vgzpo1C4aGhli6dKnMi+DLly/F/VwU5enpicTERImLW3l5OT799FOltluTnj17olevXti7dy/2798vtV4oFMp1sX3Vzz//jJSUFPHf5eXl4qFb6jIk17fffivREpmfn4/vvvsOHA4HM2bMEC+fO3cuAIhbCF6XlpaGhw8f1ukYFKXIefLixQvExsbWudX13LlzCA4Olli2cuVK5OXlYfr06eJWIT6fj/bt20uNF1hYWFhrK0Zt7O3t0a9fP9y5cwe7d++WWLdq1SqlvqyJ+kMlJyfX63NEREMTXblyRaIfb0pKSrX1VNf9aWlpYcqUKcjKysLq1asl1p05cwbBwcFo06YNvL296xx/XU2bNg3l5eX46quvJJafPXtWJf155REaGorWrVtj9OjRCAoKwtChQ/HPP//gxYsX2LRpk8JDHtX1fVhcXCzu0rZ37160bt0ae/bsQUVFBaZMmSL107OJiYnENa4uHBwckJubKxGXQCDAwoULkZmZKVX+ww8/BADMmzdPqjWwsrJSonVKmfNfRNHPury8PKmh80RDV3Xu3LnG/rxAVeLCXhuDvaKiQnzMOjo6AKoahzgcjsxjtLCwgK6uLmJiYiS+VObm5uLjjz+WKp+YmChzLHJRnYr2WZ2rV6/K7JYg7/MBiPtqixLYV124cEFmYiVqqRRtf+TIkbC1tcWuXbukPhMAyQaPup5/siiSN6xYsQKsamACuR7ytAp7e3vDxcUFp0+fxrlz5yTWfffdd8jOzsaUKVOgpaUFoKovr729PY4fPy6VsH/55Zd1+rIozzVA9JrWtT++Sm9kW7lyJcrLy/Hjjz/C1dUV/fr1g7u7O/T09JCRkYE7d+7g2rVr4ptwRMzNzbF3715MmDAB7u7uGDJkCFxdXVFWViZOVPv06YMzZ84oHNunn36Ks2fPYtiwYZgyZQr09PQQEhICY2Pjep29Zu/evfDz88PkyZOxYcMGeHh4QFdXF8+ePcPVq1eRmZkp841dHS8vL7i7u2PSpEnQ19fH8ePH8ejRI4wdO1Y85q482rVrh86dO0uM05uSkoJPP/0UPXr0EJcbMmQIvvzyS3z77bdo06YNhgwZAgcHB2RnZ+Px48e4ePEivvvuO3To0EH+SlGQIufJ4sWLFRqnd8SIEXjjjTfEYxNGRkYiLCwMLi4u+OabbyTKLliwAO+++y569+6NCRMmQCgU4vTp03KNx1ibTZs2wdvbG9OnT8fRo0fF4/RGR0ejb9++CrcaisZpXbJkCe7fvw8jIyMYGxvX+HNa7969oauriw0bNiA3N1f8s9KyZctq3V/r1q0xbtw4HD58GD169IC/vz/S09Nx4sQJ+Pv7y7ypZ8CAATh06BDGjRuHoUOHQkdHB+7u7njjjTeq3Y9ozN/vvvsOV65cQa9evZCYmIiDBw9CT08P27dvb5CfsRctWoQjR47gzz//xP3799GvXz8kJyfjwIEDGD58eI199VQlJSUF7du3x7Rp0zB58mSVjS1d1/fhvHnz8OjRI6xbt07ceuTl5YXly5fjyy+/xGeffYaNGzeKtz9gwAAcOHAAo0ePRrdu3cDj8TBy5Eh06dKl1tg+/vhjnD17Fj4+Ppg4cSJ0dHQQHh6O58+fw9fXV+rDftiwYVi4cCHWrVuHtm3bYsyYMbCwsMDz588RGhqKhQsXYv78+eK41q1bh3fffRfjxo2Dvr4+HBwcMG3atHqrO5G+ffti8+bNiIqKgpeXl/ic1tXVxdatW2vd7+jRo8Hn8+Hl5QUHBwdUVFQgJCQEDx48wPjx48VfSg0MDODp6YkLFy5g2rRpaNu2LbhcrnhM3Tlz5mD9+vXi92FBQQFOnz4NBwcHqZkEb926hbFjx6Jnz57imzqfP3+Oo0ePgsvl4pNPPqkx5jVr1iAsLAz9+vWDk5MTdHR0EBMTg9DQUDg7O2PMmDG1HneXLl3g7Owss1V47ty5SE1NhY+PDxwdHcHhcHDp0iVcu3YNXl5e4tZabW1tHDhwAEOGDMHQoUMxZMgQuLu7o6CgALdu3cLLly/Fv+rV9fyrjqrzBkVwuVzs2LEDAQEBGDZsGCZMmAAHBwdcvXoV4eHhcHFxwffffy8uz+Px8Pvvv2PkyJEYMGAAJk2ahNatWyMiIgLPnz+Hu7s77ty5I9e+5bkGhISEoFWrVujXr1/dDqxOYz3IKSYmhr377rvM1dWVGRgYME1NTWZpackGDBjAfvjhB5ljBTLGWGxsLHv77beZg4MD09LSYq1atWJubm5s7ty57Nq1a+JyigwTxRhjBw8eZG5ubkxLS4tZWVmxjz/+mBUWFtZ5yLK67jcnJ4ctW7aMde7cmenq6jIDAwPWtm1b9uabb7IjR47I3NbrRMPQJCQksO+//561adOGaWlpMQcHB7ZixQqpsURrG7KspKSELVq0iNnZ2TEtLS3Wvn179ssvvzChUChz/yEhIeyNN95g5ubmTFNTk1lZWbHevXuzb7/9lj179kxcTpEhy2QNgVXddhiT/zx5td7qOk7v9u3b2dGjR5mnpyfT1dVlpqambObMmTKHoGKsatzRtm3bMk1NTWZvb8+++uorVl5ertSQZSJ3795lw4YNYwYGBszQ0JANHTqU3b17V+khlHbs2MHc3NyYtrY2AyDxHqhu+KyTJ0+K6wSvjbtY25BbhYWFbMGCBczR0ZFpa2uztm3bsm+//bbaeqqoqGCLFi1i9vb2TENDQ6p+qnu/ZWZmsrlz5zIHBwemqanJzMzM2Pjx42UODVhTHdY2rFttsrOz2bvvvsvMzc2Zjo4O6969Ozty5Ei1572qhyyrrKxU2bZkked9eOjQIQaADRo0SOraIhAIxGOMHz9+XLz8xYsXbOLEiczMzIxxuVyJuqpt2DzRPj08PJienh4zMzNjEydOZAkJCTW+1ocPH2Z+fn7MyMiIaWtrM0dHRzZt2jR27949iXJr164Vv8+rO/9UVXeMSV4X7t27x4YNG8b4fD7T19dnAwcOlBiqTETWcf72229s5MiRzMHBQTysX8+ePdnmzZslhtBirGos2GHDhjFjY2PG4XAk3gPl5eVs5cqVrG3btkxbW5vZ29uzBQsWyPwcTU5OZl988QXz8vJiFhYWTEtLi9nb27OxY8dKjL/OmOzX9cyZM2z69Omsffv2zNDQkBkYGLCOHTuyJUuWsMzMTLnres2aNQwAi4qKkli+b98+NnHiRObi4sL09PSYkZERc3d3Z2vWrJE5PNzjx4/Z22+/zWxtbZmmpiazsLBgvr6+UsPP1eX8q+lzThV5g7xqiuPOnTts/PjxzMzMjGlqajIHBwc2b968al+D8+fPMx8fH6arq8tMTEzYhAkT2LNnz1jnzp2ZkZGRRNnq3s81XQMYq3pfcDgcNn/+/DofK4exauadJY2KaGaxp0+fKjQLkIivry8iIiKqnW6YEEJI45CYmAgnJyfMmDGjTjctkv/k5OTA2dkZEyZMwJ9//qnucFqkwsJCWFpaws3NTWZXk7patmwZ1q5di4cPH8LFxaVOz22YcVoIIYQQQhqYiYmJuJtbUlKSusNp1oqLi1FYWCixTCAQ4LPPPkNJSYnE1NaKys3NxcaNG/HBBx/UOeEFVNynlxBCCCGkMZk3bx7Kysrw7Nkzcf9lonrx8fHw8fFBQEAAnJ2dUVhYiIsXL+LBgwfo1KmT+MZ4ZTx9+hSffPKJzJsn5UFJLyHNQGJiolw/fxobG4tvzCHyo/ol1dmwYYNcI6nMnDlTqa5pRHE6OjpSI6kQ1bOxscGECRMQERGBM2fOoLKyEvb29li4cCGWLl0KfX19pffh4eEBDw8PhZ9PfXoJaQbCw8PlmqHIwcFB5jBCpGZUv6Q6jo6Ocv1sHhYWJtdseYSQ+kNJLyGEEEIIafboRjZCCCGEENLsUZ/eBiAUCpGamgpDQ8Nq54wnhBBCCHkVYwyFhYWwtrZukIl1mjtKehtAamoq7Ozs1B0GIYQQQpqg5ORk2NraqjuMJo+S3gZgaGgIoOqk5fP5ao6GEEIIIU1BQUEB7OzsxHkEUQ4lvQ1A1KWBz+dT0ksIIYSQOqGukapBHUQIIYQQQkizR0kvIYQQQghp9ijpJYQQQgghzR4lvYQQQgghpNmjpJcQQgghhDR7NHoDIYTISSAUIiY5FllFeTAzMIaHnSt4NGA8IYQ0CZT0EkKIHEJjo7E2ZCfSC3PEyywNTbBo0HT4u3qqMTJCCCHyoCYKQgipRWhsNBYe2SCR8AJARmEOFh7ZgNDYaDVFRgghRF6U9BJCSA0EQiHWhuwEk7FOtGztuZ0QCIUNGRYhhJA6oqSXEEJqEJMcK9XC+yoGIL0gBzHJsQ0XFCGEkDpTuk9vUVER0tPTkZubi1atWsHS0hIGBgaqiI0QQtQuqyhPpeUIIYSoh0JJb3BwMI4ePYrQ0FAkJCRIrW/Tpg0GDBiA0aNHIyAgQOkgCSFEXcwMjFVajhBCiHrInfQKBAJs3rwZv/zyCxISEsDYfz3cDAwMwOfzkZ+fj+LiYsTHxyM+Ph5btmxBmzZtMHfuXLz//vvg8Xj1chCEEFJfjHUNweFwJK55r7Pkm8DDzrUBoyKEEFJXcvXpPXPmDDp37oy5c+ciKSkJI0eOxK+//oqYmBiUlZWhoKAAKSkpKCwsRGlpKa5fv45ffvkFI0aMQGJiIubOnQs3NzcEBwfX9/EQQojKpORmYM6+NTUmvACwwH8qjddLCCGNHIfVdjUHwOVyYWlpic8++wzTp0+HmZmZ3DvIyspCYGAg1q5di6ysLAgEAqUCbooKCgpgZGSE/Px88Pl8dYdDCJFDWkE2Zv/9LVLzM+FiZovpXsPxW8RBiZvaOKi6ke3tPqPwse9EtcVKCGmeKH9QLbmS3pUrV+KTTz6Bnp6ewjt6+fIlfvrpJyxdulThbTRVdNIS0rRkF+Vj9q5vkZTzAnatLLF92lcwMzCWmpEtqygXi4N+AwccbJq8CH2cu6g7dEJIM0L5g2rJlfQS5dBJS0jTkV9ShP/t+g7xmclozTfFtmlfobVR9b9urTy9DQdvhqKVHh/7314FC8NWDRgtIaQ5o/xBtagTGiGE/Kuo7CXm7FuD+MxkmBsY4483l9SY8ALAwkFvoZ2FPXJfFmBx0CZUClteFy5CCGkKVJ70Pn36FEFBQbh165aqN00IIfWmpLwUH+9fh/svnsBY1wC/T1kMexOrWp+nraGFH8bOg56WDm48e4g/Lh5pgGgJIYTUlUJJ77FjxzB27Fhcu3ZNYvkPP/yAdu3aYezYsejevTtmz56tkiAJIaQ+lVWW45PDP+FmyiMYaOth85Qv4GJuK/fzHUys8OXQtwEAWy8H4erTu/UVKiGEEAUplPTu3LkTZ86cQYcOHcTLYmNj8cUXX4AxBnd3d+jp6SEwMBDHjx9XWbCEEKJqFYJKfP7PRkQ+vQddTW1smrQIHayc6rydoZ36YFzXAWBgWBL0GzKLcushWkIIIYpSKOm9efMm3N3dYWhoKF62e/duAMBvv/2GmJgYREdHg8fjYcuWLaqJlBBCVEwgFOLL478jPD4G2hqa+GXCArjbtlV4e58NmvZf/96jmyAQClUYLSGEEGUolPRmZWXBxsZGYll4eDh0dXUxc+ZMAICrqyt8fHxw//59pYMkhBBVEzIhvj29FWceXIUGl4d1Y+fD07GTUtvU0dTC2jEfQ1dTG9efPcQfl6h/LyGENBYKJb2lpaUSUwoLBALExMSgV69e0NLSEi+3trZGWlqa8lESQogKMcbwQ8jfOHo7AlwOB9+P/gh923RVybYdTa2x7N/+vX9eOoqop/dUsl1CCCHKUSjptbCwQHx8vPjvyMhIlJSUwNvbW6JcSUkJ9PX1lYuQEEJUiDGGjeH7sff6WQDANyPew0DXnirdx/DO3hjT1beqf++x35BVlKfS7RNCCKk7hZLePn364Pbt29i3bx/y8/OxatUqcDgcDBw4UKLcw4cPYW1trZJACSFEFbZeCcK2q1U32C4dMgsj3PrWy34+HzQDbc3tkF2cjyVBv1H/XkIIUTOFkt7PP/8cGhoamDp1KkxMTHD69Gl4eHigX79+4jLJycmIjY2Fp6enyoIlhJC6EAiFiE56gNP3ryA66QF2Rp3EpoiDAIAF/lMxwWNgLVtQXFX/3rnQ1dTGtaT7+PPyP/W2L0IIIbXTUORJHh4eOHXqFFauXImMjAz07NkTq1evlihz4MABGBkZwd/fXyWBEkJIXYTGRmNtyE6kF+ZIrfug7zhM6zWs3mNwMrPGsqGzsfTYZvxx8R942Lmip5I3yxFCCFEMhzHG1B1Ec0dzZxPSsEJjo7HwyAZUd3FbN3aeyvvx1mTFyS04ejsCZvrG2P/2KpgaGDXYvgkhTRflD6ql8mmICSFEnQRCIdaG7Kw24eUA+OHc3w3ax/bzwTPgYmaLrOI8LDlG/XsJIUQdFOre8KrIyEiEh4cjJSUFjDHY2trC19cXvXv3VkV8hBBSJzHJsTK7NIgwAOkFOYhJjoWnQ8cGiUlXUxtrx3yMqTu+QlTiPfx1JQjv+oxpkH0TQgiponDSm5CQgOnTpyMyMhJA1TBAAMDhcAAAvXr1ws6dO9GmTRsVhEkIIfKRd3iwhh5GzMXcFksCZuKrE3/g94uH0c2ufYMl3YQQQhRMelNTU9G3b1+kpaVBT08PQ4YMgZNT1Vz1iYmJOHPmDCIjI9GvXz9ER0dLzd5GCCH1xczAWKXlVGlkl364/uwhjt25gCVBm7Bn1ndIzHmBrKI8mBkYw8POFTwu9TojhJD6oFDS+9VXXyEtLQ3jxo3Db7/9BnNzc4n1WVlZmDNnDg4dOoTly5dj69atKgmWEEJq42HnCktDk2q7OHAAWPBN4GHn2rCB/Wvx4Bm4l5qAJ1nPMfy3+SgXVIrXWRqaYNGg6fB3paEeCSFE1RRqUjh9+jSsra2xe/duqYQXAMzMzLBr1y5YW1vj1KlTSgdJCCHy4nG5WDRousx1nH//XTRwutpaVHW1dDC26wAAkEh4ASCjMAcLj2xAaGy0OkIjhJBmTaGrfk5ODnx8fKClpVVtGS0tLfj4+CA3N1fh4AghRBH+rp6wNbaQWm7BN8G6sfPV2pIqEArxd9RJmetEI06sPbeTRngghBAVU6h7g62tLYqLi2st9/LlS+rPSwhpcM/zMpGSlwEOgPXj5qOssqLR9JltjKNLEEJIS6BQ0jthwgRs3LgRz58/rzapff78Oc6fP4+PPvpIqQAJIaSuQh9dAwB0t++AAe0bV//Yxjq6BCGENHcKNXl8+eWXcHNzw4ABA3DixAmp9SdPnoS/vz+6dOmC5cuXKx0kIYTUxbl/+8Q25Kxr8mrMo0sQQkhzJldL74ABA6SWcblcxMfHY9SoUTA2NoajoyOAqiHL8vLyAAC9e/fGiBEjEBoaqrKACSGkJumFObjzPB4A4Ne+h5qjkSYaXSKjMEfmrHHqHl2CEEKaK7mS3vDw8GrXMcaQm5sr84a1K1euiCerIISQhnD+31Zed9u2sDQ0UXM00kSjSyw8sgEcQGbiq87RJQghpLmSK+kNCwur7zgIIUQlzj36t2tD+8bXtUHE39UT68bOx9qQnRI3tWnyNPD9qI9onF5CCKkHciW9/fv3r+84CCFEadlF+Yh5FgsAjT5x9Hf1hG+77ohJjsXjzBSsORuICkEl2ls6qDs0Qghplhr972fR0dEYNmwYjI2Noa+vDy8vLxw4cECu5zLGcPr0aXzwwQfo0qULjIyMoKenB3d3d6xatQqlpaXVPjc4OBj9+/eHoaEh+Hw+/Pz8qG8yIY1cWNx1MDB0bO0MayPpiXMaGx6XC0+HjpjSYzC8nDoDAI7dvaDmqAghpHlSaMiyV6WmpiIiIgLPnz8HANjY2KBfv34qGZ83LCwMAQEB0NHRweTJk2FoaIjDhw9j0qRJSE5OxoIFC2p8fllZGYYNGwZtbW34+voiICAApaWlCA4OxtKlS3H06FGEh4dDT09P4nm7du3CtGnTYG5ujpkzZwIA9u/fj0GDBuHAgQMYP3680sdGCFG9c/8OVTawkQ1TJo9RXfoj8uk9HL97Ee/3HQsup9G3SRBCSJPCYYzJuo+iVvn5+fjoo4+wb98+CF+bOYjL5WLKlCnYuHEjjIyMFAqssrISrq6uSElJQWRkJLp27Sreb8+ePZGYmIi4uDg4OFT/U2BFRQXWrl2LOXPmoFWrVhLLx40bh+PHj2Pt2rX47LPPxOtyc3Ph7OwMDQ0N3Lx5E7a2tgCAlJQUdOvWDQDw5MkTGBoayn0sBQUFMDIyQn5+Pvh8fl2qgRAip7yXhfD/eQ4ETIig99fDwcRK3SHVSWlFOQb+8iGKyl5iy5tL0NOxk7pDIoSoGeUPqqVQU0JpaSkGDhyIPXv2QCAQoEuXLhgzZgzGjBkDd3d3CAQC7N69G4MGDUJZWZlCgZ0/fx4JCQl48803xQkvABgZGWHJkiUoLy9HYGBgjdvQ1NTE0qVLJRJe0fLFixcDACIiIiTWHTx4EHl5efj444/FCS9QNQvdRx99hKysLPzzzz8KHRMhpP6Ex8dAwIRoZ2Hf5BJeANDR1MKQjl4AgKA7EbWUJoQQUlcKJb0bN27EjRs30K1bN1y/fh03b97EoUOHcOjQIcTExODGjRvo3r07bty4gY0bNyoUmGiYtMGDB0utCwgIACCdsNaFpqYmAEBDQ7KHR33vlxBSP87F/tu1oRFOSCGvUV2qbhoOjY1GUdlLNUdDCCHNi0JJ7/79+8Hn8xEcHAwPDw+p9d26dcOpU6dgaGiIffv2KRRYfHzV4PJt27aVWmdlZQUDAwNxGUVs27YNgHRyW9N+RcuU2S8hRPUKS18i8uldAIB/E+zPK9LZ2gVOptYorSzH2YdR6g6HEEKaFYWS3ri4OPj5+cHU1LTaMmZmZvDz88OjR48UCiw/Px8Aqu0TzOfzxWXq6vTp0/jjjz/QoUMHvP3223LvV9Sfprb9lpWVoaCgQOJBCKk/Fx7fRKVQACdTa7iY29b+hEaKw+FgZJd+AKiLAyGEqJpCSa9AIBB3D6iJpqam1E1u6hYdHY1JkybByMgIBw8ehLa2tsr3sXr1ahgZGYkfdnZ2Kt8HIeQ/zaFrg8jwzj7gcji4nRKPpOwX6g6HEEKaDYWSXicnJ1y4cAElJSXVlikpKcGFCxfg5OSkUGCiltbqWlVFdzTWxfXr1zF48GBwuVwEBwejUyfpu6Nr2q+oxba2/S5evBj5+fniR3Jycp3iJITI72V5Ka48uQ2g8U9IIQ8Lw1bo4+wOAAiiMXsJIURlFEp6R44ciYyMDEydOhWZmZlS6zMzM8XrRo8erVBgNfWfTUtLQ1FRkcx+t9W5fv06Bg0aBKFQiODgYHh6yv5wrGm/NfX3fZW2tjb4fL7EgxBSPy4+voWyygrYtbJEe4vmMZvZqH+7OJy4exGCRvZrGSGENFUKJb2fffYZ7O3tERQUBGdnZ0yYMAGLFi3CokWLMH78eDg5OSEoKAgODg5YuHChQoGJpj4+e/as1Lrg4GCJMrURJbwCgQBnzpxBr169GmS/hJD6F/rvhBT+7T3B4XDUHI1q9G/rASNdA2QU5iIq8Z66wyGEkGZB4ckpnj59iilTpuDataoPHNGHjWhzvXr1wp49exTu3lBZWYn27dvj+fPn1U5O8ejRIzg6OgIAXrx4gfz8fLRu3Vqi+8GNGzcwcOBAVFZW4syZM/D29q5xv7m5uXBycoKmpiZNTkFII1daUQ6/De+jpKIMf8/8Bm7WLuoOSWW+Dw7EvhtnEdDBC2vGfKzucAghakD5g2opPA2xk5MTIiMjcfnyZYSHh0tMQ+zr61trcllrYBoa2Lp1KwICAtCvXz+JaYiTkpKwbt06ccILVPWjDQwMxPbt28VTB+fk5GDQoEHIy8vDkCFDEBISgpCQEIn9GBsbY/78+eK/W7VqhV9//RXTpk2Dh4cHJk2aBKBqmLbs7Gzs37+/TgkvIaT+XHlyByUVZbDim6Jza2d1h6NSI7v0w74bZxEWdwMFJcXg6+qrOyRCCGnSFE56Rby9veuU4F65cgWPHz/G9OnTay3r5+eHS5cuYfny5di/fz8qKirg5uaGNWvWiJPRmhQUFCA3NxcAcObMGZw5c0aqjIODg0TSCwBvvfUWzMzMsGrVKmzfvh0cDgfdu3fHsmXLMHDgQPkOlBBS75pj1waRDlaOaGtuh/jMZJx5cBUTu9O1hxBClKFw9wZFzZo1Czt37oRAIGjI3aoV/TxBiOqVV1ZgwM8foKisBNunfYVudu3VHZLK/X3tNNaf24XO1i7YNfMbdYdDCGlglD+olkI3shFCiLpFJd5HUVkJzA2M4W4r/0guTcnwTn2gweXhXmoCEjJT1B0OIYQ0aZT0EkKaJFHXBr92PcDlNM9LmYm+EXzadAUABN2hMXsJIUQZzfOTghDSrFUIKhEWdwMAMKgZzMJWk5FuVWP2nrx3CZXCltMtjBBCVI2SXkJIk3PjWSzyS4rQStcQ3exd1R1Overbpita6fGRXZyPKwm31R0OIYQ0WZT0EkKanNDYf7s2tO8BDS5PzdHUL02eBoZ3rhoh5xh1cSCEEIVR0ksIaVIEQiFCH0UDAAY2864NIiP/nZY4PD4GuS8L1RwNIYQ0TZT0EkKalFspj5DzsgCGOnrwdOio7nAaRDsLe3SwckSlUIDT9y+rOxxCCGmSKOklhDQp5/7t2uDbtjs0eUrPr9NkjOrSHwBw7M5FNUdCCCFNEyW9hJAmQ8haXtcGkSEde0OTp4HY9EQ8Sk9SdziEENLkNHjSyxhDA08CRwhpJu6lJiCjMBf6Wjrwcuqs7nAalLGeIfq39QBAY/YSQogiGjzpXb9+PZ4+fdrQuyWENAOirg1923SDtoaWmqNpeKIb2k7fu4wKQaWaoyGEkKZFqQ5xWVlZ2Lp1K8LDw5GSkgLGGGxtbeHn54fZs2fDwsJC6jmmpqYwNTVVZreEkBaIMYZzsS2za4NIH+cuMDcwRmZRHi4+vokB7T3VHRIhhDQZCrf0BgUFoV27dli6dCnOnj2LBw8e4OHDhwgJCcHSpUvRrl07/PPPP6qMlRDSgj1MS0RqfiZ0NLXh7eKu7nDUQoPLw/DOPgBozF5CCKkrhZLeqKgoTJgwAXl5eejevTt++eUXHD9+HMePH8fGjRvRo0cPFBQUYNKkSYiKilJ1zISQFujco6quDT7OXaCrqa3maNRH1MXh4uNbyC7KV3M0hBDSdCiU9H7zzTcQCAT44YcfcO3aNXz00UcYPnw4hg8fjg8//BBRUVFYv349Kisr8e2336o6ZkJIC1PVtaEq6fVvoV0bRJzNbOBm7QIBE+Lk/UvqDocQQpoMhZLeK1euoHPnzliwYEG1ZT755BO4ubnh8mUaSJ0QopzHmcl4lpMGLZ4m+rbpqu5w1E7U2nvszgUaDYcQQuSkUNJbUVEBNze3Wst17twZFRUViuyCEELERDew9XZ2g4G2npqjUb+Ajr2hraGJx5kpeJBGo+EQQog8FEp6XV1dkZycXGu558+fo3379orsghBCxERdG1rqqA2v4+vow69dDwB0QxshhMhLoaT3vffew6VLlxAREVFtmYiICFy8eBHvvfeewsERQkhidioSslKgweWhfxsPdYfTaIjH7L1/BWWV5WqOhhBCGj+Fkt533nkH8+fPx/Dhw7Fw4ULcuXMHhYWFKCwsxJ07d/DZZ59h+PDhmD9/Pt59911Vx0wIaUFEXRt6OnYCX1dfzdE0Hr0cO8PS0AQFpcUIj4tRdziEENLocZgCd0HweDzFd8jhoLKyZc0kVFBQACMjI+Tn54PP56s7HEKalMl/LUVseiK+GvY/jO3qp+5wGpWN4Qfw15UgeLu4Y9OkReoOhxCiYpQ/qJZCLb2MMYUfQqFQ1cdACGmmUnIzEJueCC6HA9+23dUdTqMj6uJw9ckdpBfmqDkaQghp3BRKeoVCoVIPQgiRh2hCih72HWGiT60cr3MwsUI32/YQMoZT92h4SEIIqYnC0xATQkh9CxVPSOGp5kgaL1Frb9DtCBqzlxBCakBJLyGkUUoryMbd1ARwwMGA9j3UHU6jNbhDL+hoaiMx5wXuPH+s7nAIIaTRUirpTUhIwKJFi+Dj44P27dtj0aL/bqSIiorCli1bkJ9Pc8MTQuou9N9RG7ratoO5QSs1R9N46WvrYmD7qpZwGrOXEEKqp3DSGxgYiM6dO2PdunW4cuUKHj9+jKysLPH6ly9f4oMPPsA///yjkkAJIS1L6CPq2iCvUf92cQh+eBUlFWVqjoYQQhonhZLeyMhI/O9//4OWlhbWrl2LqKgoqb5k/fv3h5GREY4fP66SQAkhLUdWUR5uJscBAPzbU9Jbm+4OHWBtZI6ishL8dSUIp+9fQXTSAwjoxmFCCBHTUORJa9euBWMMJ0+ehI+Pj8wyXC4XXbt2xYMHD5QKkBDS8px/dB0MDJ2tXdDayEzd4TR6XA4XnVo7IzU/E1svB4mXWxqaYNGg6dRaTgghULCl9/Lly+jZs2e1Ca+IlZUVXrx4oVBghJCWRyAUIjrpAfbfCAEA+LWjsXnlERobjZDYKKnlGYU5WHhkg7h/NCGEtGQKtfTm5eXB3t6+1nIlJSUoL6c54QkhtQuNjcbakJ0SkyzsiQ6Go4k1tVTWQCAUYm3ITpnrGAAOgLXndsK3XXfwuDRgDyGk5VLoCmhqaoqkpKRayz1+/BhWVlaK7IIQ0oKExkZj4ZENUrOK5RTnU0tlLWKSY2ucjY0BSC/IQUxybMMFRQghjZBCSa+XlxeuX7+O+/fvV1vm8uXLuH//fq1dIAghLZuopVLWtAqiZWvP7aSbsqqRVZSn0nKEENJcKZT0fvjhhxAIBBg3bhxu3boltf7hw4eYPXs2OBwO5syZo2yMhJBmjFoqlWNmYKzScoQQ0lwplPT6+/vj008/RVxcHLp374527dqBw+EgODgYXbp0gZubG+Lj4/HZZ5/By8tL1TETQpoRaqlUjoedKywNTcCpZj0HgCXfBB52rg0ZFiGENDoK39Wwbt06/PHHH7CyssLjx4/BGMOLFy9w7949mJiYYOPGjfj+++9VGSshpBmilkrl8LhcLBo0HQCqTXwXDZxON7ERQlo8Dnt9Vok6Yozh5s2bePLkCYRCIezs7ODp6QkNDYUGhmiWCgoKYGRkhPz8fPD5fHWHQ0ijIhAKMWzTvGq7OHAAWPBNcGrOz5S41UDW6BfaGppYNfJDGv2CkCaK8gfVUjrpJbWjk5aQmoXGRmPBkQ1Sy0Utl+vGzqfETQ4CoRAxybG4n5qADWH7wONwcW7eb2ilZ6ju0AghCqD8QbUafbNJdHQ0hg0bBmNjY+jr68PLywsHDhyQ+/kJCQlYsWIFRo4cCRsbG3A4HDg6Otb4HA6HU+1j5syZyh0QIUSKv6snfFzcpZZb8E0o4a0DHpcLT4eOmNn7DbhaOkLAhAh+cFXdYRFCSKMgVx+Ef/75B2PGjFF6Z0eOHMHYsWPlLh8WFoaAgADo6Ohg8uTJMDQ0xOHDhzFp0iQkJydjwYIFtW7j4sWL+Prrr8Hj8dChQwekpaXJtW8HBweZCW7Xrl3ljp8QIh/GGBKyngMAPuo/ETbG5jAzMIaHnSt1aVDQCDcfxKYn4uS9S5jcY7C6wyGEELWTq3sDl8tFr169sGLFCgQEBNRpB4wxnDp1Ct988w2uX78OgUAg1/MqKyvh6uqKlJQUREZGipPN/Px89OzZE4mJiYiLi4ODg0ON23ny5AkyMjLg7u4OXV1d6OjowMrKComJidU+h8PhoH///ggPD5fzKGtGP08QUrO4jGeYuHUxdDS0EPbJ79DV1FZ3SE1edlE+Bm/8CAImxNH3foCjqbW6QyKE1BHlD6olVxPKTz/9hEePHmHYsGGwt7fHsmXLEBYWhuLiYpnli4uLcf78eSxevBj29vYYOXIk4uPj8dNPP8kd2Pnz55GQkIA333xTonXVyMgIS5YsQXl5OQIDA2vdjrOzM7y8vKCrqyv3vgkhDSsi7gYAwMupMyW8KmJqYITezl0AACfvXVZzNIQQon5ydW+YN28epk6dihUrViAwMBCrVq3C6tWrweVyYWtrC1NTU/D5fBQUFCA7OxspKSkQCoVgjEFfXx9z5szB8uXLYWZmJndgolbWwYOlf5YTtTZHRETIvb26ysvLw5YtW5CVlQUTExN4e3vDzc2t3vZHSEsWHh8DAOjftruaI2leRnT2waWEWzhx7xI+6DcOXA51FSGEtFxyjytmZmaGX3/9FStXrsS2bdtw9OhRREVFISkpCUlJSRJltbS04O3tjdGjR2PWrFkwMjKqc2Dx8fEAgLZt20qts7KygoGBgbhMfbh9+zbee+89iWVDhgxBYGAgLCws6m2/hLQ0GYW5uP/iCTjgoF+bbuoOp1nxbdcdBtq6eJGfhZvJj9DdvoO6QyKEELWp82C6RkZG+OSTT/DJJ5+gtLQU9+/fR3p6OvLz82FsbAwLCwt06tQJOjo6SgWWn58v3p8sfD5fXEbVFixYgHHjxqFdu3bQ0tLCvXv38O233+L06dMYMWIErl69Ch6PV+3zy8rKUFZWJv67oKCgXuIkpDm48PgmAMDNxgWmBnX/gkyqp6OphYGuPXH0dgRO3L1ESS8hpEVTagYJHR0ddO/e/H6OXLduncTfvXv3xokTJzBgwABEREQgKCioxlEoVq9eja+//rq+wySkWYiIr+rP27+th5ojaZ5GdO6Lo7cjEBIbhc8Hz4COppa6QyKEELVotB28RC281bXmiu5obChcLhfvvPMOAODy5ZpvClm8eDHy8/PFj+Tk5IYIkZAmp6S8FFFP7wOg/rz1xcO+PVrzTVFUVoKIf/tOE0JIS9Rok15RX15Z/XbT0tJQVFQks79vfRLdiFfdqBUi2tra4PP5Eg9CiLSrT++hXFABW2MLuJjZqDucZonL4WJ4Zx8AwIl7F9UcDSGEqE+jTXr79+8PADh79qzUuuDgYIkyDSUqKgoAap3RjRAinwjxqA0e4HA4tZQmihrhVpX0Xkm4g5zi+rkXghBCGrtGm/T6+/vD2dkZe/bswa1bt8TL8/PzsWrVKmhpaWH69Oni5S9evEBsbKzSN7fdvXsXFRUVUsuvXLmCNWvWQFNTExMmTFBqH4QQQCAUim9i86X+vPXK0dQana1dIGBCnL5P0xITQlompW5kq08aGhrYunUrAgIC0K9fP4lpiJOSkrBu3TqJFtfFixcjMDAQ27dvl5g+OCsrCwsXLhT/XVFRgaysLIky69atE3ddWL9+PU6ePAkfHx/Y2dlBU1MT9+/fx9mzZ8HhcLBp0ya4uLjU9+ET0uzdTX2M3JcFMNTRQ1e79uoOp9kb0dkH91ITcPLeJUztOUTd4RBCSINrtEkvAPj5+eHSpUtYvnw59u/fj4qKCri5uWHNmjWYNGmSXNsoKiqSmrmtuLhYYtmKFSvESe+oUaOQl5eH27dvIyQkBOXl5bCyssLkyZMxf/589OzZU3UHSEgLJura4OPSFZq8Rn0pahYCOvbGunO78CDtKRIyU+BibqvukAghpEFxGGNM3UE0dzR3NiHSxv7xGZ5kp2LN6I8Q0LG3usNpEeYfXI/w+BjM7v0G5vpNVnc4hJBaUP6gWo22Ty8hpPlKyknDk+xUaHB56OPsru5wWowRbn0BACfvXYaQCdUcDSGENCyVJ70RERH4+eefcfToUQiFdFElhEi78G/Xhu72HWCoo6fmaFqOfm26wVBHD+mFObie9FDd4RBCSINSKOndsWMHPDw8cOnSJYnlH3/8MQYMGIBPP/0U48aNw5AhQyAQCFQSKCGk+Qj/N+mlURsalpaGJga7egEAjt+lMXsJIS2LQknvoUOHkJCQAE9PT/Gy69evY9OmTdDR0cGoUaNgY2OD0NBQ7Nu3T2XBEkKavryXhbiV/AgA0I+S3gYnGrM39FE0SspL1RwNIYQ0HIWS3nv37sHNzQ3a2triZfv27QOHw8Hff/+NI0eO4Nq1a9DR0cG2bdtUFiwhpOm7/OQ2BEyIdhb2sDE2V3c4LU5X23awNbbAy/JSnI+7oe5wCCGkwSiU9GZnZ8PWVnK4mwsXLoDP52P06NEAACsrK/Tt2xePHz9WOkhCSPMRHvffLGyk4XE4HPG0xCfvXaqlNCGENB8KJb0VFRUSfXXLyspw+/Zt9OnTB1zuf5s0NzdHRkaG8lESQpqF8soKXHlyGwAlveo0vLM3ACDy6V1kFuWqORpCCGkYCiW91tbWuH//vvjviIgIVFRUoE+fPhLlROPLEUIIAFx/9hDF5aUwNzBGx9ZO6g6nxbI3sYK7bVsIGcPp+1fUHQ4hhDQIhZJeX19fPHr0CN9//z1u376N5cuXg8PhYMgQyakt7927J9UNghDScolmYevXxgNcDg0Trk4j/u3icOIudXEghLQMCn3qLFmyBAYGBli6dCk8PDwQFRWFgQMHonv37uIycXFxePr0Kby8vFQWLCGk6WKMiZPe/u2oa4O6De7gBU2eBuIyniEu45m6wyGEkHqnUNLbpk0bXLlyBTNmzMDQoUOxYsUKHD16VKJMaGgo3N3dMXz4cFXESQhp4h5lJCGtIBs6mtro6dBJ3eG0eEa6BujXphsAau0lhLQMHMYYU3cQzR3NnU0I8MfFI9h88TD82vXAT+M/UXc4BEBY3HV8cugnmBsY48xHG8HjUpcTQhoTyh9Ui65whJAGIe7aQKM2NBo+Ll1hrGuAzKI8RCXeU3c4hBBSryjpJYTUu/SCbDxIewoOOOjXpqu6wyH/0uRpIKBjbwDUxYEQ0vxpyFPI2dkZHA4H586dg5OTE5ydneXeAYfDQUJCgsIBEkKavguPbwIAuti2gYk+DWPYmIzo7IP9N0JwPu46istKoK+tq+6QCCGkXsiV9CYmJoLD4aCiokL8t7w4HI5CgRFCmo/wf7s2+LbtXktJ0tA6W7vAwaQ1knJeIPRRNEZ26afukAghpF7IlfQ+ffoUAGBjYyPxNyGE1OZleSmuJVZNZkP9eRsfDoeDEZ29senCIZy8d4mSXkJIsyVX0uvg4FDj34QQUp2rT+6iQlAJu1aWcDK1Vnc4RIZhnX2w6cIhXEt8gPSCbFjyTdUdEiGEqBzdyEYIqVfh8TcAAL5tPai7UyNlY2yO7nauYGA4RdMSE0KaKZUmvYwxBAYG4pNPPsGGDRtQXFysys0TQpoYgVCIi//exNaf+vM2aiPc+gIAjt+9CBq+nRDSHCmU9K5fvx4mJiYICwuTWD5mzBjMnj0bv/zyCxYsWABvb2+UlJSoJFBCSNNz53k88kqKwNfRR1e7duoOh9RgoGtPaGto4knWc8SmJ6o7HEIIUTmFkt7Tp0+Dx+OhX7//bngICwvDsWPHYG5ujnnz5qFLly64e/cuduzYoapYCSFNjKhrQ982XaHB5ak5GlITQx098egax2nMXkJIM6RQ0hsXF4dOnTqBx/vvQ+zQoUPgcDjYu3cvfvzxR1y4cAF8Ph+7d+9WWbCEkKYlIk40Cxt1bWgKhrv5AADO3L+CCkGlmqMhhBDVUijpzc7OhrW15F3Yly5dgpmZGfz8/AAAhoaG8Pb2puHNCGmhErNTkZjzAhpcHvo4u6k7HCKH3k5uaKXHR87LAlx9elfd4RBCiEoplPQKhUKUlpaK/y4uLsaDBw/g7e0tUa5Vq1bIyclRLkJCSJMUEV91A5unQ0cYaOupORoiD02eBoZ26gMAOEldHAghzYxCSa+9vT1u3rwp/vvs2bMQCARSSW9ubi5MTEyUi5AQ0iRF/NuflyakaFpGdK7q4hAWdwOFpS/VHA0hhKiOQknvkCFD8OzZM8yZMwdBQUFYvHgxOBwOhg8fLlHu1q1bsLe3V0mghJCmI/dlIW6lxAGgpLep6WDlCGczG5QLKnAu9pq6wyGEEJVRKOldvHgxrKys8Pvvv2Ps2LGIi4vD1KlT4erqKi4TExOD1NRU9OnTR2XBEkKahksJtyBkDO0tHdDayEzd4ZA6qJqWuKq198Q96uJACGk+5JqG+HVWVlaIiYnBli1bkJ6ejp49e2LatGkSZe7fv49Ro0Zh7NixKgmUENJ0RMRR14ambFhnb2wMP4Abzx7ieV4mbIzN1R0SIYQojcNo6p16V1BQACMjI+Tn54PP56s7HELqVVllOXx/eh8lFWXYM+s7dGztpO6QiALe3b0K15LuY7R7f/Ry7AwzA2N42LmCx6XZ6wlpKJQ/qJZCLb2EEFKd60kPUVJRBnODVuhg5ajucIiCnM2scS3pPo7ejsDR2xEAAEtDEywaNB3+rp5qjo4QQupOJUlvfn4+CgoKqp2vnW5mI6TlCI+vmpDCt60HOByOmqMhigiNjca+GyFSyzMKc7DwyAasGzufEl9CSJOjcNKbm5uLr776CgcPHkRmZma15TgcDioraWYfQloCxth/Q5W1o/68TZFAKMTakJ0y1zEAHABrz+2Eb7vu1NWBENKkKJT05ufnw8vLC48fPwaPx4Ouri5evnyJ1q1bIy0tDYwxcDgcauElpIWJTU9ERmEudDW14enQUd3hEAXEJMcivbD6SYUYgPSCHMQkx9JrTAhpUhT6mv7DDz8gPj4e06dPR35+PsaPHw8Oh4Pnz5+jsLAQmzdvhrGxMfr370/TEBPSgoTHVXVt6OPcBdoaWmqOhigiqyhPpeUIIaSxUKil99ixYzAzM8PmzZuho6Mj0W9PT08P7733Htzd3eHj44M+ffrg3XffVVnAhJDGK+Lf/rw0VFnTZWZgrNJyhBDSWCjU0vvkyRN0794dOjo6ACBOegUCgbiMl5cXevfujb/++ksFYRJCGru0gmzEpieCy+HAx6WrusMhCvKwc4WloQmquwWRA8CSbwIPO9dqShBCSOOk8F0IrVq1Ev9fT08PQNXNba+yt7dHbGysorsghDQholZed5t2MNGn8SSbKh6Xi0WDpgNAtYnvooHT6SY2QkiTo9BVy9raGs+fPxf/Lbph7c6dOxLlnjx5Ag0N5UZFi46OxrBhw2BsbAx9fX14eXnhwIEDcj8/ISEBK1aswMiRI2FjYwMOhwNHR8danxccHIz+/fvD0NAQfD4ffn5+CA0NVeJICGnexF0baNSGJs/f1RPrxs6HhaGJ1Lr/eY+m4coIIU2SQhmpm5sbLl++LP67b9++YIxh+fLl8PT0hKGhIXbt2oWoqCh4e3srHFxYWBgCAgKgo6ODyZMnw9DQEIcPH8akSZOQnJyMBQsW1LqNixcv4uuvvwaPx0OHDh2QlpZW63N27dqFadOmwdzcHDNnzgQA7N+/H4MGDcKBAwcwfvx4hY+JkOaoqOwlriXeB1A1Pi9p+vxdPeHbrjtikmORVZSH84+iERJ7DbdS4tQdGiGEKEShaYj//PNPvPfeewgNDYWfnx+AqsT38uXL4PF44PP5yMvLAwAcPXoUb7zxRp0Dq6yshKurK1JSUhAZGYmuXbsCqBourWfPnkhMTERcXBwcHBxq3M6TJ0+QkZEBd3d36OrqQkdHB1ZWVkhMTJRZPjc3F87OztDQ0MDNmzdha2sLAEhJSUG3bt3E2zQ0NJT7WGgaQdLchTyMwmf//AIHk9YIen+dusMh9SCtIBsjfvsElUIBds38Bp2tXdQdEiHNHuUPqqVQ94YpU6bg4sWLaNeunXjZkSNHMGLECABViaOxsTF+/PFHhRJeADh//jwSEhLw5ptvihNeADAyMsKSJUtQXl6OwMDAWrfj7OwMLy8v6OrqyrXfgwcPIi8vDx9//LE44QUAW1tbfPTRR8jKysI///xT5+MhpDmjURuaPyu+KYZ06gMA2BF5Qs3REEJI3SmU9BoYGMDb2xs2NjbiZebm5jh27Bjy8/Px/PlzZGZmYt68eQoHFh4eDgAYPHiw1LqAgAAAQEREhMLbb2z7JaQpEgiFiHx6D6GPogEA/WjUhmZtZq/hAKqmKX6WU3tXMUIIaUxUfvutnp4eWrduDa6Sd/bGx8cDANq2bSu1zsrKCgYGBuIyqlTTfkXL6mO/hDQ1obHRGLZpHt7fuxolFWUAgCXHNyM0NlrNkZH60sbCDn3bdAUDw99Rp9QdDiGE1IlCmSmPx8Pbb79da7l33nlH4dEb8vPzAVR1Z5CFz+eLy6hSTfsV9aepbb9lZWUoKCiQeBDSnITGRmPhkQ1S09VmFuZg4ZENlPg2YzO9qrqxBd25gOwi1V+DCSGkviiU9DLGIO/9bwrcJ9fkrV69GkZGRuKHnZ2dukMiRGUEQiHWhuyErHe2aNnaczshEAobMizSQDzsXOFm7YJyQQX23Tir7nAIIURu9Tq6+MuXL6GpqanQc0UtrdW1qoruaFS1mvYrarGtbb+LFy9Gfn6++JGcnKzyOAlRl5jkWKkW3lcxAOkFOYhJpolpmiMOh4OZXlU3KO+/EYKX5aVqjogQQuRTb0lvXl4eLl26hNatWyv0/Jr6z6alpaGoqEhmv1tl1bTfmvr7vkpbWxt8Pl/iQUhzkVWUp9JypOnxbdcd9iZWKCgtxj+3wtUdDiGEyEXupNfZ2Vn8AIBDhw5JLHv1YW9vDwsLC6SkpGDo0KEKBda/f38AwNmz0j+fBQcHS5RRJXXtl5CmwszAWKXlSNPD43Ix/d+RHHZdO4UKQaWaIyKEkNrJPTnFq6MxcDicWvvqamlpYeTIkdiyZQuMjY3rHFhlZSXat2+P58+fVzs5xaNHj8RTCr948QL5+flo3bp1jd0P5JmcwsnJCZqamjQ5BSEyCIRCDNs0r9ouDhwAFnwTnJrzM3hKjuJCGq+yynIM2zQf2cX5WDnyAwzv7KPukAhpdih/UC25h1Z4+vQpgKob05ydnTF+/Hj88MMPMstqaWnB3Nxc4ZEbAEBDQwNbt25FQEAA+vXrJzENcVJSEtatWydOeIGqfrSBgYHYvn27eOpgAMjKysLChQvFf1dUVCArK0uizLp162BmZgYAaNWqFX799VdMmzYNHh4emDRpEoCqaYizs7Oxf//+OiW8hDQ3PC4XCwZOxaJ/Nkqt4/z776KB0ynhbea0NbTwpmcANoYfwI6rJzCskzc4HE7tTySEEDWROyt9dbrfGTNmoG/fvrVOAawsPz8/XLp0CcuXL8f+/ftRUVEBNzc3rFmzRpyM1qaoqEhq5rbi4mKJZStWrBAnvQDw1ltvwczMDKtWrcL27dvB4XDQvXt3LFu2DAMHDlTNwRHShPE4PAAABxywV8ZxsOCbYNHA6fB39VRXaKQBTeg2EH9dOYb4zGRceXIH3i7u6g6JEEKqJXf3BqI4+nmCNDez//4GMcmP8HafUfBy6oysojyYGRjDw86VWnhbmPXnduHva6fh6dARf05dqu5wCGlWKH9QLcX7H8jAGMPOnTtx69YtODg44J133oG+vr4qd0EIUbMHL54iJvkRNLg8TOo+CBaGrdQdElGjqT2HYu/1s4hOeoB7qQnobO2i7pAIIUQmhZpk1q9fDxMTE4SFhUksHzNmDGbPno1ffvkFCxYsgLe3N0pKSlQSKCGkcdgTfQYAENDRixJeAiu+KYZ06gMACIw8qeZoCCGkegolvadPnwaPx0O/fv3Ey8LCwnDs2DGYm5tj3rx56NKlC+7evYsdO3aoKlZCiJplFuXizIOrAIA3PYeoORrSWMzoNQwAEProGp7lpKk5GkIIkU2hpDcuLg6dOnUCj8cTLzt06BA4HA727t2LH3/8ERcuXACfz8fu3btVFiwhRL0O3ghFpVCArrbt0Km1s7rDIY1EWwt7+Lh0hZAx/B11St3hEEKITAolvdnZ2bC2tpZYdunSJZiZmcHPzw8AYGhoCG9vb/FQZ4SQpq2sshwHb4YCAKZSKy95zUyvEQCAoDsXkF0ke/p4QghRJ4WSXqFQiNLS/+ZbLy4uxoMHD+Dt7S1RrlWrVsjJkT2APSGkaTl9/ypyXxagNd8Ufu17qDsc0sh0t3eFm7ULygUV2HdDekZLQghRN4WSXnt7e9y8eVP899mzZyEQCKSS3tzcXJiYmCgXISFE7Rhj4hvYJvcIgAaXV8szSEvD4XAw0+sNAMD+GyF4WV5ayzMIIaRhKZT0DhkyBM+ePcOcOXMQFBSExYsXg8PhYPjw4RLlbt26BXt7e5UESghRn+tJDxCX8Qy6mtoY4+6r7nBII+XbrjvsTaxQUFqMf26FqzscQgiRoFDSu3jxYlhZWeH333/H2LFjERcXh6lTp8LV1VVcJiYmBqmpqejTp4/KgiWEqMfuf1t5R3bpB74ujb1NZONxuZjeq6rxY9e1U6gQVKo5IkII+Y9Ck1NYWVkhJiYGW7ZsQXp6Onr27Ilp06ZJlLl//z5GjRqFsWPHqiRQQoh6PMtJQ0R8VXemN3sEqDka0ti94eaD3yIO4kVBNs4+jMLwzt61P4kQQhoATUPcAGgaQdKUrTm7E3uvB6Nvm67YOPEzdYdDmoCtl4Pwa8QBtLOwx/63V4HD4ag7JEKaJMofVEuh7g2EkJahsPQlgu5EAKDJKIj8JnoMhK6mNuIynuHKkzvqDocQQgBQ0ksIqcHROxF4WV4KZzMbeDl2Vnc4pIng6+pjXLcBAIAdkSfUHA0hhFSRK+nl8XjQ0NBAXFyc+G95HxoaCnUbJoSomUAoxN7oYABVk1HQT9SkLt7qORQaXB6ikx7gXmqCusMhhBD5kl7GGIRCocTf8j5efR4hpOmIiL+B1PxMGOsaYHhnH3WHQ5oYK74phnSqGr0nMPKkmqMhhBA5k16hUAihUIh27dpJ/C3vgxDS9Oz+t5V3XDd/6GhqqTka0hTN6DUMABD66Bqe5aSpORpCSEtHfXoJIVJi0xJx49lDaHB5mNh9oLrDIU1UWwt7+Lh0hZAx/H3ttLrDIYS0cJT0EkKkiCajGNShFywNaSpxoriZXiMAAMfuRCCnOF/N0RBCWjKl7zKLjIxEeHg4UlJSwBiDra0tfH190bt3b1XERwhpYFlFeTjz4CoAGqaMKK+7vSs6W7vgXmoC9l4/iw/7T1B3SISQFkrhpDchIQHTp09HZGQkgKqb2wCI7/Du1asXdu7ciTZt2qggTEJIQzkYE4oKQSW62LSFm7WLusMhTRyHw8FMrxFYeORn7Lt+Fu42bVFY9hJmBsbwsHMFj0s/OBJCGoZCSW9qair69u2LtLQ06OnpYciQIXBycgIAJCYm4syZM4iMjES/fv0QHR0NGxsblQZNCKkfZZXlOBhzDkDVMGWEqIJfux4wMzBGVlEePjrwg3i5paEJFg2aDn9XTzVGRwhpKRRKer/66iukpaVh3Lhx+O2332Bubi6xPisrC3PmzMGhQ4ewfPlybN26VSXBEkLq15kHV5HzsgBWfFNKRIjKhMfdQFZRntTyjMIcLDyyAevGzqfzjRBS7xT6Xen06dOwtrbG7t27pRJeADAzM8OuXbtgbW2NU6dOKR0kIaT+Mcaw+1rVDWyTug+CBpen5ohIcyAQCrE2ZKfMdezff9ee2wkBDW9JCKlnCiW9OTk58PHxgZZW9WN3amlpwcfHB7m5uQoHRwhpONefPURcxjPoaGpjbFc/dYdDmomY5FikF+ZUu54BSC/IQUxybMMFRQhpkRRKem1tbVFcXFxruZcvX1J/XkKaCNEwZW+49YWRroGaoyHNhaxuDcqUI4QQRSmU9E6YMAHh4eF4/vx5tWWeP3+O8+fPY/z48QoHRwhpGCm5GYiIiwEAvNkjQM3RkObEzMBYpeUIIURRCiW9X375Jdzc3DBgwACcOHFCav3Jkyfh7++PLl26YPny5UoHSQipX3uvB4OBwdvFHU5m1uoOhzQjHnausDQ0AaeGMpZ8E3jYuTZYTISQlkmh0RuGDx8OLpeL+Ph4jBo1CsbGxnB0dARQNWRZXl4eAKB3794YPny4xHM5HA5CQ0OVCpoQojpFZS9x9HY4ABqmjKgej8vFokHTsfDIBnDw381rr1o0cDqN10sIqXcKJb3h4eHi/zPGkJubK/OGtStXrkgtE01eQQhpHIJuX0BxeSmcTa3R28lN3eGQZsjf1RPrxs7H2pCdMm9qM9HnqyEqQkhLo1DSGxYWpuo4CCFqIBAKsfd6MICqKYfpSympL/6unvBt1x0xybHIKsqDmYExjt25gON3L+KrE3/gwNuroKulo+4wCSHNmEJJb//+/VUdByFEDS48jkFKXgb4OvoY7uaj7nBIM8fjcuHp0FH8t6ulI64l3kdybjp+Cd+PzwfPUGN0hJDmjjpREdKCiSajGN9tAHQ1tdUcDWlpDHX0sHz4OwCAvdfPIjrxvpojIoQ0Z0olvYwxnDp1CsuWLcN7772Hbdu2iddlZmYiLi4OAoFA6SAJIaoXm56I688egsfhYlL3QeoOh7RQfZy7YHy3AQCA5Sf/RHFZiZojIoQ0Vwonvbdv30aHDh3wxhtvYNWqVdi6dSsuXbokXh8SEoIOHTrQNMSENFJ7oqv68g7q0AuWfFM1R0Nask8GvAlrI3Ok5mfix9A96g6HENJMKZT0pqSkYODAgYiLi8PQoUOxdu1aMCY5EM3o0aOhqamJoKAglQRKCFGd7KJ8nL5fNboKDVNG1E1fWxdfj3gXAHD41nlceXJHzRERQpojhZLeVatWITs7Gxs2bMCJEyewcOFCqTJ6enpwd3dHdHS00kESQlRDIBQiOukBVgfvQIWgEm7WLnCzaaPusAiBp0NHTPl3NsAVJ7egoLT2qe4JIaQuFEp6z5w5A1dXV8ydO7fGco6Ojnjx4oVCgRFCVCs0NhrDNs3DO7tX4tyjawCAZznpCI2lL6akcZjrNwn2JlbIKMzFDyF/qzscQkgzo1DSm5qaCje32gex53A4KCgoUGQXhBAVCo2NxsIjG6QmBsgvLcLCIxso8SWNgq6mNr4Z8R64HA6O372I8Lgb6g6JENKMKJT06uvrIzMzs9ZyT58+hYmJiSK7IISoiEAoxNqQnTKnfxVZe24nBEJhg8VESHW62rbDtF7DAADfnv4LeS8L1RwRIaS5UCjpdXNzw40bN5CVlVVtmaSkJNy+fRvdu3dXODgAiI6OxrBhw2BsbAx9fX14eXnhwIEDddpGWVkZvvnmG7Rt2xY6OjqwtrbGu+++i4yMDKmyiYmJ4HA41T5WrFih1PEQ0tBikmNlTv0qwgCkF+QgJjm24YIipAZz+o2Hs5kNsovzsTp4h7rDIYQ0EwrNyPbWW2/hwoUL+N///oc9e/ZAT09PYn15eTnmzJmDiooKvPXWWwoHFxYWhoCAAOjo6GDy5MkwNDTE4cOHMWnSJCQnJ2PBggW1bkMoFGLUqFEIDg6Gl5cXxo0bh/j4eGzduhWhoaGIjIyEubm51PPc3d0xevRoqeW+vr4KHw8h6pBVlKfScoTUN20NLXz7xvuYvmM5gh9Gwt+1JwZ36KXusAghTZxCSe+sWbOwe/duHDt2DK6urhgypGrIo9u3b2Pu3Lk4duwYnj17hoEDB2LSpEkKBVZZWYl33nkHXC4XFy5cQNeuXQEAX331FXr27IklS5Zg/PjxcHBwqHE7gYGBCA4OxpQpU7B7925wOBwAwO+//44PPvgAy5Ytwx9//CH1vK5du1KrLmkWzAyMVVqOkIbQqbUzZvcZiT8vH8WqM9vQ3c4VpgZG6g6LENKEKdS9gcfj4fjx45gyZQqeP3+OrVu3AgBu3ryJX3/9Fc+ePcO4ceNw5MgRhQM7f/48EhIS8Oabb4oTXgAwMjLCkiVLUF5ejsDAwFq38+effwIAVq9eLU54AeC9996Ds7Mzdu/ejZISmgGINF8edq41TjHMAWDJN4GHnWvDBUWIHN71GYN2FvbIKynCd2e2SY0HTwghdaFQSy8AGBgYYPfu3fjyyy9x6tQpPHnyBEKhEHZ2dhg6dKhEoqqI8PBwAMDgwYOl1gUEVI3lGBERUeM2SktLERUVhfbt20u1CHM4HAwaNAh//PEHrl+/jr59+0qsT01NxaZNm5Cfnw9LS0v4+vrCxcVFiSMiRD1CHkahpKJM5jrR18BFA6eDx1VqVnJCVE6Tp4Fv33gfU7d/ibC46zh1/zKGd/ZRd1iEkCZK4aRXxNXVFa6uqm8hio+PBwC0bdtWap2VlRUMDAzEZaqTkJAAoVAocxuvbjs+Pl4q6Q0JCUFISIj4bw6Hg6lTp+L333+Hvr5+jfstKytDWdl/SQYN20bUJTE7Fd+crvolZqBrT9x9/ljipjYLvgkWDZwOf1dPdYVISI3aWzrgPZ8x2HThEL4/G4geDh1haUijAhFC6k7ppLe+5OfnA6jqziALn88Xl1FmG6+WA6pmkvvyyy8xevRouLi4QCgUIiYmBkuXLsWuXbvw8uVLHD58uMb9rl69Gl9//XWNZQipbyUVZfjsyC94WV6KHvYdsGb0xwCqRnPIKsqDmYExPOxcqYWXNHqz+oxEWHwMHrx4gm9ObcWvEz+T6K5GCCHyoE+711hYWOCbb76Bh4cHjIyM0KpVK/j7++P8+fNo3749jhw5gpiYmBq3sXjxYuTn54sfycnJDRQ9If9ZczYQ8ZnJMNU3wurRH4LH5YLH5cLToSOGduoDT4eOlPCSJkGDy8O3I96DFk8TlxNu4+jtmru2EUKILI32E0/UOltda25BQUG1Lbh12car5Wqip6eHadOmAQAuX75cY1ltbW3w+XyJByEN6didCzh6OwJcDgffj/oQ5gat1B0SIUpxMbfFnP7jAQDrzv2N1PzaJ0gihJBXNdqk99X+tq9LS0tDUVFRtX11RZydncHlcqvt+1tTv2FZzMzMAADFxcVylSdEHR5nJGPVme0AgA/6joOnYyc1R0SIakzrOQzutm1RXF6Kr0/+CSGjWQQJIfJrtElv//79AQBnz56VWhccHCxRpjq6urro2bMnHj16hKSkJIl1jDGEhIRAX18fPXr0kCumqKgoAICjo6Nc5QlpaC/LS/HZP7+gtLIcvZ3c8Lb3KHWHRIjK8LhcfDPifehoaCEq8T723whBdNIDnL5/BdFJD2gqbUJIjRpt0uvv7w9nZ2fs2bMHt27dEi/Pz8/HqlWroKWlhenTp4uXv3jxArGxsVJdGd59910AVf1sXx3j8Y8//sCTJ08wdepU6OrqipffvHlT5liQR44cQWBgIFq1aoWhQ4eq6jAJURnGGL49/ReeZqfCwrAVVo6cAy6n0b7FCVGIg4kV5vpVTXq05uxOvLN7JRYHbcI7u1di2KZ5CI2NVnOEhJDGqtGO3qChoYGtW7ciICAA/fr1k5iGOCkpCevWrZNocV28eDECAwOxfft2zJw5U7x8xowZ2L9/P/bu3YunT5+if//+ePz4MY4cOQInJyd89913Evv95JNPkJCQgN69e8PW1hYCgQAxMTG4dOkStLW1sWPHDrn6ABPS0A7fCsPp+1fA43CxZvTHMNGnvuSkeTI3lN1HPaMwBwuPbMC6sfNpGD5CiBSFmoG++eYbHDt2rNZyx48fxzfffKPILgAAfn5+uHTpEry9vbF//35s3rwZlpaW2LdvHxYsWCDXNrhcLoKCgrBixQpkZmbip59+wuXLl/H222/j6tWrMDc3lyj/1ltvoXPnzoiMjMTvv/+OP/74A6mpqfjf//6HW7duYeTIkQofDyH1JTYtEWvP7gQAfOw3Cd3s2qs5IkLqh0AoxLqQXTLXiX6jW3tuJ3V1IIRI4TAF5nXkcrmYOXMmtm3bVmO5d955B9u2bYNAIFA4wOZANNJEfn4+jeRAVK6w9CXe3L4Mybnp6N/WAz+N/4S6NZBmKzrpAd7ZvbLWcn9OXQpPh44NEBEh9YfyB9Wq109GoVBIA4gTUo8YY/j61J9Izk1HayMzfDPiPUp4SbOWVZSn0nKEkJajXj8dk5OTYWBgUJ+7IKRF23f9LM7FXoMGl4e1oz+GkS6930jzZmZgrNJyhJCWQ+4b2Xbu3Cnx9+PHj6WWiVRWVuL+/fsICwtD7969lYuQECLT3dQErA/dDQD41P9NuNm0UXNEhNQ/DztXWBqaIKMwB9X1zdPV1Ia7TbsGjYsQ0vjJ3aeXy+WKuyowxmrttsAYA5fLxZEjR1r8zV/UJ4eoWn5JESZvW4oX+VkY2N4TP4ydR12JSIsRGhuNhUc2AEC1ie9A155YNXIOtDQ0GywuQlSN8gfVkruld/r06eIP1cDAQLi4uMDb21tmWS0tLdja2mL06NFwc3NTTaSEEABVXyi/OvEHXuRnwa6VJZYPf5cSXtKi+Lt6Yt3Y+VgbshPphTni5ZZ8Ewzp0Ad7rp/BudhrKCp7iR/HfQI9LR01RksIaSzqdfQGUoW+qRFVCow8iZ/O74EWTxOBM5ajg5WTukMiRC0EQiFikmORVZQHMwNjeNi5gsflIvLpPXxy6EeUVJTBzdoFv05aRP3dSZNE+YNqKZT0JiUlwcDAAKampvURU7NDJy1RlZvJj/C/Xd9BwIRYNmQ2xnv4qzskQhqlu88f48P9a1FQWow25rb4bfIXsKhmUgtCGivKH1RLodEbHBwcKOElpAEIhEJEJz3A6ftXcP7RdSz65xcImBBDO/XBuG4D1B0eIY2Wm00bbJv2FcwNWuFxZgpm/f01knPT1R0WIUSN5GrpffbsGQDAxsYGPB5P/Le87O3tFYuumaBvakQRobHRUn0WAcDCoBWOvr+O+ikSIofneRl4f+/3SM5Nh5m+MX6b8jnaWbTszyTSdFD+oFpyJb1cLhdcLhcPHjxAu3btJEZyqHUHHA4qKyuVDrQpo5OW1JXo7vTq3pzrx86Hv6tng8ZESFOVVZSHOfvWIC7jGQx19LBx4mfoaktDmpHGj/IH1ZJr9AZ7e3twOBxoampK/E0IUT2BUIi1ITurTXg5ANae2wnfdt3B49Lsa4TUxszAGFvfWoa5B9bhVkoc3t+zGuvHzYe3i7u6QyOENCCFbmQjdUPf1EhdRCc9wDu7V9Za7s+pS+Hp0LEBIiKkeSipKMPCIz/jcsJtaHB5WDnyAwR0pAmUSONF+YNqUTMRIY1MVlGeSssRQqroampjw/hPEdDBC5VCAb44ugmHbp5Xd1iEkAYi9+QUhJCGoa2hJVc5MwPj+g2EkGZIk6eBVaM+BF9HHwdvhuK703+hoKQIs3q/ASFjMsf9JYQ0D5T0EtKIRD69h1Vnap70hQPAgm8CDzvXhgmKkGaGx+ViyZBZMNI1wNYrQfglfD9uP49HbFqi5AxvhiZYNGg63TRKSDOh0FdYHo8n10NHRwe2trYYM2YMjh07purYCWk2KgSV+On8Hry/dzWyivNhaWgCoCrBfZXo70UDp1MLFCFK4HA4+Mh3Ihb4TwUARMTHSA0PmFGYg4VHNiA0NlodIRJCVEyhT03GmFyP8vJypKamIigoCGPGjME777yj6vgJafKSctIwI3AFAiNPAgAmdPPH0ffXYf3Y+bD4N/kVseCbYB0NV0aIyrzpOQR8HX2Z60R3ea89txMCobDhgiKE1AuFujcIhUIsXrwYv/76Kz744ANMnToVjo6O4HA4SExMxO7du7F582Z88MEH+Pjjj3H+/HksXLgQ27Ztw6BBgzBx4kRVHwchTQ5jDMfuXsD3wYEoqSiDka4Blg97BwPa9wAA+Lt6wrddd+pjSEg9ikmORUFpcbXrGYD0ghzEJMfSaCmENHEKJb1///031q1bh7CwMPj4+Eis69KlC7p06YKRI0fC19cXHTt2xIwZM+Di4oK+ffti27ZtlPSSFq+gtBirzmzHmQdXAQA97Dtg5cgPYMmXnN6bx+XSBy0h9YhGSyGk5VCoyWjjxo3w8fGRSnhf5e3tDR8fH/z666/iv93d3XHz5k3FIiWkmbiVEodJfy3BmQdXweNw8bHvRPzx5hKphJcQUv/kHQWFpv0mpOlTKOl9+PAhbGxsai1nbW2N2NhY8d8uLi7Iy8tTZJeENHkCoRB/XDyCt//+Fi/ys2BrbIEd05fj7T6jqMsCIWriYecKS0MTqZtGX/fV8d+xNzoYFYLKBomLEKJ6Cn3Samho4N69e7WWu3//PjQ0/utBIRAIoK8v+4YBQpqzF/lZeGf3d9h88TAETIjhnb2x7+2VcLNpo+7QCGnReFwuFg2aDqD60VIsDU2QX1qMNSE7Me7Pz3H+UTRoMlNCmh6Fkt5evXrh7t272LRpU7VlfvvtN9y5cwe9evUSL3v27BksLS0V2SUhTYJAKER00gOcvn8F0UkPIBAKEfIwChP/WoyY5EfQ19LBypEfYOXIOTDQ1lN3uIQQVN00uq6a0VLWj52Pkx9uwLKhb8NEj49nOWn49PAGzP77W9x9/lhNERNCFMFhCnxdvXTpEnx9fcEYQ58+ffDmm29KjN6wd+9eXLp0CVwuF+fPn0ffvn2Rnp4OGxsbzJ49G1u2bKmPY2m0aO7sliE0NhprQ3ZKjPWpo6mN0ooyAEBnaxesHvUh7FrRFz9CGiOBUFjjaCnFZSXYEXkCf0edQmllOQBgSMfe+Nh3ImyMLdQVNmnGKH9QLYWSXgDYt28f3n33XRQVFYHDkfxRiDEGPT09/PHHH5g6tWrg7+TkZJw/fx5eXl5o37698pE3IXTSNn+hsdFYeGQDqnsz+bf3xPejP4ImjyZBJKSpSy/IxqYLh3D8zkUwMGjyNDClRwD+12cU+Lr/deGrLYkmpDaUP6iWwkkvALx48QJ//fUXIiIikJKSAgCwsbFBv3798Pbbb8t1s1tLQCdt8yYQCjFs0zyp2ZxeZck3wak5P9MHHiHNSGx6In4K3YOoxPsAACNdA7zrPQYTuw/EhfibUr/80LTGpK4of1AtpZJeIh86aZu36KQHeGf3ylrL/Tl1KY25S0gzwxjD5Se38WPoHjzJeg4AMNU3QnZxvlRZ0W+iNKsikRflD6pFv7USoqDishKceXAV268el6s8DW5PSPPD4XDg49IVXk5uCLodgU0RB2UmvEDV7G4cVE1r7NuuO/3yQ0gDUzrpjYyMRFhYGJ4/r/qGa2NjAz8/P3h5eSkdHCGNDWMMd54/xj+3wxD8IBIl/96kJg95B8EnhDQ9GlwexnUbAEtDE3x04Idqy9G0xoSoj8JJ77NnzzB16lRcuXIFAMRjFopuavP29sauXbtgb2+vgjAJUa+8l4U4ce8S/rkVjoSsFPFyR5PWGN21P3ZfO4OsojyZN7JxUDX0kYeda4PFSwhRj8Kyl3KVe5qVSkkvIQ1MoaQ3Ly8Pfn5+ePr0KXR0dBAQEAAXFxcAwJMnT3DmzBlcunQJ/v7+uH79OoyMjFQaNCGqUtPd1UImRHTiA/xzOxyhj6LFMzHpaGhhUIdeGNvVD11t24HD4cDO2AoLj2wAB5BIfEV9+BYNnE4/ZRLSAsj7i86q4O04df8y/F17wr99D1gbmddvYIQQxW5k+/LLL7Fy5UoMGzYMW7ZsgbW1tcT6tLQ0vPPOOzh16hSWLl2Kb775RmUBN0XUEb1xkjWurqWhCd7zGYvsl/k4ejscz/MyxetcLR0xtqsfhnTqDb6O9MyCMrfHN8GigXS3NiEthWg0l4zCnGqHMNTk8lAhFEgs62jlBH9XT/i394SjqbXM59EQaC0P5Q+qpVDS27lzZ2RmZuLp06fQ05M9q9TLly/h5OQEc3NzuaYsbs7opG18ahtXV8RAWxdDO3ljbFdfdLByqnW79KFECBFdXwDZv/ysGzsfnaydcf5RNEIfXcfN5FgIX/kodjGzFSfA7SzsweFwqv2STkOgNW+UP6iWQkmvvr4+hg8fjgMHDtRYbuLEiTh58iSKi4sVDrA5oJO2cZFnXF1NngaWDpmFgA5e0NXSacDoCCHNQV1++ckpzsf5uBs4/yga1xLvo/KVVmC7VpZoY26LsLgbUvugIdCaP8ofVEuhPr08Hg8VFRW1lqusrASXWrmICinaklpeWYHY9ETcfZ6AiMcxNSa8AFAhqISNsQUlvIQQhfi7esK3XXe5rlcm+kYY320AxncbgIKSYlx4HINzj6Jx9ckdJOemIzk3XeY+lB0CjX6ZIi2NQklv27ZtER4ejry8PBgbG8ssk5OTg7CwMLRr106Z+AgRk/fnPcYYknPTcTc1AXefP8bd1Md4lJ4k0XoiDxpXlxCiDB6XW+cRGvi6+hjh1hcj3PriZXkpAiNP4o9LR6otLxoCbVPEAQzp1AdOptZyTXdO3SVIS6RQ0jthwgQsWbIEw4cPx5YtW9CpUyeJ9Xfv3sV7772HgoICTJo0SSWBkqZJVS0J1fXBzSjMwYIjG/CO92jwuFzcTU3A/dQE5JUUSW2jla4hOtu4wESPj6A7F2rdJ42rSwhRJz0tHTiatpar7Larx7Ht6nFo8jTgbGaDdhb2aG/pgPYW9mhn6QAjXQNx2ZqupwuPbFC4uwS1HJPGTqE+vSUlJejTpw9u374NLpeLbt26wcmp6iafJ0+e4NatWxAKhejatSuuXLkCHZ2W/RNxS+2To6qWBHn64L5Ok6eBDlaO6GzdBm7WLnCzbgMbY3NwOJxa764Wjat7as7PdMEmhKiVvNOctzW3w4uCLBSVlchcb8U3RXtLB7Qxt8OhmFDkl0o3DACKX/+o5bh+tNT8ob4olPQCQG5uLj744AMcOnQIQqFQYh2Xy8WECROwadMmmJiYKBVgdHQ0li9fjitXrqCiogJubm749NNPMXHiRLm3UVZWhjVr1uDvv/9GcnIyTExMMGLECHz33XewsLCQ+Zzdu3fj559/xv3796GlpQVvb29888038PDwqPMx1OdJq+pv1vXdMlvTjRfllRVIK8hGWkE2UvOzkFaQhRf52YhLT8LD9MRa9+nl2Bm+7bqjs7UL2ls61PgTnzx3V9OFmhCibnX5ks7lcJCan4lH6c/wKD0JcRnPEJeRJDH0orzWjZ2HAe17gMup/fqvyPVeHo31860hUdKrWgonvSLJycm4cOGCxDTE/fr1g52dndLBhYWFISAgADo6Opg8eTIMDQ1x+PBhJCUlYd26dViwYEGt2xAKhRg2bBiCg4Ph5eWF/v37Iz4+Hv/88w+cnJwQGRkJc3PJQcFXrlyJZcuWwcHBAePGjUNhYSH27duH8vJyhIaGwtvbu07HUV8nraq/WTdky6yhth5GufsivSBbnNxmFefVOeZXrR71IYZ26iN3eRpXlxDSFCj7Jb2w9CXiM6oS4dBH0bj+7KFc+9Xg8mBmYAwLw1awMGgFc0OTqv//+7eFoQlM9Y0w7s/Pq73eN5aW4/poiW6IJJqSXtVSOumtL5WVlXB1dUVKSgoiIyPRtWtXAEB+fj569uyJxMRExMXFwcHBocbtbN++HbNnz8aUKVOwe/du8TTJv//+Oz744AO8++67+OOPP8Tl4+Pj0bFjRzg7O+PatWvi2eRu3boFLy8vODs74969e3UalaI+TlpVf7OWd3uMMZRWlKGgtFjG4yUKS4oRn5mMsLjrCh2XjoYWWhuZwYpvitZGZmjNN0VJRRm2XT1e63P/nLq0zjeNNMVv/oSQlkdVX9Ll7S6haosGTkMPx44w0NaFgbYeDLR1q21FVtfnW100VHcOSnpVq9EmvWfPnkVAQABmzZqFbdu2SawLDAzEzJkz8fXXX+Orr76qcTt9+vTB1atXkZiYKJEgM8bQpk0bpKenIzMzE7q6ugCAJUuWYPXq1QgMDMT06dMltjVr1izs2LEDERER6Nevn9zHouqTtraWVNE36xMf/IRKoQBlFeUorSwX/1taUY6yynKUVpShtLICL8tLsP7c7hrnjOdxuODr6KOw7GWdR0GojreLO/o4dUFrI1NY8c3Q2sgMxroG4i8mrx8v9cElhLRkqviSLu/19Nj7PyLvZQEyCnP/feQgo+jff19ZVlJRptCxcMCBvrYODLT1YKitV5UI6+hCX0sXEfExNW7XRI+PXyd+Bl0tHWhrakKbp1X1r4YWNLg8ic8QeT8v6/L5UV/dOWShpFe15Bq94dmzZ0rtxN7evs7PCQ8PBwAMHjxYal1AQAAAICIiosZtlJaWIioqCu3bt5dqEeZwOBg0aBD++OMPXL9+HX379pVrv4okvaoWkxxbY9cB0RA2nmtmqGyfAiZEbkmh+G8NLg+GOvrg6+iDr6Mn/r+hjj6Ky0pw6v7lWrc502uEXC2zPC4XiwZNx8IjG8CB7J/3Fg2cTgkvIaRZU2QINFnbkOd6qq2hCUu+KSz5ptVuizGGi49vYe7BdbXu18KwFSoFAhSWvUSFoBIMDEVlJSgqK0Easut0DDkvC/Dmji9lruNyONDW0IK2RlUSLGQMmUW51R8Dqj4vPzn4I1obm4HH5UGDy4MGr+pfTS4PGjwNaHB54HG54HK4+O3CIZlfGJQdN5nUP7mSXkdHR6nWN3lxOBxUVlbW+Xnx8fEAqsYEfp2VlRUMDAzEZaqTkJAAoVAocxuvbjs+Pl6c9MbHx8PAwABWVlY1lq9JWVkZysr++5ZaUFAAAMjOzkZ5eXmNz5XH0xfJdX6OBpdXdSHgaUJLQxM6GqJ/tVBYVoKnOam1buN/PUdgQJseMNTWg46GVrXnhEAoxLXE+zX20TXXN4a9rhmysrLkit/dzAlfDZqNTVeOSGzXTN8Yc/qMhbuZk9zbIoSQlkyV19P2RjYw0zeu9Xr/96SvxElgeWUFistLUFxeiqLyEvH/i8tLcCs1HqGPpWefe52Bli44HA7KKitQLvhvsiwhYyipKKtzC/SFhJt1Kl8dURIddi8KXa1l5x51UVhYWHshIje5kl57e3uFk15F5efnA4C4T+3r+Hy+uIwy23i1nOj/1Y3oIKu8LKtXr8bXX38ttfzYsWPibhTKeF6eJ1e5AMOOsNNsBQ0OD1xZrx8DUAE8r8zDU9Se9GbGJuPSE/negN251ghG9XF6cK0RdPSoXNt61XidLnjBy8dLVg49jhZaaxoh82YCjtxMqPO2CCGkJVPV9VSV13uDcvkayfx12sFGyxhAVYuzAAyVTAABhKhkQgiYEJUQIq0iH5eKaz8eV21LGHC1IQADA4OQMQjF/wqrljOGAkEpMgS1fw4GR4TiifZduY6lJiUlsoegI4qRK+lNTEys5zCal8WLF+PTTz8V/11QUAA7OzuMHDkShoaGSm9fIBTi8t6va/1m/emkd+T6eUXe7c2ZOLNOP9d4Pb0t1ZJg/m9LQl8nd7m3QwghpHFT1fVe1Z9HAqEQU+XY3s9TvpBre7dS47HwxK+1lgvo76+ylt758+crvR1SRaEZ2RqCqHW2ulbVgoICtGrVSultvFpO9P+6lJdFW1sb2traUstNTU1V1hF9ccDMGoew+SJgJiyrabFuiO0BwBgzf4zs7kejIxBCSDOnyut9Y/588zMxgWXEnlpvBPTr3Esln3VaWlpKb4P8p9FmHzX1n01LS0NRUVG1fXVFnJ2dweVyq+2DK6vfcNu2bVFUVIS0tDS5yquLv6sn1o2dDwtDyck/LPgmCt05qurtiYhuvBjaqQ88HTpSwksIIc2Uqq73jfnzTXQjIPBf0ixCN1Y3fo22pbd///5YvXo1zp49i8mTJ0usCw4OFpepia6uLnr27InIyEgkJSVJDVkWEhICfX199OjRQ2K/V69exdmzZ6WGLJN3vw3F39UTvu26q6wlVdXbI4QQQhTRmD/fREn06+P0WtDkRo1eox2nt7KyEu3bt8fz58+rnZzi0aNHcHR0BAC8ePEC+fn5aN26tUT3g7pOThEXF4dOnTo1+skpCCGEEKI+NCNb09Nok16gbtMQz5w5E4GBgdi+fTtmzpwpXi5rGuLHjx/jyJEjcHR0RFRUVJOdhpgQQgghzRflD6rVqH+39vPzw6VLl+Dt7Y39+/dj8+bNsLS0xL59+yQS3ppwuVwEBQVhxYoVyMzMxE8//YTLly/j7bffxtWrV6USXgBYunQpdu3aBXNzc2zevBkHDhxA3759ceXKlTonvIQQQgghRP0adUtvc0Hf1AghhBBSV5Q/qFajbuklhBBCCCFEFSjpJYQQQgghzR4lvYQQQgghpNlrtOP0NieibtOiGd0IIYQQQmojyhvo9ivVoKS3ARQWFgIA7Ozs1BwJIYQQQpqawsJCiTkIiGJo9IYGIBQKkZqaCkNDQ/HkGCKenp6Ijo6u87JX/y4oKICdnR2Sk5Pr7e5OWTGp8nm1latuPdWffOWo/pQrR/WnXDmqP+XK1aX+ZC2n+mu69ccYQ2FhIaytres0KRaRjVp6GwCXy4Wtra3MdTweT+qNIs8yWWX4fH69XbRk7U+Vz6utXHXrqf7kK0f1p1w5qj/lylH9KVeuLvUnaznVX9OuP2rhVR362qBmH374oULLZJWpT4ruT97n1VauuvVUf/KVo/pTrhzVn3LlqP6UK1eX+pO1nOqvadcfUR3q3tAM0ODVyqH6Uw7Vn3Ko/pRD9accqj/lUP01LdTS2wxoa2tj+fLl0NbWVncoTRLVn3Ko/pRD9accqj/lUP0ph+qvaaGWXkIIIYQQ0uxRSy8hhBBCCGn2KOklhBBCCCHNHiW9hBBCCCGk2aOkl6CwsBDvv/8+rK2toa+vj27duuHQoUPqDqvJcHR0BIfDkXr88MMP6g6tyXj27BkmTpyIVq1aQV9fH56ennj+/Lm6w2oSVqxYIXXu9ejRQ91hNUkffPABOBwOfv31V3WH0mT89NNP6NSpEwwMDGBsbIwBAwYgKipK3WE1GatXr0aPHj1gaGgIS0tLTJw4EYmJieoOq9mipJfgk08+QXh4OA4cOIC7d+9i4sSJmDx5Mu7cuaPu0JqE6OhovHjxQvzYs2cPAGDs2LFqjqxpyM7Oho+PD4yNjXHu3DncuXMHX331Fd0NXQfu7u4S52BwcLC6Q2pyTpw4gatXr8La2lrdoTQpDg4O+PHHH3H79m1cuXIFbdq0QUBAALKzs9UdWpMQERGBjz/+GFFRUThz5gxycnIwdOhQVFZWqju0ZolGbyDo3Lkz3nrrLXzxxRfiZaampli/fj1mzpypvsCaqKlTpyIlJQURERHqDqVJWLRoEaKioqi+FLRixQqcOHEC169fV3coTVZ6ejp69OiBU6dO4Y033sDChQvx0UcfqTusJkk0bm14eDj69++v7nCanKdPn8LZ2Rm3b99Gly5d1B1Os0MtvWqya9cuvPfee+jRowe0tbXB4XCwY8eOGp8THR2NYcOGwdjYGPr6+vDy8sKBAweUjqVPnz4ICgpCWloaGGM4ePAgysrKGvUFqzHV36vy8/Pxzz//YNasWSrdrqo1pvo7fvw4PDw8MG7cOFhYWMDT0xNHjhxRerv1qTHVHwA8fPgQrVu3Rps2bTBr1iykpaWpZLv1pbHV36xZszB37ly4ubmpZHv1rbHVn0h5eTm2bNmCVq1aNeq6bKz1B1R9hgCAiYmJyrdNADCiFg4ODgwAMzMzE/9/+/bt1ZY/f/4809TUZIaGhuydd95hn376qfh569atUyqWkpISNmXKFAaAaWhoMD6fz4KDg5XaZn1rTPX3qs2bNzMDAwNWVFSksm3Wh8ZUf9ra2kxbW5utWLGCxcTEsB9++IFxuVwWERGh1HbrU2Oqv1OnTrEDBw6wO3fusNOnTzNPT0/WoUMHVlpaqtR261Njqr+NGzeyfv36MYFAII5t48aNSm2zvjWm+mOMsQsXLjB9fX3G5XKZtbU1u379utLbrE+Nrf5EKisrmb+/Pxs2bJjKtkkkUdKrJiEhISwxMZExxtjq1atrfNNVVFQwFxcXpq2tzW7evClenpeXx9q1a8e0tLTE2xL5/PPPGYAaHyLff/8969ixIzt9+jS7desW+/bbb5mxsTG7f/++yo9bVRpT/b3K09OTzZ49WyXHWJ8aU/1pamoyHx8fieePHDmSTZ06VTUHWw8aU/29LiMjg+no6LBDhw4pfZz1pbHU38OHD5mFhQV7+vSp+LlNIeltLPUn8vLlSxYfH88iIyPZ22+/zZydnVlmZqZKj1mVGlv9McaYUChk//vf/1jbtm1ZRkaGyo6VSKKktxGo7U0XHBzMALBZs2ZJrduxYwcDwL7++muJ5RkZGezhw4c1PhirulhpamqyM2fOSDx/4MCB7MMPP1TNAdYzddbfq+7du8cAsIsXL6rkuBqKuuvPzs6Ovf322xLP//zzz1nfvn2VP7gGoO76k8XV1ZX98MMPCh9TQ1Jn/W3fvp1xOBzG4/HEDwCMy+Uyd3d3VR9qvWiM51+bNm3Y2rVrFT6mhtQY6k8oFLL333+fOTo6smfPnqns2Ig0DdmdHkhjEh4eDgAYPHiw1LqAgAAAkLoJyNzcHObm5rVuu6KiAhUVFeDxeBLLeTwehEKhghE3LvVZf6/atm0b2rZtCx8fH8UCbaTqu/769OmDx48fSyyLi4uDg4ODAtE2Pg11/onk5uYiKSkJjo6OCj2/sanP+hs9erTU8G4BAQGYOXNmo++XL6+GPv8AgDGGsrIyhZ/fmNR3/THG8OGHH+LkyZOIiIiAnZ2dcgGTGlHS2wTEx8cDANq2bSu1zsrKCgYGBuIydcXn89G3b18sXLgQv/76K6ytrXHs2DGEhITg1KlTSsXdWNRn/YlUVlZi165dmD9/vlLbaYzqu/4++eQTeHt7Y/369Rg1ahTOnTuH48ePN5vRHOq7/j777DO88cYbsLe3R0pKCpYsWQIbGxsMGzZM4W02JvVZf8bGxjA2NpZYpqmpKb4psDmo7/Pv888/x8iRI2Fra4ucnBz89ttvSElJwbhx4xTeZmNS3/X34YcfYu/evTh+/Dh0dXXFN6GamJhAS0tL4e0S2SjpbQJEd3MaGRnJXM/n88VlFLFv3z58/vnnmDBhAvLz89GmTRvs2LFD/C22qavv+gOAkydPIisrC9OnT1dqO41Rfddfr169cPDgQSxduhTLli1Du3btcPDgQfTp00fhbTYm9V1/ycnJmDx5MrKysmBpaYn+/fvj77//hp6ensLbbEwa4v3bnNV3/aWmpmLy5MnIyMiAiYkJPD09cfHiRXTo0EHhbTYm9V1/mzdvBgD07dtXYnlYWBh8fX0V3i6RjZJeAmtra/z999/qDqNJGzVqFAQCgbrDaLLGjBmDMWPGqDuMJmnfvn3qDqFZodmw6oY+O5TDaKqEBkXj9DYBom+Y1X2bFA0GTmSj+lMO1Z9yqP6UQ/WnHKo/5VD9NS+U9DYBor5EsvoNpaWloaioSGZ/I1KF6k85VH/KofpTDtWfcqj+lEP117xQ0tsEiGZGO3v2rNS64OBgiTJEGtWfcqj+lEP1pxyqP+VQ/SmH6q95oaS3CfD394ezszP27NmDW7duiZfn5+dj1apV0NLSapY3UKkK1Z9yqP6UQ/WnHKo/5VD9KYfqr3nhMOpFrRZbt27FpUuXAAB3795FTEwMvL29xcPk+Pj44H//+5+4fFhYGAICAqCjo4PJkyfD0NAQhw8fRlJSEtatW4cFCxao5TjUhepPOVR/yqH6Uw7Vn3Ko/pRD9deCqXVqjBZsxowZNU5ROGPGDKnnREVFsSFDhjA+n890dXVZz5492b59+xo++EaA6k85VH/KofpTDtWfcqj+lEP113JRSy8hhBBCCGn2qE8vIYQQQghp9ijpJYQQQgghzd7/27v/kKjvPw7gz4/Z3WkXK80r1KVWh0rWefqHY6behs0fWc6UbSTjJoNasLHRtmJC/oBBtaBRM2puShub+1FskWFiE68VorGoU1aNyDsrK1Mb69IrO31///B7n/XpNC0vi9vzAYJ8Xu97v9+f9wfk5YfX+95MeomIiIjI5zHpJSIiIiKfx6SXiIiIiHwek14iIiIi8nlMeomIiIjI5zHpJSIiIiKfx6SXiIiIiHwek14iolHY7XZIkoTIyMgpGU8Ige3btyMuLg4BAQGQJAmSJMnxu3fvori4GHq9Hmq1ekrnRkTkC5j0EtGUiIyMhCRJsNvtXu23rKwMkiShrKzMq/1OtT179mDjxo24cuUKsrKyYDabYTab5fjmzZuxZcsWOBwO5Obmwmw2o6CgYErn6CtrTUT/Tf5PewJERAT8/PPPAID9+/dj+fLlY8aPHz8OvV4/pXMjIvIFfNNLRPQMuHTpEgCMmdCOFycioodj0ktET9S+ffsgSRI6OzsBAFFRUXK9qiRJsFgsivYnT57Ea6+9htDQUKhUKuh0OqxcuRJHjx716FuSJJSXlwMAysvLFf2+9dZbcruzZ8+itLQUycnJCAsLg0qlQnBwMNLT0+U3qN7kcrnw9ddfw2QyISgoCGq1GlFRUVi/fj0uX76saGsymSBJEmw2GwDl+pSVlcllIUII+Z7dP/v27VP0derUKRQWFmL+/PlQq9UICgpCRkYG6urqHjrX6upqpKenY86cOVCr1QgPD0d6ejq++OILud1E1xoAbt68ieLiYixevBiBgYGYOXMmEhMT8dlnn8HpdHrMwWKxQJIkmEwmDAwMoKSkBLGxsQgMDGTdMhF5DcsbiOiJWrRoEcxmMw4cOID+/n7k5+dDq9XK8Xnz5sm/f/XVV3jnnXcwPDwMo9EIk8mEzs5OHD58GIcPH0ZZWRlKS0vl9mazGWfOnIHVaoXBYEB8fLwcW7Zsmfz7jh07UFVVhZiYGCxZsgSzZs3CpUuX0NTUhMbGRrS0tGDHjh1euV+Hw4FVq1bBYrFAq9UiMTERISEhaG9vx969e7F//34cPXoURqMRAJCZmYnIyMhR1yc+Ph4FBQXo7e3FN998I9/z/WvrtnPnTmzYsAHDw8OIj49HUlISrl+/DovFgoaGBpSXl6OkpEQx13/++Qc5OTk4ceIEpk+fjhdffBGhoaG4fv062tra0NjYiPfee++R1rqjowMvv/wyOjs7ERISguzsbNy7dw9NTU3YtGkTfvrpJ/z222+YPXu2x9rduXMHJpMJZ8+eRWpqKgwGA/r6+ib5RIiI/k8QEU2BiIgIAUDYbLZR421tbcLf319IkiS+/fZbRayurk6oVCoBQDQ0NChipaWlAoAoLS0dc2yLxSIuXrzocf38+fMiPDxcABCtra2KmM1mEwBERETEhO7Pbc2aNQKAyMnJEd3d3YrY559/LgAIvV4vXC6XIjbe+gAQY/3Jrq+vF5IkiTlz5ohjx44pYm1tbfI9WiwWRWz16tUCgDAajR7j3rt3Txw8eFBxbSJrnZSUJACIVatWidu3b8vXb9y4IRISEgQAsWbNGsVnmpqa5PtbunSpuHbt2pj9ExE9LpY3ENEzYefOnXC5XMjLy8Obb76piGVlZWHt2rUAgO3btz9y32lpaViwYIHH9ejoaGzevBkAcODAgceYtdK5c+fwww8/IDQ0FDU1NdDpdIr4Bx98gOzsbFy4cAFHjhyZ9HhupaWlEEJg7969SE1NVcSWLFkiv8W+v1zBarXil19+gUajQW1trUcZgb+/P3Jzcx9pHidOnEBraysCAwNRWVmJGTNmyLGQkBBUVlYCAH788UdcuXJl1D4qKioUb/+JiLyF5Q1E9Exw1/Y+WB/q9vbbb6OiogLHjx/H0NAQpk2b9kj93759G0eOHMHp06fR29uLwcFBAMC1a9cAAH/99ddjz92trq4OQghkZWVh5syZo7YxmUyoq6tDc3MzcnJyJj1mb28vTp48iYCAAKxcuXLMMQGgublZvlZfXw8AWLFiBcLCwiY9D+DfZ5iZmYm5c+d6xBMTE2EwGGC1WnHs2DEUFhYq4jqdDikpKV6ZCxHRg5j0EtEzoaurC8DIRq7RLFy4EMBI3WdfX5/HW9SHqa2tRVFR0UPrQ2/duvUIsx1dR0cHAKCqqgpVVVUPbdvT0zPp8QDAZrNBCAGn0wm1Wj3hMd0bC2NiYrwyD2D8ZwiMPEer1Sq3vR83rRHRk8Skl4h8WldXF15//XU4nU5s3LgRhYWFiIyMhFarhZ+fHxoaGpCRkSF/O8JkDA8PAxjZgGYwGB7aNikpadLj3T+mVqtFfn6+V/p8WgICAp72FIjIhzHpJaJnQlhYGC5evIiOjg7ExcV5xN1vUTUaDYKCgibcb21tLZxOJ/Ly8rBt2zaP+IULFx5/0g94/vnnAQDJycmoqKjwWr8TGVOSJFRXV8PPb2JbNebPnw8AOH/+vNfm4i6TcD+r0bhj3iqpICKaKG5kI6IpoVKpAIx8L+xo3HWnD373rFt1dTUAICUlBf7+//6/Pl6/N2/eBABERER4xIQQqKmpGX/yE5SVlQUAOHToEO7cueO1fh8mNDQUS5cuhcPhkOt0JyIzMxPASB3y1atXJ/SZiT7D+vp6dHd3e8RPnz6NM2fOwM/Pz2PDHRHRk8akl4imRHh4OADgzz//HDX+/vvvw9/fHwcPHsR3332niDU0NODLL78EAHz00UeP1G9sbCyAkW9ncG9aA4ChoSGUlJQoNndNltFoRH5+Pi5fvozVq1fDbrd7tOnv78f3338/alL4uD799FMAQFFREWpraz3iQgi0traioaFBvhYfH4/c3Fw4nU7k5ubKJ765uVwuHDp0SHFtvLVetmwZkpKS4HQ6sW7dOgwMDMix3t5erFu3DgDwxhtvyG+oiYimiiS8UchGRDSO3bt3491334VWq8Urr7wiH07w8ccfIzo6GgBQWVmJ9evXY3h4GAkJCYiJiUFnZyeam5shhPA4nAIAuru7sXDhQvT39yM5ORl6vR7Tpk1DcnIyioqK4HK58MILL+DUqVPQarVIS0vDjBkz0NraiqtXr2LDhg3Ytm0b0tLSFKfD2e12REVFISIiYtTkdSwOhwN5eXlobGyESqWCwWBAVFQUhBCw2+2wWq0YHBzEuXPnFJvIIiMj0dnZCZvNNuqGLkmSAGDM2uNdu3bhww8/hMvlwqJFixAdHY3nnnsOPT09sFqtuHHjBjZt2oStW7fKn/n777+RnZ2NlpYWqFQqxeEU7e3t6OnpUYw33loDysMpdDodUlNT5cMpbt26hYSEBI/DKSwWC1566SWPZ0BE5FVP5+uBiei/ZmhoSGzZskUsXrxYaDQa+TCCpqYmRbuWlhZRUFAg5s2bJ/z9/UVwcLBYsWKFx6EU9/v9999Fenq6mD17tvDz8xMAhNlsluMOh0MUFxeL6OhoodFohE6nE6+++qr4448/5IMR0tLSFH0+7uEU7nutqakR2dnZYu7cuWL69OkiODhYxMXFiaKiIvHrr7+KwcFBxWcmcziFW3t7u1i7dq3Q6/VCo9GIwMBAsWDBApGRkSF27dolurq6PD5z9+5dsWfPHpGSkiJmzZolVCqVCA8PF8uXLxe7d+/2aD/eWgshRF9fn/jkk09EbGysPA+j0Si2bt0qBgYGPPoc6xkQEXkT3/QSERERkc9jTS8RERER+TwmvURERETk85j0EhEREZHPY9JLRERERD6PSS8RERER+TwmvURERETk85j0EhEREZHPY9JLRERERD6PSS8RERER+TwmvURERETk85j0EhEREZHPY9JLRERERD6PSS8RERER+bz/AaW/7kDXN9FPAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# The genuine pipeline for one channel: simulate a titration, extract logit\n", + "# epistasis, classify. This is what you'd run on real data.\n", + "from tfscreen.analysis.extract_epistasis import extract_epistasis\n", + "\n", + "eff_cat = (MutationEffects()\n", + " .add_mutation(\"A1V\", H=2.0) # contrasts H\n", + " .add_mutation(\"A2V\", LE=2.0)) # contrasts LE -> cross H x LE, peak\n", + "titr = build_titration_df(mdl, effector, effects=eff_cat, observable_std=0.02)\n", + "ep = extract_epistasis(titr, y_obs=\"observable\", y_std=\"observable_std\",\n", + " group_by=\"titrant_conc\", scale=\"logit\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(6.5, 4))\n", + "sub = ep[ep[\"genotype\"] == \"A1V/A2V\"]\n", + "ax.axhline(0, color=\"0.6\", lw=1)\n", + "ax.semilogx(sub[\"titrant_conc\"], sub[\"ep_obs\"], \"o-\", color=\"#2f855a\")\n", + "ax.set(xlabel=\"total effector\", ylabel=\"logit epistasis (ep_obs)\",\n", + " title=\"Genuine pipeline: build_titration_df -> extract_epistasis (scale=logit)\")\n", + "fig.tight_layout()\n", + "\n", + "# Caveat: logit-propagated error near saturation is huge, so cat_response's\n", + "# magnitude gate ('fittable') will read False even though the SHAPE is a clean peak.\n", + "print(\"median ep_std (logit-propagated):\", round(float(ep[\"ep_std\"].median()), 2),\n", + " \" <- why the screen above uses clean fixed-error curves for shape calls\")" + ] + }, + { + "cell_type": "markdown", + "id": "11c997b0", + "metadata": {}, + "source": [ + "### Reading the screen\n", + "\n", + "The two tables are the ground-truth **intrinsic** shape of each mechanism and the\n", + "shape `cat_response` **observes** at a realistic noise level. The mapping is clean:\n", + "\n", + "- **Peaks/dips are a redistribution signature that touches $LE$.** Only `redist_*`\n", + " channels that contrast an occupied apo state against $LE$ curve (`cross_H_LE`,\n", + " `cross_L_LE` → *peak*; `same_LE` → *dip*). `cross_H_L` (no $LE$ contrast) is a\n", + " monotone **step**.\n", + "- **Direct within-state interactions never peak.** `direct_e_LE`/`direct_e_H` are\n", + " **steps**; `direct_e_HD` is a **flat** offset. A peak in real data therefore points\n", + " to *redistribution across the ligand transition*, not a contact confined to one\n", + " conformation.\n", + "- **The backdrop decides which peaks are alive — and this is a readout.** A\n", + " $p_\\cdot p_{LE}$ peak needs its partner apo state populated. In the observed table\n", + " `cross_H_LE` is a clean peak in the H-dominant/mixed backdrops but washes to **flat**\n", + " in L-dominant (H depopulated), while `cross_L_LE` does the mirror image — a peak\n", + " where $L$ is populated, flat in H-dominant. **So which backdrops light a peak up\n", + " tells you which conformation the pair redistributes between.**\n", + "- **Noise-free vs observed is a detectability lesson.** Where the intrinsic amplitude\n", + " is small (a peak in a backdrop that depopulates the relevant state), `cat_response`\n", + " correctly reads **flat** — the mechanism is there but undetectable at this noise.\n", + "- **The peak sits at the binding transition.** Its location is the wt ligand-transition\n", + " midpoint to leading order; mutations that perturb $g_{LE}$ drag it toward their own\n", + " shifted transition (a quartet blend, not strictly mutation-independent) — matching\n", + " \"right around the effector concentration over which binding occurs.\"\n", + "\n", + "**Caveats for real data.** (1) Extract epistasis on **logit θ**, but read shape and\n", + "magnitude separately — logit error propagation explodes near saturation (previous\n", + "cell), so a clean peak can be flagged non-`fittable` by the magnitude gate even when\n", + "the shape is obvious. (2) Selector matters: `select_by='aicc'` (used here) is\n", + "conservative and reports only shapes you can trust, while `select_by='shape'` is the\n", + "liberal, exploratory classifier and will over-call structure on low-amplitude curves\n", + "— on perfectly noise-free curves it even chases solver-level micro-structure.\n", + "\n", + "**Practical loop:** `basis_curves` + `plot_epistasis_decomposition` to reason about a\n", + "candidate mechanism, `epistasis_coeffs` to read which basis curve it activates, and\n", + "the backdrop screen to map mechanism → observed `cat_response` shape. You never\n", + "enumerate the doubles — you enumerate backdrops and channels." + ] + }, + { + "cell_type": "markdown", + "id": "7fdd770f", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 10. Measurement range: when is a peak actually observable?\n", + "\n", + "A peak that exists in `logit θ` is not the same as a peak you can *measure*.\n", + "Experimentally θ is only resolvable to some floor `eps` (say `0.01`): you cannot\n", + "tell `0.99` from `0.999`, or `0.01` from `0.0001`. That caps the usable logit scale\n", + "at `±L* = ±ln((1−eps)/eps) ≈ ±4.6`, and — importantly — the failure is\n", + "**one-sided**: `logit(0.99 ± 0.01)` runs over `[3.89, +∞)`. The symmetric\n", + "delta-method scale `σ/(θ(1−θ))` that the epistasis tools use blows up there but\n", + "cannot represent that infinite tail.\n", + "\n", + "Two consequences drive everything below:\n", + "\n", + "1. **Epistasis is a second difference**, so it is measurable only where **all four**\n", + " genotypes (wt, A, B, AB) are simultaneously resolvable — the *intersection* of\n", + " their windows.\n", + "2. **The peak location and the resolvable window are set by different constants.**\n", + " The peak sits at the ligand transition (`K_eff`), but θ leaves saturation where\n", + " `logit θ = ln_K_conf + ln_K_dna + ln[L]` passes through `±L*` — which moves with\n", + " `ln_K_dna`. Tight DNA binding pins θ at 1 through the transition and **buries the\n", + " peak in the saturated zone.**" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d786f537", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usable logit band: +/- 4.595\n", + "\n", + " theta logit CI(z=1) delta-sd\n", + " 0.5 +0.00 [ -0.04, +0.04] 0.04\n", + " 0.9 +2.20 [ +2.09, +2.31] 0.11\n", + " 0.99 +4.60 [ +3.89, +inf] 1.01\n", + " 0.999 +6.91 [ +4.50, +inf] 10.01\n" + ] + } + ], + "source": [ + "from tfscreen.simulate.toy_thermo import (\n", + " logit_ci, resolvable_logit, measurement_window, plot_measurement_window,\n", + ")\n", + "\n", + "# (a) The asymmetric, one-sided logit CI from a theta measurement of +/- 0.01.\n", + "print(f\"usable logit band: +/- {resolvable_logit(0.01):.3f}\\n\")\n", + "print(f\"{'theta':>7} {'logit':>7} {'CI(z=1)':>18} {'delta-sd':>9}\")\n", + "for th in (0.5, 0.9, 0.99, 0.999):\n", + " c, lo, hi = logit_ci(np.array([th]), 0.01, z=1.0)\n", + " hi_s = \"+inf\" if not np.isfinite(hi[0]) else f\"{hi[0]:+.2f}\"\n", + " print(f\"{th:>7} {c[0]:>+7.2f} [{lo[0]:+6.2f}, {hi_s:>6}] {0.01/(th*(1-th)):>8.2f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "19702010", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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xBNP6PDhw5o8ebIKCwtVWFjobH/yySe1dOlSnT59Wvn5+SosLNTs2bMDOsaecq0BAID/UftdGbUfAF8J7eoBAEBP9Ze//EWStGbNGk2aNOmy7Vu2bFFqampAxwYAAADfovYD4Cv8ZR0A+EljY6MkXbYYu1o7AAAAug9qPwC+wmQdgOtWUVEhh8OhhoYGSdLNN9/sXJPD4XCopqbGZf93331Xc+bMUUJCgsLDwxUfH69p06Zp06ZNbn07HA4tWbJEkrRkyRKXfufPn+/cb+/evVq8eLEyMjI0ePBghYeHa8CAAcrOznZ+aulL586d04svvqisrCz1799fERERuvnmm/XII4/o0KFDLvtmZWXJ4XCorq5Okuv1KS0tdb5CYmbOzB0/FRUVLn3985//1Lx58zR06FBFRESof//+ysnJ0fr166841vLycmVnZ+umm25SRESEEhMTlZ2drd/85jfO/a71WktSc3OzSkpKdPvttysqKkoxMTEaM2aMnnnmGbW2trqNoaamRg6HQ1lZWWppadGiRYs0YsQIRUVFsTYLAADdDLUftR+1HxBYvAYL4LqlpKSosLBQa9eu1aeffqpZs2YpOjra2T5o0CDnv3//+9/r4YcfVnt7u9LS0pSVlaWGhga9/vrrev3111VaWqrFixc79y8sLNTOnTu1a9cujRo1SqNHj3a2jR8/3vnvZ599VitXrtTw4cN1xx13KDY2Vo2Njaqurtbbb7+tbdu26dlnn/VJ3tOnT2v69OmqqalRdHS0xowZo7i4OO3Zs0fPPfec1qxZo02bNiktLU2SlJubq+Tk5E6vz+jRozV79mw1NTXpD3/4gzPzxde2w/Lly/X9739f7e3tGj16tNLT03X06FHV1NSoqqpKS5Ys0aJFi1zGevLkSU2dOlVbt25VWFiYxo0bp4SEBB09elS7d+/W22+/re9+97vXda0PHjyoe+65Rw0NDYqLi9OUKVPU1tam6upqFRcX689//rPeeust3XjjjW7X7rPPPlNWVpb27t2ru+++W6NGjdKJEye8vCMAACCQqP2o/aj9gAAzAPBQUlKSSbK6urpO23fv3m2hoaHmcDjsj3/8o0vb+vXrLTw83CRZVVWVS9vixYtNki1evPiy566pqbEDBw64bd+/f78lJiaaJNu+fbtLW11dnUmypKSka8rXoaCgwCTZ1KlT7dix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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# (b) The same cross_H_LE peak in two constructs: a moderate repressor (peak\n", + "# resolvable) vs a tight one (peak buried in saturation). Shaded green = usable\n", + "# band; solid = resolvable, dotted = censored; grey span = joint window.\n", + "fine = np.logspace(-9, -1, 300)\n", + "constructs = {\n", + " \"moderate (ln_K_dna=18)\": ThermoModel(0.0, 18.0, 14.0, 1e-6, 1e-9),\n", + " \"tight (ln_K_dna=22)\": ThermoModel(0.0, 22.0, 14.0, 1e-6, 1e-9),\n", + "}\n", + "fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))\n", + "for ax, (label, mdl_c) in zip(axes, constructs.items()):\n", + " plot_measurement_window(mdl_c, fine, {\"H\": 2.0}, {\"LE\": 2.0}, eps=0.01, ax=ax)\n", + " ax.set_title(f\"{label}\\n{ax.get_title()}\")\n", + " ax.set_ylim(-8, 12)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "34a22256", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ln_K_dna theta@peak joint_window captured true_peak measurable\n", + " 14 0.1395 1.0e-09..9.5e-05 True 0.237 0.237\n", + " 18 0.8984 1.0e-09..5.2e-03 True 0.238 0.238\n", + " 22 0.9979 2.2e-04..1.0e-01 False 0.238 0.020\n", + " 26 1.0000 1.2e-02..1.0e-01 False 0.238 0.000\n", + " 30 1.0000 None False 0.238 0.000\n" + ] + } + ], + "source": [ + "# (c) Scan DNA affinity: how much of the true peak survives inside the joint\n", + "# measurement window. The peak location is fixed by K_eff; tightening ln_K_dna\n", + "# slides the resolvable window off it.\n", + "from tfscreen.simulate.toy_thermo import free_ensemble\n", + "\n", + "a, b = {\"H\": 2.0}, {\"LE\": 2.0}\n", + "quartet = {\"wt\": {}, \"A\": a, \"B\": b, \"AB\": {\"H\": 2.0, \"LE\": 2.0}}\n", + "rows = []\n", + "for lnKd in (14, 18, 22, 26, 30):\n", + " mdl_s = ThermoModel(0.0, lnKd, 14.0, 1e-6, 1e-9)\n", + " ep = exact_epistasis(mdl_s, fine, a, b)\n", + " i_pk = int(np.argmax(np.abs(ep)))\n", + " win = measurement_window(mdl_s, fine, quartet, eps=0.01)[\"joint\"]\n", + " if win is None:\n", + " captured, amp_in = False, 0.0\n", + " else:\n", + " inside = (fine >= win[0]) & (fine <= win[1])\n", + " captured = win[0] <= fine[i_pk] <= win[1]\n", + " amp_in = np.max(np.abs(ep[inside])) if inside.any() else 0.0\n", + " rows.append({\n", + " \"ln_K_dna\": lnKd,\n", + " \"theta@peak\": round(float(mdl_s.observable(fine)[i_pk]), 4),\n", + " \"joint_window\": None if win is None else f\"{win[0]:.1e}..{win[1]:.1e}\",\n", + " \"captured\": captured,\n", + " \"true_peak\": round(float(np.abs(ep[i_pk])), 3),\n", + " \"measurable\": round(float(amp_in), 3),\n", + " })\n", + "print(pd.DataFrame(rows).to_string(index=False))" + ] + }, + { + "cell_type": "markdown", + "id": "b41768f4", + "metadata": {}, + "source": [ + "### Reading Section 10\n", + "\n", + "- **The edge failure is one-sided (panel a).** `θ = 0.99 ± 0.01` gives a logit CI\n", + " `[3.89, +∞)` — the delta-method σ (`≈1.0`) is finite and symmetric and so\n", + " understates it. Treat points with θ outside `[eps, 1−eps]` as *bounds*, not\n", + " measurements.\n", + "- **Same peak, different construct (panel b).** At `ln_K_dna=18` the quartet's\n", + " `logit θ` sweeps through the green band right where the peak sits → *captured*. At\n", + " `ln_K_dna=22` the curves are pinned near the top rail (dotted = censored) across\n", + " the transition, and the peak sits to the **left** of the joint window → *lost*,\n", + " even though the underlying thermodynamics are unchanged.\n", + "- **The scan quantifies it (panel c).** As `ln_K_dna` rises, `θ@peak → 1` and the\n", + " measurable amplitude collapses (`0.24 → 0.02 → 0`) while the *true* peak is\n", + " constant. **A peak is observable only when repression is intermediate at the\n", + " peak's effector concentration.**\n", + "\n", + "**Operational recipe.** Your model-free measurement window is\n", + "`measurement_window(model, fine_grid, {\"wt\":{}, \"A\":…, \"B\":…, \"AB\":…})[\"joint\"]` —\n", + "the effector range where all four are resolvable. A peak is trustworthy only if its\n", + "location falls inside it. To *recover* a peak that lies outside, you must assert the\n", + "thermodynamic model and extrapolate; quantify how much you're leaning on that by\n", + "refitting with vs without the censored points and watching the peak estimate and its\n", + "CI move. Design knob: pick a construct/`protein_total`/operator strength so θ is\n", + "mid-range (not saturated) across the effector concentrations where the peak lives —\n", + "otherwise a tight repressor hides its own allosteric epistasis." + ] + }, { "cell_type": "code", "execution_count": null, @@ -755,6 +1291,14 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb3ef8-fcad-484c-be76-f3145fc718c1", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/tfscreen/simulate/toy_thermo/__init__.py b/src/tfscreen/simulate/toy_thermo/__init__.py index ed51da8..c2fab56 100644 --- a/src/tfscreen/simulate/toy_thermo/__init__.py +++ b/src/tfscreen/simulate/toy_thermo/__init__.py @@ -20,3 +20,18 @@ STATES, ) from .sampling import sample_effects # noqa: F401 +from .basis import ( # noqa: F401 + free_ensemble, + basis_curves, + epistasis_coeffs, + predict_epistasis, + exact_epistasis, + classify_shape, + resolvable_logit, + logit_ci, + measurement_window, + plot_basis, + plot_epistasis_decomposition, + plot_measurement_window, + FREE_STATES, +) diff --git a/src/tfscreen/simulate/toy_thermo/basis.py b/src/tfscreen/simulate/toy_thermo/basis.py new file mode 100644 index 0000000..e2bdce9 --- /dev/null +++ b/src/tfscreen/simulate/toy_thermo/basis.py @@ -0,0 +1,425 @@ +""" +Basis-curve decomposition of logit-scale epistasis for the four-state model. + +Why this exists +--------------- +For any genotype the observable satisfies, *exactly*, + + logit(theta) = ln K_conf + ln K_dna + ln[L_free] + = -g_HD + ln[L_free] + const, + +and free [L] is strictly monotone-decreasing in effector (mass balance). So a +*single* genotype's logit(theta) curve can never peak. All peak structure lives +in the epistasis second difference + + eps(E) = logit(theta_AB) - logit(theta_A) - logit(theta_B) + logit(theta_wt). + +Writing g_k^geno = g_k^wt + (sum of single ddG) + (pair epistasis e_k), the g_HD +term contributes only a flat offset -e_HD, and the partition function +Z = e^-g_L + e^-g_H + E e^-g_LE contributes, to second order in the ddGs, + + eps(E) ~ - Cov_p(delta_A, delta_B) - sum_k p_k(E) e_k - e_HD + +where p_k(E) is the wild-type free-protein occupancy of state k in {L, H, LE} +and the covariance is over that ensemble. Using the identity +Cov_p(a,b) = sum_{j~ 1-2 kT). + """ + coeffs = epistasis_coeffs(ddg_A, ddg_B, epi) + p = free_ensemble(model, effector, "wt") + E = np.atleast_1d(np.asarray(effector, dtype=float)) + + cross = {(j, k): coeffs["cross"][(j, k)] * p[j] * p[k] + for j, k in _STATE_PAIRS} + direct = {s: -coeffs["direct"][s] * p[s] for s in FREE_STATES} + offset = np.full(E.shape, coeffs["offset"]) + + total = offset.copy() + for v in cross.values(): + total = total + v + for v in direct.values(): + total = total + v + return {"total": total, "cross": cross, "direct": direct, "offset": offset} + + +def _logit_theta(model, effector, ddg): + th = np.atleast_1d(model.observable(effector, ddg=ddg)) + th = np.clip(th, 1e-15, 1.0 - 1e-15) + return np.log(th / (1.0 - th)) + + +def exact_epistasis(model, effector, ddg_A, ddg_B, epi=None): + """ + Exact logit-scale epistasis (11-10)-(01-00) from the full solver. + + ``ddg_A``/``ddg_B`` are the two singles; ``epi`` (per-state) is applied only + to the double. No second-order approximation -- this is the ground truth the + basis prediction is meant to reproduce. + """ + dA, dB, e = _as_ddg(ddg_A), _as_ddg(ddg_B), _as_ddg(epi) + dAB = {s: dA[s] + dB[s] + e[s] for s in STATES} + l00 = _logit_theta(model, effector, {}) + l10 = _logit_theta(model, effector, dA) + l01 = _logit_theta(model, effector, dB) + l11 = _logit_theta(model, effector, dAB) + return (l11 - l10) - (l01 - l00) + + +# --------------------------------------------------------------------------- # +# Measurement range / logit error propagation +# --------------------------------------------------------------------------- # + +def resolvable_logit(eps=0.01): + """ + Half-width of the usable logit band given a theta resolution floor ``eps``. + + If theta can only be resolved to within ``eps`` of 0 or 1, then logit(theta) + is only trustworthy inside ``[-L*, +L*]`` with ``L* = ln((1-eps)/eps)`` + (``~4.595`` for ``eps=0.01``). Outside that band one tail of the confidence + interval runs to +/-inf (see :func:`logit_ci`). + """ + eps = float(eps) + return float(np.log((1.0 - eps) / eps)) + + +def logit_ci(theta, sigma, z=1.0): + """ + Asymmetric (censored) logit confidence interval from ``theta +/- z*sigma``. + + Propagates a symmetric measurement error in *theta* space through the logit + by transforming the interval endpoints, not by linearizing. As a theta band + touches 0 or 1 the corresponding logit tail diverges, so this returns + ``+/-inf`` there -- the one-sided blow-up that the delta-method scale + ``sigma/(theta*(1-theta))`` cannot represent. + + Parameters + ---------- + theta : array_like + Observed occupancy/observable in (0, 1). + sigma : array_like + Measurement standard deviation in theta space (broadcast to ``theta``). + z : float + Half-width of the interval in sigmas (e.g. 1.0 or 1.96). + + Returns + ------- + (center, lower, upper) : tuple of numpy.ndarray + ``center`` = logit(theta); ``lower``/``upper`` are the logit-transformed + ``theta -/+ z*sigma`` endpoints, with ``-inf``/``+inf`` where the theta + band reaches 0/1. + """ + theta = np.atleast_1d(np.asarray(theta, dtype=float)) + sigma = np.broadcast_to(np.asarray(sigma, dtype=float), theta.shape) + tiny = 1e-15 + + def _logit(x): + return np.log(x / (1.0 - x)) + + center = _logit(np.clip(theta, tiny, 1.0 - tiny)) + hi_th = theta + z * sigma + lo_th = theta - z * sigma + upper = np.where(hi_th < 1.0, _logit(np.clip(hi_th, tiny, 1.0 - tiny)), np.inf) + lower = np.where(lo_th > 0.0, _logit(np.clip(lo_th, tiny, 1.0 - tiny)), -np.inf) + return center, lower, upper + + +def measurement_window(model, effector, genotypes, eps=0.01): + """ + Effector range over which each genotype -- and the whole set jointly -- is + resolvable (theta in ``[eps, 1-eps]``). + + Because theta is monotone in effector, each resolvable set is a contiguous + interval, reported as ``(E_lo, E_hi)`` on the supplied grid (so pass a fine + ``effector`` grid). Epistasis is a second difference, so it is measurable only + where **all** genotypes are simultaneously resolvable -- the ``"joint"`` key + holds that intersection. + + Parameters + ---------- + genotypes : dict + ``{label: ddg_dict}``. Use e.g. ``{"wt": {}, "A": ddg_A, "B": ddg_B, + "AB": {...}}`` for an epistasis quartet. + + Returns + ------- + dict + ``label -> (E_lo, E_hi)`` per genotype (``None`` if never resolvable on + the grid), plus ``"joint" -> (E_lo, E_hi)`` (``None`` if the intersection + is empty or any genotype is unresolvable). + """ + E = np.asarray(effector, dtype=float) + out = {} + for label, ddg in genotypes.items(): + th = np.atleast_1d(model.observable(E, ddg=ddg)) + ok = (th >= eps) & (th <= 1.0 - eps) + out[label] = (float(E[ok].min()), float(E[ok].max())) if ok.any() else None + + windows = list(out.values()) + if windows and all(w is not None for w in windows): + lo = max(w[0] for w in windows) + hi = min(w[1] for w in windows) + out["joint"] = (lo, hi) if lo <= hi else None + else: + out["joint"] = None + return out + + +def plot_measurement_window(model, effector, ddg_A, ddg_B, eps=0.01, ax=None): + """ + Plot the epistasis quartet's logit(theta) against the resolvable band. + + Shades the usable band ``[-L*, +L*]`` (from ``eps``), draws each of wt/A/B/AB + as ``logit(theta)`` (solid where resolvable, dotted where censored), marks the + epistasis peak location, and shades the joint measurement window. Makes it + visible whether the peak falls where all four genotypes can actually be + measured. Requires matplotlib (lazy import). Returns the Axes. + """ + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots(figsize=(7, 4.5)) + E = np.asarray(effector, dtype=float) + Lstar = resolvable_logit(eps) + + ax.axhspan(-Lstar, Lstar, color="#68d391", alpha=0.15, zorder=0, + label=f"resolvable band (theta in [{eps}, {1-eps}])") + quartet = {"wt": {}, "A": _as_ddg(ddg_A), "B": _as_ddg(ddg_B), + "AB": {s: _as_ddg(ddg_A)[s] + _as_ddg(ddg_B)[s] for s in STATES}} + colors = {"wt": "#2b6cb0", "A": "#dd6b20", "B": "#38a169", "AB": "#805ad5"} + for label, ddg in quartet.items(): + lt = _logit_theta(model, E, ddg) + inband = np.abs(lt) <= Lstar + ax.semilogx(E, np.where(inband, lt, np.nan), "-", color=colors[label], + lw=1.8, label=label) + ax.semilogx(E, np.where(~inband, lt, np.nan), ":", color=colors[label], + lw=1.2, alpha=0.7) + + ep = exact_epistasis(model, E, ddg_A, ddg_B) + peak_E = E[int(np.argmax(np.abs(ep)))] + ax.axvline(peak_E, color="0.35", ls="--", lw=1, label="epistasis peak") + + win = measurement_window(model, E, quartet, eps=eps)["joint"] + if win is not None: + ax.axvspan(win[0], win[1], color="0.6", alpha=0.12, zorder=0) + captured = win[0] <= peak_E <= win[1] + else: + captured = False + ax.axhline(0, color="0.7", lw=0.8) + ax.set_xlabel("total effector") + ax.set_ylabel("logit(theta)") + ax.set_title(f"peak {'INSIDE' if captured else 'OUTSIDE'} joint window") + ax.legend(fontsize=7, loc="upper right") + return ax + + +def plot_basis(model, effector, ax=None): + """ + Plot the wt occupancies and the three basis curves for a backdrop. + + Left of the shared axis: p_L, p_H, p_LE. Overlaid: the products p_L p_H + (monotone), p_L p_LE and p_H p_LE (peak-shaped). Requires matplotlib; import + is lazy so the module has no plotting dependency at import time. Returns the + Axes. + """ + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots(figsize=(6.5, 4)) + E = np.asarray(effector, dtype=float) + b = basis_curves(model, E) + for s, c in zip(FREE_STATES, ("#f6ad55", "#63b3ed", "#dd6b20")): + ax.semilogx(E, b[s], "-", lw=1, color=c, alpha=0.5, label=f"p_{s}") + styles = {"L*H": ("#718096", "--"), "L*LE": ("#2f855a", "-"), + "H*LE": ("#805ad5", "-")} + for key, (c, ls) in styles.items(): + ax.semilogx(E, b[key], ls, lw=2, color=c, label=key) + ax.set_xlabel("total effector") + ax.set_ylabel("occupancy / product") + ax.set_title("Ensemble occupancies and epistasis basis curves") + ax.legend(fontsize=8, ncol=2) + return ax + + +def plot_epistasis_decomposition(model, effector, ddg_A, ddg_B, epi=None, + ax=None): + """ + Overlay exact epistasis, the 2nd-order prediction, and its contributions. + + Solid black: exact eps(E). Dashed: the predicted total. Thin coloured lines: + each nonzero basis / direct / offset contribution. Makes it obvious which + term produces a peak and how far the second-order prediction drifts from the + truth. Requires matplotlib (lazy import). Returns the Axes. + """ + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots(figsize=(6.5, 4)) + E = np.asarray(effector, dtype=float) + exact = exact_epistasis(model, E, ddg_A, ddg_B, epi) + parts = predict_epistasis(model, E, ddg_A, ddg_B, epi) + + ax.axhline(0, color="0.6", lw=1) + ax.semilogx(E, exact, "-", color="black", lw=2.5, label="exact") + ax.semilogx(E, parts["total"], "--", color="#c53030", lw=1.5, + label="2nd-order total") + for (j, k), v in parts["cross"].items(): + if np.any(v): + ax.semilogx(E, v, "-", lw=1, alpha=0.7, label=f"cross {j}*{k}") + for s, v in parts["direct"].items(): + if np.any(v): + ax.semilogx(E, v, ":", lw=1, alpha=0.7, label=f"direct {s}") + if np.any(parts["offset"]): + ax.semilogx(E, parts["offset"], ":", lw=1, alpha=0.7, label="offset e_HD") + ax.set_xlabel("total effector") + ax.set_ylabel("logit-scale epistasis") + ax.set_title(f"exact shape: {classify_shape(exact)}") + ax.legend(fontsize=8) + return ax + + +def classify_shape(eps, tol=1e-6, peak_ratio=1.3, edge=3): + """ + Coarse label for an epistasis curve: 'flat', 'step', or 'peak'. + + A quick, dependency-free classifier (not a substitute for ``cat_response``): + 'flat' if the amplitude is below ``tol``; 'peak' if the extremum is interior + (at least ``edge`` points from either end) and exceeds ``peak_ratio`` times + the larger endpoint magnitude; otherwise 'step'. + """ + eps = np.asarray(eps, dtype=float) + amp = np.max(np.abs(eps)) + if amp < tol: + return "flat" + i = int(np.argmax(np.abs(eps))) + interior = edge <= i <= len(eps) - 1 - edge + ends = max(abs(eps[0]), abs(eps[-1])) + if interior and abs(eps[i]) > peak_ratio * ends: + return "peak" + return "step" diff --git a/tests/tfscreen/simulate/toy_thermo/test_basis.py b/tests/tfscreen/simulate/toy_thermo/test_basis.py new file mode 100644 index 0000000..3e5f4c9 --- /dev/null +++ b/tests/tfscreen/simulate/toy_thermo/test_basis.py @@ -0,0 +1,248 @@ +"""Tests for the logit-epistasis basis-curve decomposition.""" + +import numpy as np +import pytest + +from tfscreen.simulate.toy_thermo import ThermoModel +from tfscreen.simulate.toy_thermo.basis import ( + free_ensemble, + basis_curves, + epistasis_coeffs, + predict_epistasis, + exact_epistasis, + classify_shape, + resolvable_logit, + logit_ci, + measurement_window, + FREE_STATES, + _STATE_PAIRS, +) + + +@pytest.fixture +def model(): + # Notebook default: mixed L/H apo state, tight operator, trace DNA. + return ThermoModel(ln_K_conf=0.0, ln_K_dna=18.0, ln_K_eff=14.0, + protein_total=1e-6, dna_total=1e-9) + + +@pytest.fixture +def effector(): + return np.logspace(-9, -1, 60) + + +# --------------------------------------------------------------------------- # +# Occupancies / basis curves +# --------------------------------------------------------------------------- # + +def test_occupancies_sum_to_one(model, effector): + p = free_ensemble(model, effector) + total = sum(p[s] for s in FREE_STATES) + assert np.allclose(total, 1.0) + + +def test_occupancies_limits(model, effector): + # LE turns on with effector: ~0 at the low end, dominant at the high end. + p = free_ensemble(model, effector) + assert p["LE"][0] < 1e-3 + assert p["LE"][-1] > 0.9 + + +def test_basis_products_are_products(model, effector): + b = basis_curves(model, effector) + for a, c in _STATE_PAIRS: + assert np.allclose(b[f"{a}*{c}"], b[a] * b[c]) + + +def test_le_products_peak_interior(model, effector): + # p_L*p_LE and p_H*p_LE are zero at both ends and peak in the interior; + # p_L*p_H is monotone (extremum at an edge). + b = basis_curves(model, effector) + for key in ("L*LE", "H*LE"): + curve = b[key] + assert curve[0] < curve.max() * 0.1 + assert curve[-1] < curve.max() * 0.1 + i = int(np.argmax(curve)) + assert 3 < i < len(curve) - 4 + assert int(np.argmax(b["L*H"])) < 3 # monotone decay from the low-E end + + +# --------------------------------------------------------------------------- # +# Coefficient bookkeeping +# --------------------------------------------------------------------------- # + +def test_coeffs_cross_state_le(): + # gH x gLE -> single nonzero coefficient on the H*LE product, sign +4. + c = epistasis_coeffs({"H": 2.0}, {"LE": 2.0}) + assert c["cross"][("H", "LE")] == pytest.approx(4.0) + assert c["cross"][("L", "H")] == 0.0 + assert c["cross"][("L", "LE")] == 0.0 + + +def test_coeffs_hd_is_offset_only(): + # Additive HD and pair epistasis on HD never touch the cross/direct basis. + c = epistasis_coeffs({"HD": 2.0}, {"HD": 2.0}, epi={"HD": 3.0}) + assert all(v == 0.0 for v in c["cross"].values()) + assert all(v == 0.0 for v in c["direct"].values()) + assert c["offset"] == pytest.approx(-3.0) + + +def test_coeffs_direct_free_state(): + c = epistasis_coeffs({"H": 1.0}, {"H": 1.0}, epi={"LE": 2.0}) + assert c["direct"]["LE"] == pytest.approx(2.0) + assert c["offset"] == 0.0 + + +# --------------------------------------------------------------------------- # +# Prediction vs exact +# --------------------------------------------------------------------------- # + +def test_prediction_converges_without_depletion(effector): + # The closed form assumes effector is an external field. In the + # low-depletion limit (Ke*[L] << 1) the second-order prediction converges + # to exact as ddG shrinks. + m = ThermoModel(ln_K_conf=0.0, ln_K_dna=18.0, ln_K_eff=14.0, + protein_total=1e-10, dna_total=1e-12) + a, b = {"H": 0.1}, {"LE": 0.1} + pred = predict_epistasis(m, effector, a, b)["total"] + exact = exact_epistasis(m, effector, a, b) + i = int(np.argmax(np.abs(exact))) + assert pred[i] / exact[i] == pytest.approx(1.0, abs=0.1) + + +def test_prediction_recovers_shape_in_depleted_regime(model, effector): + # With meaningful depletion the magnitude drifts (~15% high) and the tails + # skew, but the qualitative signature is preserved: same peak sign and the + # peak sits at the same location. + a, b = {"H": 2.0}, {"LE": 2.0} + pred = predict_epistasis(model, effector, a, b)["total"] + exact = exact_epistasis(model, effector, a, b) + assert classify_shape(pred) == "peak" and classify_shape(exact) == "peak" + ip, ie = int(np.argmax(np.abs(pred))), int(np.argmax(np.abs(exact))) + assert np.sign(pred[ip]) == np.sign(exact[ie]) + assert abs(ip - ie) <= 3 + + +def test_prediction_breakdown_sums_to_total(model, effector): + parts = predict_epistasis(model, effector, {"H": 2.0}, {"LE": 2.0}, + epi={"LE": 1.0}) + recon = parts["offset"].copy() + for v in parts["cross"].values(): + recon = recon + v + for v in parts["direct"].values(): + recon = recon + v + assert np.allclose(recon, parts["total"]) + + +def test_cross_state_le_pair_peaks(model, effector): + # The headline result: a mutation on an apo state x a mutation on LE peaks. + exact = exact_epistasis(model, effector, {"H": 2.0}, {"LE": 2.0}) + assert classify_shape(exact) == "peak" + + +def test_pure_hd_additive_never_peaks(model, effector): + exact = exact_epistasis(model, effector, {"HD": 2.0}, {"HD": 2.0}) + assert classify_shape(exact) in ("flat", "step") + + +def test_cross_state_no_le_is_step(model, effector): + # gH x gL has an L*H coefficient only -> monotone, never a peak. + exact = exact_epistasis(model, effector, {"H": 2.0}, {"L": 2.0}) + assert classify_shape(exact) == "step" + + +def test_direct_le_interaction_is_step(model, effector): + # A within-state interaction is occupancy-weighted (monotone), not a peak. + exact = exact_epistasis(model, effector, {"LE": 0.0}, {"LE": 0.0}, + epi={"LE": 2.0}) + assert classify_shape(exact) == "step" + + +def test_peak_location_tracks_ligand_transition(model, effector): + # The epistasis peak sits at the ligand transition (where p_LE ~ 0.5). + exact = exact_epistasis(model, effector, {"H": 2.0}, {"LE": 2.0}) + p = free_ensemble(model, effector) + i_peak = int(np.argmax(np.abs(exact))) + i_trans = int(np.argmin(np.abs(p["LE"] - 0.5))) + assert abs(i_peak - i_trans) <= 5 + + +# --------------------------------------------------------------------------- # +# Shape classifier +# --------------------------------------------------------------------------- # + +def test_classify_shape_flat(): + assert classify_shape(np.zeros(30)) == "flat" + + +def test_classify_shape_peak(): + x = np.linspace(-3, 3, 41) + assert classify_shape(np.exp(-x**2)) == "peak" + + +def test_classify_shape_step(): + assert classify_shape(1 / (1 + np.exp(-np.linspace(-6, 6, 41)))) == "step" + + +# --------------------------------------------------------------------------- # +# Measurement range / logit error propagation +# --------------------------------------------------------------------------- # + +def test_resolvable_logit_value_and_monotonicity(): + assert resolvable_logit(0.01) == pytest.approx(4.5951, abs=1e-3) + # tighter resolution -> wider usable band + assert resolvable_logit(0.001) > resolvable_logit(0.01) > resolvable_logit(0.1) + + +def test_logit_ci_center_matches_logit(): + theta = np.array([0.2, 0.5, 0.8]) + center, _, _ = logit_ci(theta, 0.01) + assert np.allclose(center, np.log(theta / (1 - theta))) + + +def test_logit_ci_symmetric_in_the_middle(): + center, lo, hi = logit_ci(np.array([0.5]), 0.01, z=1.0) + assert np.isfinite(lo).all() and np.isfinite(hi).all() + assert (hi - center) == pytest.approx(center - lo, abs=1e-6) # ~symmetric at 0.5 + + +def test_logit_ci_one_sided_blowup_near_one(): + # theta + z*sigma reaches 1 -> upper tail is +inf, lower stays finite. + center, lo, hi = logit_ci(np.array([0.995]), 0.01, z=1.0) + assert np.isposinf(hi).all() + assert np.isfinite(lo).all() + assert np.isfinite(center).all() + + +def test_logit_ci_one_sided_blowup_near_zero(): + center, lo, hi = logit_ci(np.array([0.005]), 0.01, z=1.0) + assert np.isneginf(lo).all() + assert np.isfinite(hi).all() + + +def test_measurement_window_is_interval_at_edges(model, effector): + fine = np.logspace(-9, -1, 400) + win = measurement_window(model, fine, {"wt": {}}, eps=0.01) + lo, hi = win["wt"] + assert lo < hi + # theta leaves the [eps, 1-eps] band just outside the reported edges + th = model.observable(np.array([lo, hi])) + assert (th <= 1 - 0.01 + 1e-6).all() and (th >= 0.01 - 1e-6).all() + + +def test_measurement_window_joint_is_intersection(model): + fine = np.logspace(-9, -1, 400) + genos = {"wt": {}, "B": {"LE": 2.0}} + win = measurement_window(model, fine, genos, eps=0.01) + lo = max(win["wt"][0], win["B"][0]) + hi = min(win["wt"][1], win["B"][1]) + assert win["joint"] == pytest.approx((lo, hi)) + + +def test_measurement_window_none_when_unresolvable(): + # Very tight DNA binding keeps theta pinned at 1 across a low-effector grid. + m = ThermoModel(0.0, 30.0, 14.0, 1e-6, 1e-9) + grid = np.logspace(-9, -6, 50) + win = measurement_window(m, grid, {"wt": {}}, eps=0.01) + assert win["wt"] is None + assert win["joint"] is None From 82c8fa6f7b8293a5798eb4d6424d06c13e5946e6 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Tue, 21 Jul 2026 09:49:13 -0400 Subject: [PATCH 24/26] joint epistasis estimator, generic compare posterior --- CLAUDE.md | 13 +- docs/badges/coverage-badge.svg | 2 +- docs/badges/tests-badge.svg | 2 +- pyproject.toml | 3 +- reports/flake.txt | 44 +-- reports/junit/junit.xml | 2 +- .../{compare_theta.py => compare_feature.py} | 93 +++--- src/tfscreen/analysis/extract_epistasis.py | 123 +++++--- ...re_theta_cli.py => compare_feature_cli.py} | 69 +++-- src/tfscreen/tfmodel/analysis/extraction.py | 187 +++++++++++++ .../tfmodel/scripts/predict_epistasis_cli.py | 205 ++++++++++++++ ...mpare_theta.py => test_compare_feature.py} | 77 +++-- .../analysis/test_extract_theta_epistasis.py | 264 ++++++++++++++++++ 13 files changed, 924 insertions(+), 160 deletions(-) rename src/tfscreen/analysis/{compare_theta.py => compare_feature.py} (90%) rename src/tfscreen/analysis/scripts/{compare_theta_cli.py => compare_feature_cli.py} (55%) create mode 100644 src/tfscreen/tfmodel/scripts/predict_epistasis_cli.py rename tests/tfscreen/analysis/{test_compare_theta.py => test_compare_feature.py} (87%) create mode 100644 tests/tfscreen/tfmodel/analysis/test_extract_theta_epistasis.py diff --git a/CLAUDE.md b/CLAUDE.md index dec2d60..e3fc646 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,9 +52,10 @@ tfs-sample-prior # Draw prior predictive samples tfs-extract-params # Extract parameters from checkpoint tfs-predict-growth # Predict growth from fitted model tfs-predict-theta # Predict operator occupancy +tfs-predict-epistasis # Joint second-order epistasis from the theta posterior (per-draw ep, then quantiles; captures cross-genotype posterior covariance the marginal tfs-extract-epistasis path drops) tfs-cat-response # Fit categorical response curves tfs-extract-epistasis # Calculate second-order epistasis from a long-form observable table (--scale add|mult|logit; --scale_constant rescales the transform before epistasis, e.g. -RT to put logit onto a free-energy scale) -tfs-compare-theta # Grade per-genotype theta stability across N estimate runs (seeds / k-fold dropouts) +tfs-compare-feature # Grade per-genotype stability of any quantile-summarized feature (theta/growth/epistasis) across N estimate runs (seeds / k-fold dropouts); --sd_tier_edges sets the A/B/C/D cutlines to the feature's scale tfs-diagnose-nan # Diagnose NaN issues in inference tfs-simulate # Simulate a full experiment tfs-report-cfu0 # Report average ln_cfu0 by genotype class from a simulate config @@ -94,7 +95,7 @@ FASTQ files | `process_raw/` | FASTQ parsing, count normalization, ln_cfu calculation | | `simulate/` | Full experiment simulation from thermodynamics to read counts | | `simulate/growth/` | Growth/growth-transition linkage models for simulation | -| `analysis/` | Downstream statistical analysis of inference outputs (cat_response, extract_epistasis, compare_theta) | +| `analysis/` | Downstream statistical analysis of inference outputs (cat_response, extract_epistasis, compare_feature) | | `mle/` | General-purpose MLE regression (FitManager, least squares, WLS, NLS) | | `mle/curve_models/` | Empirical curve-fitting functions and MODEL_LIBRARY used by cat_response | | `mle/fitters/` | Low-level fitter implementations (least_squares, matrix_nls, matrix_wls) | @@ -425,6 +426,14 @@ When a list of genotypes, titrant names, or concentrations is needed, the `_cli` `tfs-predict-growth` and `tfs-predict-theta` output a boolean column `in_training_data` (1/0) at the `(genotype, titrant_name, titrant_conc)` tuple level. +### `in_regime` column (`tfs-predict-epistasis`) + +`tfs-predict-epistasis` appends a trailing `in_regime` (int 0/1) after the `q` columns. It is `1` only when **all four corners** of the mutant cycle (wt, both singles, double) have their θ posterior — the central `regime_ci` interval (default 95%) — inside the resolvable band `[regime_eps, 1 - regime_eps]` (default `eps=0.01`, whose logit band is ±~4.6). Outside that band `logit(θ)` saturates and the linear-in-θ growth likelihood constrains it weakly, so the epistasis there leans on the θ-model's functional form and cross-genotype posterior covariance — `in_regime == 0` rows are **model-conditional** (a tight CI can still be flagged 0: the perfectly-correlated-but-saturated case). It is the posterior-mass analogue of the toy model's `measurement_window` join (`simulate/toy_thermo/basis.py`); it does **not** separately test whether the growth signal exceeds the growth noise (that is the heavier `m·A/σ_growth` identifiability check). Computed in `extract_theta_epistasis` (`tfmodel/analysis/extraction.py`) on the raw θ samples, so it is independent of `scale`/`scale_constant`. A MAP checkpoint has one draw, so the interval collapses to a point-value band check. + +### Quantile-output column convention + +Any CLI that emits a posterior/quantile summary of an estimate writes those quantiles as **bare `q` columns** — `q0.5` (median), `q0.025`, `q0.975`, etc. — with **no feature-name prefix** on the column. This holds for `tfs-predict-theta`, `tfs-predict-growth`, and `tfs-predict-epistasis`; the feature a file describes is conveyed by the file, not by the column name. Downstream tools rely on this: `resolve_obs_columns` (used by `tfs-extract-epistasis`) defaults `y_obs` to `q0.5` and `y_std` to `(q0.841 - q0.159)/2`, and `tfs-compare-feature` reads the whole `q` ladder. Point estimate + std outputs (e.g. the marginal `tfs-extract-epistasis`'s `ep_obs`/`ep_std`) are a different, non-quantile output shape and keep their descriptive names. + ### Registered entry points All scripts under `tfmodel/scripts/` and `analysis/scripts/` follow the `_cli.py` naming convention and are registered in `pyproject.toml`. diff --git a/docs/badges/coverage-badge.svg b/docs/badges/coverage-badge.svg index 80ba118..c24babe 100644 --- a/docs/badges/coverage-badge.svg +++ b/docs/badges/coverage-badge.svg @@ -1 +1 @@ -coverage: 96.24%coverage96.24% \ No newline at end of file +coverage: 96.12%coverage96.12% \ No newline at end of file diff --git a/docs/badges/tests-badge.svg b/docs/badges/tests-badge.svg index 68fdd82..aace045 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4283tests4283 \ No newline at end of file +tests: 4328tests4328 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bba6529..2694733 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,9 +82,10 @@ tfs-sample-prior = "tfscreen.tfmodel.scripts.sample_prior_cli:main" tfs-extract-params = "tfscreen.tfmodel.scripts.extract_params_cli:main" tfs-predict-growth = "tfscreen.tfmodel.scripts.predict_growth_cli:main" tfs-predict-theta = "tfscreen.tfmodel.scripts.predict_theta_cli:main" +tfs-predict-epistasis = "tfscreen.tfmodel.scripts.predict_epistasis_cli:main" tfs-cat-response = "tfscreen.analysis.scripts.cat_response_cli:main" tfs-extract-epistasis = "tfscreen.analysis.scripts.extract_epistasis_cli:main" -tfs-compare-theta = "tfscreen.analysis.scripts.compare_theta_cli:main" +tfs-compare-feature = "tfscreen.analysis.scripts.compare_feature_cli:main" tfs-diagnose-nan = "tfscreen.tfmodel.scripts.diagnose_nan_cli:main" tfs-simulate = "tfscreen.simulate.scripts.simulate_cli:main" tfs-report-cfu0 = "tfscreen.simulate.scripts.report_cfu0_cli:main" diff --git a/reports/flake.txt b/reports/flake.txt index 1f40457..5203ff1 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -13,8 +13,8 @@ ./src/tfscreen/analysis/cat_response/cat_fit.py:471:55: E712 comparison to True should be 'if cond is True:' or 'if cond:' ./src/tfscreen/analysis/cat_response/cat_fit.py:473:57: E712 comparison to False should be 'if cond is False:' or 'if not cond:' ./src/tfscreen/analysis/cat_response/cat_response.py:65:1: C901 'cat_response' is too complex (16) -./src/tfscreen/analysis/compare_theta.py:200:1: C901 'compare_theta' is too complex (12) -./src/tfscreen/analysis/compare_theta.py:632:1: C901 'aggregate_theta' is too complex (11) +./src/tfscreen/analysis/compare_feature.py:205:1: C901 'compare_feature' is too complex (12) +./src/tfscreen/analysis/compare_feature.py:639:1: C901 'aggregate_feature' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:9:1: C901 'mutant_cycle_pivot' is too complex (11) ./src/tfscreen/analysis/extract_epistasis.py:9:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/extract_epistasis.py:79:25: E231 missing whitespace after ',' @@ -23,18 +23,18 @@ ./src/tfscreen/analysis/extract_epistasis.py:101:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:114:1: W293 blank line contains whitespace ./src/tfscreen/analysis/extract_epistasis.py:126:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:130:1: C901 'extract_epistasis' is too complex (19) -./src/tfscreen/analysis/extract_epistasis.py:134:37: E252 missing whitespace around parameter equals -./src/tfscreen/analysis/extract_epistasis.py:134:38: E252 missing whitespace around parameter equals -./src/tfscreen/analysis/extract_epistasis.py:212:27: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:212:34: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:230:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:253:55: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:253:60: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:253:65: E231 missing whitespace after ',' -./src/tfscreen/analysis/extract_epistasis.py:255:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:261:1: W293 blank line contains whitespace -./src/tfscreen/analysis/extract_epistasis.py:268:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:203:1: C901 'extract_epistasis' is too complex (16) +./src/tfscreen/analysis/extract_epistasis.py:207:37: E252 missing whitespace around parameter equals +./src/tfscreen/analysis/extract_epistasis.py:207:38: E252 missing whitespace around parameter equals +./src/tfscreen/analysis/extract_epistasis.py:285:27: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:285:34: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:303:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:326:55: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:326:60: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:326:65: E231 missing whitespace after ',' +./src/tfscreen/analysis/extract_epistasis.py:328:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:334:1: W293 blank line contains whitespace +./src/tfscreen/analysis/extract_epistasis.py:341:1: W293 blank line contains whitespace ./src/tfscreen/analysis/stats_test_suite.py:8:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/analysis/stats_test_suite.py:8:31: E231 missing whitespace after ',' ./src/tfscreen/analysis/stats_test_suite.py:8:42: E231 missing whitespace after ',' @@ -1417,14 +1417,14 @@ ./src/tfscreen/tfmodel/analysis/extraction.py:103:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/analysis/extraction.py:196:1: E302 expected 2 blank lines, found 1 ./src/tfscreen/tfmodel/analysis/extraction.py:274:1: E302 expected 2 blank lines, found 1 -./src/tfscreen/tfmodel/analysis/extraction.py:275:29: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/analysis/extraction.py:276:29: E128 continuation line under-indented for visual indent -./src/tfscreen/tfmodel/analysis/extraction.py:331:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/analysis/extraction.py:383:1: C901 'extract_growth_predictions' is too complex (11) -./src/tfscreen/tfmodel/analysis/extraction.py:482:32: E127 continuation line over-indented for visual indent -./src/tfscreen/tfmodel/analysis/extraction.py:501:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/analysis/extraction.py:503:16: E221 multiple spaces before operator -./src/tfscreen/tfmodel/analysis/extraction.py:504:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/analysis/extraction.py:462:29: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/analysis/extraction.py:463:29: E128 continuation line under-indented for visual indent +./src/tfscreen/tfmodel/analysis/extraction.py:518:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/analysis/extraction.py:570:1: C901 'extract_growth_predictions' is too complex (11) +./src/tfscreen/tfmodel/analysis/extraction.py:669:32: E127 continuation line over-indented for visual indent +./src/tfscreen/tfmodel/analysis/extraction.py:688:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/analysis/extraction.py:690:16: E221 multiple spaces before operator +./src/tfscreen/tfmodel/analysis/extraction.py:691:16: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:41:15: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:45:12: E221 multiple spaces before operator ./src/tfscreen/tfmodel/analysis/predict_unmeasured.py:99:16: E221 multiple spaces before operator diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index f9a6083..15c91a7 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/tfscreen/analysis/compare_theta.py b/src/tfscreen/analysis/compare_feature.py similarity index 90% rename from src/tfscreen/analysis/compare_theta.py rename to src/tfscreen/analysis/compare_feature.py index 6361c07..d6c2086 100644 --- a/src/tfscreen/analysis/compare_theta.py +++ b/src/tfscreen/analysis/compare_feature.py @@ -1,14 +1,19 @@ """ -Cross-run stability grading for per-genotype theta estimates. - -Given N independent theta estimates of the same library (e.g. runs that differ -only by random seed, or k-fold dropouts of the binding training data), this -module scores every genotype on two independent axes and assigns a graded -stability tier: - - Axis 1 -- reproducibility: how much the point estimates (``q0.5``) of theta - disagree across runs, measured in native theta units. This is the - axis the tier (A/B/C/D) is graded on. +Cross-run stability grading for any per-genotype quantile-summarized feature. + +Given N independent estimates of the same library (e.g. runs that differ only by +random seed, or k-fold dropouts of the training data), this module scores every +genotype on two independent axes and assigns a graded stability tier. The +feature being compared is whatever quantile ladder the input tables carry +(``q`` columns) -- theta, growth rate, epistasis, etc. -- so the tool is +feature-agnostic; only the point estimate (``q0.5``) and a 1-sigma half-width +matter to the math. + + Axis 1 -- reproducibility: how much the point estimates (``q0.5``) of the + feature disagree across runs, measured in the feature's native + units. This is the axis the tier (A/B/C/D) is graded on, so its + cutlines (``sd_tier_edges``) must be chosen to match the feature's + scale -- the defaults are tuned for theta (an occupancy in [0, 1]). Axis 2 -- self-consistency: whether the run-to-run disagreement is explained by each run's own reported uncertainty (derived from the stored @@ -16,8 +21,8 @@ *not* folded into the tier -- it distinguishes "honestly uncertain" genotypes from the dangerous "overconfident and inconsistent" ones. -Agreement is assessed in absolute theta units (no registration/rescaling of -runs). Two comparison modes are supported: +Agreement is assessed in the feature's absolute units (no registration/rescaling +of runs). Two comparison modes are supported: * mean mode (``reference_df is None``): symmetric. The target is the cross-run mean; every run is treated equally. Intended for same-data/different-seed @@ -28,7 +33,7 @@ it. Intended for k-fold dropout runs measured against a full-data fit. The public API operates on already-loaded DataFrames so it is trivially -testable; the ``tfs-compare-theta`` CLI wraps it with file IO. +testable; the ``tfs-compare-feature`` CLI wraps it with file IO. """ import numpy as np @@ -95,9 +100,9 @@ def _condition_grid(df, keys): def _extract_run(df, keys, point_quantile, sigma_quantiles): """ - Reduce one estimate table to key columns plus ``theta`` and ``sigma``. + Reduce one estimate table to key columns plus ``value`` and ``sigma``. - ``theta`` is the ``point_quantile`` column (median by default); ``sigma`` is + ``value`` is the ``point_quantile`` column (median by default); ``sigma`` is the symmetric 1-sigma half-width ``(q_hi - q_lo)/2`` from ``sigma_quantiles``. Parameters @@ -114,7 +119,7 @@ def _extract_run(df, keys, point_quantile, sigma_quantiles): Returns ------- pandas.DataFrame - Columns ``keys + ['theta', 'sigma']``. + Columns ``keys + ['value', 'sigma']``. Raises ------ @@ -135,7 +140,7 @@ def _extract_run(df, keys, point_quantile, sigma_quantiles): ) out = df.loc[:, keys].copy() - out["theta"] = df[point_col].to_numpy(dtype=float) + out["value"] = df[point_col].to_numpy(dtype=float) out["sigma"] = (df[hi_col].to_numpy(dtype=float) - df[lo_col].to_numpy(dtype=float)) / 2.0 return out @@ -197,18 +202,20 @@ def _assign_tier(rms_sd, n_present, n_runs, min_coverage, sd_tier_edges): return "D" -def compare_theta(estimate_dfs, - reference_df=None, - *, - min_coverage=0.5, - sd_tier_edges=(0.02, 0.05, 0.10), - overdispersion_threshold=2.0, - point_quantile=0.5, - sigma_quantiles=(0.159, 0.841)): +def compare_feature(estimate_dfs, + reference_df=None, + *, + min_coverage=0.5, + sd_tier_edges=(0.02, 0.05, 0.10), + overdispersion_threshold=2.0, + point_quantile=0.5, + sigma_quantiles=(0.159, 0.841)): """ - Grade per-genotype theta stability across N independent estimate runs. + Grade per-genotype feature stability across N independent estimate runs. - See the module docstring for the two axes and the two comparison modes. + See the module docstring for the two axes and the two comparison modes. The + graded feature is whatever quantile ladder the tables carry (``q`` + columns); ``sd_tier_edges`` must match that feature's native scale. Parameters ---------- @@ -324,7 +331,7 @@ def compare_theta(estimate_dfs, if mode == "reference": ref = _extract_run(reference_df, keys, point_quantile, sigma_quantiles) - ref = ref.rename(columns={"theta": "theta_ref", "sigma": "sigma_ref"}) + ref = ref.rename(columns={"value": "value_ref", "sigma": "sigma_ref"}) before = long[_GENOTYPE_KEY].nunique() long = long.merge(ref, on=keys, how="inner") after = long[_GENOTYPE_KEY].nunique() @@ -334,13 +341,13 @@ def compare_theta(estimate_dfs, f"the reference table.", flush=True, ) - long["_target"] = long["theta_ref"] - long["_dev"] = long["theta"] - long["theta_ref"] + long["_target"] = long["value_ref"] + long["_dev"] = long["value"] - long["value_ref"] long["_var"] = long["sigma"] ** 2 + long["sigma_ref"] ** 2 else: # Symmetric: the per-grid mean is the target. - long["_target"] = long.groupby(keys)["theta"].transform("mean") - long["_dev"] = long["theta"] - long["_target"] + long["_target"] = long.groupby(keys)["value"].transform("mean") + long["_dev"] = long["value"] - long["_target"] long["_var"] = long["sigma"] ** 2 if long.empty: @@ -382,18 +389,18 @@ def _per_grid_table(long, keys, estimator): Returns a frame keyed by ``keys`` with: * ``spread`` -- Axis-1 dispersion at that grid point (std or half-range in mean mode; RMS deviation from the reference in reference mode). - * ``mu`` -- the target theta curve value (cross-run mean or reference). + * ``mu`` -- the target feature curve value (cross-run mean or reference). * ``grid_label`` -- the ``sd[...]`` output-column name for this point. """ grp = long.groupby(keys, sort=False) - if "theta_ref" in long.columns: + if "value_ref" in long.columns: # Reference mode: RMS deviation of the runs from the reference. spread = np.sqrt(grp["_dev"].apply(lambda d: np.nanmean(np.square(d)))) elif estimator == "std": - spread = grp["theta"].std(ddof=1) + spread = grp["value"].std(ddof=1) else: - spread = (grp["theta"].max() - grp["theta"].min()) / 2.0 + spread = (grp["value"].max() - grp["value"].min()) / 2.0 mu = grp["_target"].first() @@ -450,7 +457,7 @@ def _rms(s): n_terms = chi_g["term"].apply(lambda s: int(np.isfinite(s).sum())) mean_sigma = chi_g["sigma"].mean() - is_reference = "theta_ref" in long.columns + is_reference = "value_ref" in long.columns n_grids = long.groupby(_GENOTYPE_KEY)[keys[1:]].apply( lambda d: len(d.drop_duplicates()) ) @@ -491,14 +498,14 @@ def stability_crosstabs(result, overdispersion_threshold=2.0, flat_range_threshold=0.1): """ - Build the two 2x2 interpretation tables from a ``compare_theta`` result. + Build the two 2x2 interpretation tables from a ``compare_feature`` result. ``low_coverage`` genotypes are excluded (they were never graded). Parameters ---------- result : pandas.DataFrame - Output of :func:`compare_theta`. + Output of :func:`compare_feature`. overdispersion_threshold : float, optional Split between "consistent" (<=) and "overconfident" (>). Default 2.0. flat_range_threshold : float, optional @@ -624,14 +631,14 @@ def _mixture_quantiles(value_matrix, probs): f_mix = f_sum / n # Invert F_mix. Collapse ties (flat CDF = zero-density gaps) keeping the - # smallest theta for each CDF level, matching the standard quantile def. + # smallest value for each CDF level, matching the standard quantile def. uf, idx = np.unique(f_mix, return_index=True) return np.interp(probs, uf, grid[idx]) -def aggregate_theta(estimate_dfs, *, progress_every=200_000): +def aggregate_feature(estimate_dfs, *, progress_every=200_000): """ - Combine N theta estimates into one aggregate theta-vs-condition table. + Combine N feature estimates into one aggregate feature-vs-condition table. For every ``(genotype, [titrant_name,] titrant_conc)`` the N runs present are combined as an **equal-weight mixture** of their per-run marginal posteriors @@ -675,7 +682,7 @@ def aggregate_theta(estimate_dfs, *, progress_every=200_000): ) if len(estimate_dfs) < 2: raise ValueError( - f"aggregate_theta requires at least 2 estimate tables; " + f"aggregate_feature requires at least 2 estimate tables; " f"got {len(estimate_dfs)}." ) diff --git a/src/tfscreen/analysis/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index 50b9be5..745f956 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -127,6 +127,79 @@ def mutant_cycle_pivot( return pd.concat(result_dfs).reset_index(drop=True) +def _epistasis_from_corners( + obs_00, + obs_10, + obs_01, + obs_11, + scale: str, + scale_constant: float = 1.0, + logit_eps: float = 1e-9, + warn_out_of_range: bool = True, +): + """ + Compute second-order epistasis from the four mutant-cycle corner values. + + This is the single definition of *what epistasis means* on each scale, + shared by the marginal path (``extract_epistasis``, which pushes per-point + quantile-derived means through it) and the joint-posterior path + (``tfscreen.tfmodel.analysis.extraction.extract_theta_epistasis``, which + pushes a full ``(num_sample, num_cycle)`` array through it). It operates + elementwise, so the corner arguments may be scalars, pandas Series, or NumPy + arrays of any shape; the result has the broadcast shape of the inputs. + + Parameters + ---------- + obs_00, obs_10, obs_01, obs_11 : array-like + The observable at the wildtype (``00``), each single mutant (``10``, + ``01``), and the double mutant (``11``). + scale : {"add", "mult", "logit"} + The epistatic scale. See ``extract_epistasis`` for the definitions. + scale_constant : float, default 1.0 + Constant applied to the (identity/logit) transform before the linear + difference-of-differences; a no-op for ``"mult"``. + logit_eps : float, default 1e-9 + Clamp applied inside the logit transform (``scale="logit"`` only). + warn_out_of_range : bool, default True + If True, warn when a logit-scale input falls outside ``[0, 1]``. + + Returns + ------- + array-like + The epistasis value(s), broadcast to the shape of the inputs. + + Raises + ------ + ValueError + If ``scale`` is not one of "add", "mult", or "logit". + """ + if scale == "add": + return scale_constant * ((obs_11 - obs_10) - (obs_01 - obs_00)) + + if scale == "logit": + raw = [obs_00, obs_10, obs_01, obs_11] + if warn_out_of_range and any( + bool(((o < 0.0) | (o > 1.0)).any()) for o in raw): + warnings.warn( + "scale='logit' expects an observable in [0, 1]; values outside " + "this range were found and clamped. Check that y_obs is a " + "fraction/occupancy (e.g. theta), not fitness or dG.", + stacklevel=2, + ) + clip = [np.clip(o, logit_eps, 1.0 - logit_eps) for o in raw] + t_00, t_10, t_01, t_11 = (scale_constant * np.log(c / (1.0 - c)) + for c in clip) + return (t_11 - t_10) - (t_01 - t_00) + + if scale == "mult": + return (obs_11 / obs_10) / (obs_01 / obs_00) + + raise ValueError( + "scale should be 'add' (additive), 'mult' (multiplicative), or " + "'logit' (additive on the logit scale)\n" + ) + + def extract_epistasis( df: pd.DataFrame, y_obs: str, @@ -266,46 +339,32 @@ def extract_epistasis( std_01 = cycles[f"01_{y_std}"] std_11 = cycles[f"11_{y_std}"] - # Additive scale. scale_constant scales the (identity) transform, so it - # factors straight out of the linear difference-of-differences. - if scale == "add": - ep_obs = scale_constant * ((obs_11 - obs_10) - (obs_01 - obs_00)) - if y_std is not None: + # The epistasis value itself is computed by the shared helper so the + # marginal path here and the joint-posterior path (extract_theta_epistasis) + # can never disagree on what epistasis means on a given scale. Error + # propagation (below) is specific to the marginal path and stays here. + ep_obs = _epistasis_from_corners(obs_00, obs_10, obs_01, obs_11, + scale=scale, + scale_constant=scale_constant, + logit_eps=logit_eps) + + if y_std is not None: + # Additive: scale_constant factors straight out of the linear ddd. + if scale == "add": ep_std = np.abs(scale_constant) * np.sqrt( std_11**2 + std_10**2 + std_01**2 + std_00**2) - # Logit scale: additive epistasis of logit(Y). Removes a saturating [0, 1] - # measurement scale so within-state interactions are not masked by it. - # scale_constant multiplies the logit transform up front (e.g. -RT to put - # ep_obs on a free-energy scale, since logit(theta) = -dG/RT). - elif scale == "logit": - raw = [obs_00, obs_10, obs_01, obs_11] - - # logit is only defined on (0, 1); warn if the observable is not a - # fraction (a common sign of pointing y_obs at fitness/dG), then clamp. - if any(((o < 0.0) | (o > 1.0)).any() for o in raw): - warnings.warn( - "scale='logit' expects an observable in [0, 1]; values outside " - "this range were found and clamped. Check that y_obs is a " - "fraction/occupancy (e.g. theta), not fitness or dG.", - stacklevel=2, - ) - - clip = [o.clip(logit_eps, 1.0 - logit_eps) for o in raw] - t_00, t_10, t_01, t_11 = (scale_constant * np.log(c / (1.0 - c)) - for c in clip) - ep_obs = (t_11 - t_10) - (t_01 - t_00) - if y_std is not None: - # delta method: d/dy [sc * logit(y)] = sc / (y (1 - y)) + # Logit: delta method, d/dy [sc * logit(y)] = sc / (y (1 - y)). + elif scale == "logit": + clip = [o.clip(logit_eps, 1.0 - logit_eps) + for o in (obs_00, obs_10, obs_01, obs_11)] raw_std = [std_00, std_10, std_01, std_11] t_std = [np.abs(scale_constant) * s / (c * (1.0 - c)) for s, c in zip(raw_std, clip)] ep_std = np.sqrt(sum(s**2 for s in t_std)) - # Multiplicative scale - else: - ep_obs = (obs_11 / obs_10) / (obs_01 / obs_00) - if y_std is not None: + # Multiplicative: relative-error propagation of the ratio-of-ratios. + else: rel_err_sq = ((std_11 / obs_11)**2 + (std_10 / obs_10)**2 + (std_01 / obs_01)**2 + (std_00 / obs_00)**2) ep_std = np.abs(ep_obs) * np.sqrt(rel_err_sq) diff --git a/src/tfscreen/analysis/scripts/compare_theta_cli.py b/src/tfscreen/analysis/scripts/compare_feature_cli.py similarity index 55% rename from src/tfscreen/analysis/scripts/compare_theta_cli.py rename to src/tfscreen/analysis/scripts/compare_feature_cli.py index 793032e..7a95def 100644 --- a/src/tfscreen/analysis/scripts/compare_theta_cli.py +++ b/src/tfscreen/analysis/scripts/compare_feature_cli.py @@ -1,12 +1,13 @@ """ -CLI for grading per-genotype theta stability across N estimate runs. +CLI for grading per-genotype stability of any quantile-summarized feature across +N estimate runs. """ import pandas as pd -from tfscreen.analysis.compare_theta import ( - compare_theta as _compare_theta, - aggregate_theta, +from tfscreen.analysis.compare_feature import ( + compare_feature as _compare_feature, + aggregate_feature, stability_crosstabs, ) from tfscreen.util.cli import generalized_main, read_lines @@ -22,22 +23,25 @@ def _format_crosstabs(crosstabs): return "\n".join(blocks) -def compare_theta(estimates_file, - out_prefix="tfs_compare_theta", - reference=None, - min_coverage=0.5, - overdispersion_threshold=2.0, - write_aggregate=False): +def compare_feature(estimates_file, + out_prefix="tfs_compare_feature", + reference=None, + min_coverage=0.5, + sd_tier_edges=(0.02, 0.05, 0.10), + overdispersion_threshold=2.0, + write_aggregate=False): """ - Grade per-genotype theta stability across N independent estimate runs. + Grade per-genotype feature stability across N independent estimate runs. - Reads N theta-estimate CSVs (each with the standard quantile columns: - genotype, titrant_conc [, titrant_name], q0.001 ... q0.5 ... q0.999) and - scores every genotype on two axes: reproducibility (run-to-run spread of the - q0.5 point estimate, in native theta units -- the graded tier) and + Reads N estimate CSVs (each with the standard quantile columns: genotype, + titrant_conc [, titrant_name], q0.001 ... q0.5 ... q0.999) and scores every + genotype on two axes: reproducibility (run-to-run spread of the q0.5 point + estimate, in the feature's native units -- the graded tier) and self-consistency (whether that spread is explained by each run's reported - uncertainty -- a flag). Writes a per-genotype table to {out_prefix}.csv and - the interpretation crosstabs to {out_prefix}_crosstabs.txt. + uncertainty -- a flag). The compared feature is whatever the q columns + hold (theta, growth rate, epistasis, ...), so the tool is feature-agnostic. + Writes a per-genotype table to {out_prefix}.csv and the interpretation + crosstabs to {out_prefix}_crosstabs.txt. Parameters ---------- @@ -46,7 +50,7 @@ def compare_theta(estimates_file, ('#' comments allowed). In the default (mean) mode at least two are required. out_prefix : str, optional - Prefix for the output files. Default 'tfs_compare_theta'. + Prefix for the output files. Default 'tfs_compare_feature'. reference : str or None, optional Path to a reference-run CSV. If given, switches to reference mode: each estimate run is scored by its deviation from this run (e.g. k-fold @@ -55,16 +59,22 @@ def compare_theta(estimates_file, min_coverage : float, optional Minimum fraction of estimate runs a genotype must appear in to be graded; below this it is tiered 'low_coverage'. Default 0.5. + sd_tier_edges : sequence of float, optional + Three ascending cutlines on the run-to-run spread (rms_sd) separating + tiers A|B, B|C, C|D, in the feature's native units. Default + (0.02, 0.05, 0.10), which is tuned for theta (occupancy in [0, 1]); set + edges appropriate to the feature's scale when comparing anything else + (e.g. growth rate or logit-epistasis), or the tiers are meaningless. overdispersion_threshold : float, optional Overdispersion above this sets the 'overdispersed' flag and the 'overconfident' crosstab column. Default 2.0. write_aggregate : bool, optional If set, also write {out_prefix}_aggregate.csv: a long-form aggregate - theta-vs-condition table combining the N estimate runs as an equal-weight - mixture of their per-run posteriors (quantiles reconstructed and mixed). - The aggregate error folds in both per-run posterior width and run-to-run - spread and does not shrink with N. Only the N estimate runs are mixed -- - a reference run (if any) is never included. Default False. + feature-vs-condition table combining the N estimate runs as an + equal-weight mixture of their per-run posteriors (quantiles reconstructed + and mixed). The aggregate error folds in both per-run posterior width and + run-to-run spread and does not shrink with N. Only the N estimate runs + are mixed -- a reference run (if any) is never included. Default False. """ paths = read_lines(estimates_file) if not paths: @@ -78,10 +88,11 @@ def compare_theta(estimates_file, print(f"Reading reference {reference}...", flush=True) reference_df = pd.read_csv(reference) - result = _compare_theta( + result = _compare_feature( estimate_dfs, reference_df=reference_df, min_coverage=min_coverage, + sd_tier_edges=tuple(sd_tier_edges), overdispersion_threshold=overdispersion_threshold, ) @@ -105,8 +116,8 @@ def compare_theta(estimates_file, print(f"Wrote interpretation crosstabs to {crosstab_file}", flush=True) if write_aggregate: - print("Building aggregate theta-vs-condition table...", flush=True) - aggregate = aggregate_theta(estimate_dfs) + print("Building aggregate feature-vs-condition table...", flush=True) + aggregate = aggregate_feature(estimate_dfs) aggregate_file = f"{out_prefix}_aggregate.csv" aggregate.to_csv(aggregate_file, index=False) print(f"Wrote {len(aggregate)} aggregate rows to {aggregate_file}", @@ -114,8 +125,10 @@ def compare_theta(estimates_file, def main(): - generalized_main(compare_theta, - manual_arg_types={"reference": str}) + generalized_main(compare_feature, + manual_arg_types={"reference": str, + "sd_tier_edges": float}, + manual_arg_nargs={"sd_tier_edges": 3}) if __name__ == "__main__": diff --git a/src/tfscreen/tfmodel/analysis/extraction.py b/src/tfscreen/tfmodel/analysis/extraction.py index 4f20aa8..9bb9428 100644 --- a/src/tfscreen/tfmodel/analysis/extraction.py +++ b/src/tfscreen/tfmodel/analysis/extraction.py @@ -271,6 +271,193 @@ def extract_theta_curves(orchestrator, posteriors, q_to_get=None, manual_titrant return calc_df.drop(columns=internal_cols) +def extract_theta_epistasis(orchestrator, posteriors, q_to_get=None, + manual_titrant_df=None, scale="logit", + scale_constant=1.0, + group_by=("titrant_name", "titrant_conc"), + regime_eps=0.01, regime_ci=0.95): + """ + Extract second-order epistasis quantiles from the joint theta posterior. + + Unlike calculating epistasis from per-genotype marginal theta estimates + (which treats the four corners of each mutant cycle as independent and + propagates ``sqrt(sum std**2)``), this function draws theta for every + genotype from the *same* posterior sample, computes epistasis within each + draw, and then quantiles across draws. The resulting uncertainty therefore + reflects the true posterior covariance between the wildtype, single, and + double mutants of each cycle. + + Only genotypes seen during training are supported: the joint sample matrix + comes from the theta component's ``build_calc_df`` / ``compute_theta_samples`` + interface (the same one used by ``extract_theta_curves``), which returns + training genotypes. Out-of-training genotypes have no joint sample matrix + and are not handled here. + + Parameters + ---------- + orchestrator : ModelOrchestrator + The fitted model instance. + posteriors : dict or str + Posterior samples (dict, NpzFile, or path to a ``.npz``/``.h5`` file). + A MAP checkpoint provides a single "draw"; the returned quantile columns + then all collapse to that point estimate (no uncertainty). + q_to_get : dict or array-like, optional + Quantile levels to extract. Defaults to the standard dense set used by + the other extraction functions. + manual_titrant_df : pd.DataFrame, optional + A DataFrame specifying ``'titrant_name'`` and ``'titrant_conc'`` values + at which to calculate theta before building cycles. If it also has a + ``'genotype'`` column it selects the genotypes; otherwise all training + genotypes are used. + scale : {"logit", "add", "mult"}, default "logit" + The epistatic scale. ``"logit"`` is the natural choice for theta (an + occupancy in ``[0, 1]``); see + ``tfscreen.analysis.extract_epistasis`` for the definitions. + scale_constant : float, default 1.0 + Constant applied to the transform before the difference-of-differences + (e.g. ``-RT`` to report logit epistasis as an interaction free energy). + Rejected for ``scale="mult"`` (it cancels in the ratio-of-ratios). + group_by : sequence of str, default ("titrant_name", "titrant_conc") + Columns defining a unique condition; epistasis is computed independently + within each. For theta, each concentration is its own condition. + regime_eps : float, default 0.01 + Theta resolution floor for the ``in_regime`` flag. A cycle corner is + "in the resolvable band" when its theta posterior sits inside + ``[regime_eps, 1 - regime_eps]``; outside that band logit(theta) is + saturated and its uncertainty is dominated by the theta-model + extrapolation rather than the data. Must satisfy ``0 <= regime_eps < + 0.5``. + regime_ci : float, default 0.95 + Central posterior-mass fraction that must lie inside the band for a + corner to count as in-regime (e.g. 0.95 requires the theta 2.5-97.5% + interval within ``[regime_eps, 1 - regime_eps]``). Must be in (0, 1). + + Returns + ------- + pd.DataFrame + One row per (double-mutant genotype x condition) with the ``group_by`` + columns, ``genotype``, one ``q`` column per quantile (the + library-wide quantile-output convention, e.g. ``q0.5``, ``q0.025``), and + a trailing ``in_regime`` column (int 0/1). ``in_regime == 1`` means all + four cycle corners (wt, both singles, double) have their theta posterior + (central ``regime_ci`` interval) within ``[regime_eps, 1 - regime_eps]``, + so the epistasis is backed by in-band posterior mass; ``0`` means at + least one corner is near saturation, so the estimate leans on the + theta-model's extrapolation / posterior covariance and should be treated + as model-conditional. (This is the posterior-mass analogue of a + measurement-window check; it does *not* separately test whether the + growth signal exceeds the growth noise.) Empty if no complete mutant + cycles (wt + both singles + double) exist. + + Raises + ------ + ValueError + If the theta component does not implement the sample interface, if + ``scale_constant`` is non-trivial for ``scale="mult"``, or if + ``regime_eps``/``regime_ci`` are out of range. + """ + from tfscreen.analysis.extract_epistasis import ( + mutant_cycle_pivot, + _epistasis_from_corners, + ) + + if scale == "mult" and scale_constant != 1.0: + raise ValueError( + "scale_constant has no effect when scale='mult' (it cancels in the " + "ratio-of-ratios). Use scale='add' or 'logit', or leave " + "scale_constant at its default of 1.0." + ) + + if not (0.0 <= regime_eps < 0.5): + raise ValueError( + f"regime_eps must be in [0, 0.5); got {regime_eps}." + ) + if not (0.0 < regime_ci < 1.0): + raise ValueError( + f"regime_ci must be in (0, 1); got {regime_ci}." + ) + + module = model_registry.get("theta", {}).get(orchestrator._theta) + if module is None or not (hasattr(module, "build_calc_df") + and hasattr(module, "compute_theta_samples")): + raise ValueError( + f"extract_theta_epistasis requires the theta component to implement " + f"build_calc_df and compute_theta_samples. " + f"'{orchestrator._theta}' does not support this interface." + ) + + q_to_get, param_posteriors = load_posteriors(posteriors, q_to_get) + + # Joint sample matrix: (num_sample, num_row), rows aligned to calc_df. + calc_df, internal_cols, extra_kwargs = module.build_calc_df( + orchestrator, manual_titrant_df) + theta_samples = module.compute_theta_samples( + calc_df, param_posteriors, **extra_kwargs) + + group_by = list(group_by) + + # Pivot on the row index (not the observable): mutant_cycle_pivot matches + # wt/single/single/double per condition and returns, for each cycle, the + # calc_df row index of each of the four corners. Those indices then gather + # the corresponding columns out of the joint sample matrix. + pivot_df = calc_df[["genotype"] + group_by].copy() + pivot_df["_row_idx"] = np.arange(len(calc_df)) + cycles = mutant_cycle_pivot(pivot_df, + extract_columns=["_row_idx"], + group_by=group_by) + + # Drop any cycle missing a corner (e.g. a double whose single parent has no + # theta row): without all four joint samples epistasis is undefined. The + # missing corner surfaces as a NaN row index from the pivot's reindex. + idx_cols = [f"{c}__row_idx" for c in ("00", "10", "01", "11")] + if not cycles.empty: + cycles = cycles.dropna(subset=idx_cols) + + if cycles.empty: + return pd.DataFrame(columns=["genotype"] + group_by + + list(q_to_get) + ["in_regime"]) + + idx_00 = cycles["00__row_idx"].values.astype(int) + idx_10 = cycles["10__row_idx"].values.astype(int) + idx_01 = cycles["01__row_idx"].values.astype(int) + idx_11 = cycles["11__row_idx"].values.astype(int) + + # Each of these is (num_sample, num_cycle); epistasis is computed per draw. + ep_samples = _epistasis_from_corners( + theta_samples[:, idx_00], + theta_samples[:, idx_10], + theta_samples[:, idx_01], + theta_samples[:, idx_11], + scale=scale, + scale_constant=scale_constant, + ) + + out = cycles[["genotype"] + group_by].copy() + for q_name, q_val in q_to_get.items(): + out[q_name] = np.quantile(ep_samples, q_val, axis=0) + + # in_regime: are all four cycle corners' theta posteriors inside the + # resolvable band [regime_eps, 1 - regime_eps]? Outside it logit(theta) + # saturates and the epistasis leans on the theta-model extrapolation, so the + # flag marks whether the estimate is backed by in-band posterior mass. A + # MAP checkpoint has a single "draw", so the interval collapses to the point + # estimate and this reduces to a point-value band check. + lo_q = (1.0 - regime_ci) / 2.0 + hi_q = 1.0 - lo_q + + def _corner_in_band(idx): + th = theta_samples[:, idx] # (num_sample, num_cycle) + lo = np.quantile(th, lo_q, axis=0) + hi = np.quantile(th, hi_q, axis=0) + return (lo >= regime_eps) & (hi <= 1.0 - regime_eps) + + in_regime = (_corner_in_band(idx_00) & _corner_in_band(idx_10) + & _corner_in_band(idx_01) & _corner_in_band(idx_11)) + out["in_regime"] = in_regime.astype(int) + + return out.sort_values(["genotype"] + group_by).reset_index(drop=True) + + def extract_theta_unmeasured(orchestrator, posteriors, target_genotypes, manual_titrant_df, q_to_get=None, genotype_batch_size=2000): diff --git a/src/tfscreen/tfmodel/scripts/predict_epistasis_cli.py b/src/tfscreen/tfmodel/scripts/predict_epistasis_cli.py new file mode 100644 index 0000000..9c514d3 --- /dev/null +++ b/src/tfscreen/tfmodel/scripts/predict_epistasis_cli.py @@ -0,0 +1,205 @@ +import pandas as pd +from tfscreen.tfmodel.configuration_io import read_configuration +from tfscreen.tfmodel.inference.checkpoint_io import resolve_param_file +from tfscreen.tfmodel.analysis.extraction import extract_theta_epistasis +from tfscreen.util.cli import generalized_main, read_lines + + +def predict_epistasis(config_file, + param_file, + out_prefix="tfs_pred_epistasis", + genotypes_file=None, + titrant_names_file=None, + titrant_concs_file=None, + only_files=False, + scale="logit", + scale_constant=1.0, + regime_eps=0.01, + regime_ci=0.95): + """ + Predict second-order epistasis from the joint posterior of a fitted model. + + For every double mutant, epistasis is calculated on its mutant cycle (the + double, its two single-mutant parents, and the wildtype). In contrast to + running tfs-extract-epistasis on a tfs-predict-theta table -- which uses the + per-genotype marginal theta estimates and assumes the four corners are + independent -- this command draws theta for all genotypes from the same + posterior sample, computes epistasis within each draw, and then reports + quantiles across draws. The uncertainty therefore reflects the true + posterior covariance between the corners of each cycle. + + Epistasis is computed independently at each condition (titrant_name, + titrant_conc). Output columns are 'genotype', 'titrant_name', + 'titrant_conc', one 'q' column per quantile (e.g. 'q0.5', 'q0.025', + 'q0.975' -- the library-wide quantile-output convention), and a trailing + 'in_regime' flag (int 0/1), written to {out_prefix}.csv. + + 'in_regime' marks whether the estimate is backed by data or by model + extrapolation. It is 1 only when all four corners of the mutant cycle (wt, + both singles, double) have their theta posterior (central 95% interval by + default) inside the resolvable band [regime_eps, 1 - regime_eps]. When a + corner's theta is near saturation (0 or 1), logit(theta) is compressed and + the growth data constrain it weakly, so the epistasis there leans on the + theta-model's functional form and cross-genotype posterior covariance -- + treat in_regime == 0 rows as model-conditional. (This checks posterior mass + only; it does not separately test whether the growth signal exceeds the + growth noise.) + + Only genotypes seen during training are supported (the joint sample matrix + is built from the training theta curves); requesting an out-of-training + genotype is an error. + + Parameters + ---------- + config_file : str + Path to the YAML configuration file. + param_file : str + Path to a posterior .h5 file produced by tfs-sample-posterior, or a MAP + checkpoint .pkl file produced by tfs-fit-model. A .pkl provides a + single point estimate, so every ep_ column collapses to that + value (no uncertainty); run tfs-sample-posterior first to obtain a + Laplace posterior and real quantiles. + out_prefix : str, optional + Prefix for the output CSV file. Written to {out_prefix}.csv. + Default 'tfs_pred_epistasis'. + genotypes_file : str or None, optional + Plain-text file with one genotype per line (slash-separated mutations, + e.g. 'M42I/K84L', or 'wt'). Restricts the analysis to cycles whose + genotypes appear here (unioned with all training genotypes unless + --only_files is set). All genotypes must have been seen during + training. Default None. + titrant_names_file : str or None, optional + Plain-text file with one titrant name per line. Must be provided + together with titrant_concs_file (or both omitted). The resulting + (name, conc) pairs are unioned with the training titrant grid unless + --only_files is set. A single name is broadcast across all + concentrations in titrant_concs_file. Default None. + titrant_concs_file : str or None, optional + Plain-text file with one concentration per line. Must be provided + together with titrant_names_file (or both omitted). Default None. + only_files : bool, optional + If True, use only the genotypes and titrant (name, conc) pairs supplied + via file arguments, ignoring training-data combinations. Default False. + scale : {"logit", "add", "mult"}, optional + Epistatic scale. 'logit' (default) computes additive epistasis of + logit(theta) -- the natural scale for an occupancy in [0, 1]. 'add': + (Y11 - Y10) - (Y01 - Y00). 'mult': (Y11 / Y10) / (Y01 / Y00). + scale_constant : float, optional + Constant applied to the transform before the difference-of-differences; + multiplies the reported epistasis. Default 1.0. Mainly a unit + conversion for scale='logit': since logit(theta) = -dG/RT, passing -RT + (e.g. -0.6159 for kcal/mol at 310.15 K) reports epistasis as an + interaction free energy. Has no effect on (and is rejected for) + scale='mult'. + regime_eps : float, optional + Theta resolution floor for the 'in_regime' flag. A cycle corner counts + as resolvable when its theta posterior sits in [regime_eps, + 1 - regime_eps]. Default 0.01 (matching a 1%-in-theta resolution, whose + logit band is +/- ~4.6). Must be in [0, 0.5). + regime_ci : float, optional + Central posterior-mass fraction required inside the band for a corner to + count as in-regime. Default 0.95 (checks the theta 2.5-97.5% interval). + Must be in (0, 1). + """ + if (titrant_names_file is None) != (titrant_concs_file is None): + raise ValueError( + "titrant_names_file and titrant_concs_file must be provided " + "together (or both omitted)." + ) + + print(f"Loading configuration from {config_file}...", flush=True) + orchestrator, _ = read_configuration(config_file) + is_map = param_file.endswith(".pkl") + param_file = resolve_param_file(param_file, orchestrator, out_prefix) + + training_genotypes = set(orchestrator.training_tm.df["genotype"].unique()) + + # Resolve requested genotypes and fail fast on any out-of-training genotype: + # the joint sample matrix only covers training genotypes. + file_genotypes = read_lines(genotypes_file) if genotypes_file else [] + out_of_training = [g for g in file_genotypes if g not in training_genotypes] + if out_of_training: + raise ValueError( + "tfs-predict-epistasis only supports genotypes seen during " + "training (joint epistasis needs a joint theta sample for every " + "cycle corner). The following requested genotype(s) were not in " + f"the training data: {out_of_training}." + ) + + if only_files and file_genotypes: + requested_genotypes = set(file_genotypes) + elif file_genotypes: + requested_genotypes = training_genotypes | set(file_genotypes) + else: + requested_genotypes = training_genotypes + + # Resolve requested titrant grid (mirrors tfs-predict-theta). + if titrant_names_file is not None: + titrant_names = read_lines(titrant_names_file) + titrant_concs = [float(x) for x in read_lines(titrant_concs_file)] + if len(titrant_names) == 1: + titrant_names = titrant_names * len(titrant_concs) + elif len(titrant_names) != len(titrant_concs): + raise ValueError( + f"titrant_names_file has {len(titrant_names)} entries but " + f"titrant_concs_file has {len(titrant_concs)}. Supply either " + "one name (broadcast to all concentrations) or one per " + "concentration." + ) + file_titrant_df = pd.DataFrame({ + "titrant_name": titrant_names, + "titrant_conc": titrant_concs, + }) + if only_files: + manual_titrant_df = file_titrant_df + else: + training_titrant_df = ( + orchestrator.training_tm.df[["titrant_name", "titrant_conc"]] + .drop_duplicates() + .reset_index(drop=True) + ) + manual_titrant_df = ( + pd.concat([training_titrant_df, file_titrant_df]) + .drop_duplicates() + .reset_index(drop=True) + ) + else: + manual_titrant_df = None + + print(f"Computing joint epistasis (scale='{scale}') over " + f"{len(requested_genotypes)} genotype(s)...", flush=True) + + q_to_get = [0.5] if is_map else None + result_df = extract_theta_epistasis( + orchestrator=orchestrator, + posteriors=param_file, + q_to_get=q_to_get, + manual_titrant_df=manual_titrant_df, + scale=scale, + scale_constant=scale_constant, + regime_eps=regime_eps, + regime_ci=regime_ci, + ) + + # Restrict cycles to those whose double mutant is a requested genotype. + if not result_df.empty: + result_df = result_df[ + result_df["genotype"].isin(requested_genotypes) + ].reset_index(drop=True) + + out_file = f"{out_prefix}.csv" + result_df.to_csv(out_file, index=False) + print(f"Wrote {len(result_df)} rows to {out_file}", flush=True) + + +def main(): + generalized_main(predict_epistasis, + manual_arg_types={"genotypes_file": str, + "titrant_names_file": str, + "titrant_concs_file": str, + "only_files": bool, + "scale": str}) + + +if __name__ == "__main__": + main() diff --git a/tests/tfscreen/analysis/test_compare_theta.py b/tests/tfscreen/analysis/test_compare_feature.py similarity index 87% rename from tests/tfscreen/analysis/test_compare_theta.py rename to tests/tfscreen/analysis/test_compare_feature.py index f9e7f69..2fb19c8 100644 --- a/tests/tfscreen/analysis/test_compare_theta.py +++ b/tests/tfscreen/analysis/test_compare_feature.py @@ -1,14 +1,14 @@ """ -Unit tests for tfscreen.analysis.compare_theta. +Unit tests for tfscreen.analysis.compare_feature. """ import numpy as np import pandas as pd import pytest -from tfscreen.analysis.compare_theta import ( - compare_theta, - aggregate_theta, +from tfscreen.analysis.compare_feature import ( + compare_feature, + aggregate_feature, stability_crosstabs, _detect_keys, _extract_run, @@ -119,7 +119,7 @@ def test_extract_run_sigma(): df = make_estimate({"wt": [0.3, 0.7]}, sigma=0.05) keys = _detect_keys(df) out = _extract_run(df, keys, 0.5, (0.159, 0.841)) - np.testing.assert_allclose(out["theta"].to_numpy(), [0.3, 0.7]) + np.testing.assert_allclose(out["value"].to_numpy(), [0.3, 0.7]) np.testing.assert_allclose(out["sigma"].to_numpy(), [0.05, 0.05]) @@ -159,7 +159,7 @@ def test_assign_tier_low_coverage(): def test_mean_mode_identical_runs(): df = make_estimate({"wt": [0.2, 0.8], "m1": [0.1, 0.9]}) - result = compare_theta([df, df.copy(), df.copy()]) + result = compare_feature([df, df.copy(), df.copy()]) assert (result["rms_sd"] == 0).all() assert (result["max_sd"] == 0).all() assert (result["tier"] == "A").all() @@ -171,7 +171,7 @@ def test_mean_mode_known_offset_halfrange(): d = 0.2 r1 = make_estimate({"g": [0.5, 0.5]}) r2 = make_estimate({"g": [0.5, 0.5 + d]}) - result = compare_theta([r1, r2]) + result = compare_feature([r1, r2]) row = get_row(result, "g") assert row["spread_estimator"] == "half_range" # conc0 spread 0, conc1 spread d/2 @@ -183,7 +183,7 @@ def test_mean_mode_std_estimator_for_large_n(): # >=5 runs => sample std (ddof=1). thetas = [0.5, 0.5, 0.5, 0.5, 0.6] # at a single conc across 5 runs runs = [make_estimate({"g": [t]}, concs=(0.0,)) for t in thetas] - result = compare_theta(runs) + result = compare_feature(runs) row = get_row(result, "g") assert row["spread_estimator"] == "std" np.testing.assert_allclose(row["rms_sd"], np.std(thetas, ddof=1)) @@ -193,7 +193,7 @@ def test_flat_curve_is_tier_a_and_zero_range(): # Genotype pinned flat across all concs and runs: perfectly stable, no # dynamic range. Must land in tier A (the whole point of absolute spread). flat = make_estimate({"flat": [1e-4, 1e-4, 1e-4]}, concs=(0.0, 1.0, 2.0)) - result = compare_theta([flat, flat.copy(), flat.copy()]) + result = compare_feature([flat, flat.copy(), flat.copy()]) row = get_row(result, "flat") assert row["tier"] == "A" np.testing.assert_allclose(row["dynamic_range"], 0.0) @@ -205,7 +205,7 @@ def test_axis2_wide_sigma_not_flagged(): d = 0.2 r1 = make_estimate({"g": [0.5, 0.5]}, sigma=0.5) r2 = make_estimate({"g": [0.5, 0.5 + d]}, sigma=0.5) - result = compare_theta([r1, r2], overdispersion_threshold=2.0) + result = compare_feature([r1, r2], overdispersion_threshold=2.0) row = get_row(result, "g") # chi2 = d^2/(2 sigma^2), dof = 4 terms - 2 grids = 2 expected = (d ** 2 / (2 * 0.5 ** 2)) / 2 @@ -217,7 +217,7 @@ def test_axis2_tight_sigma_flagged(): d = 0.2 r1 = make_estimate({"g": [0.5, 0.5]}, sigma=0.01) r2 = make_estimate({"g": [0.5, 0.5 + d]}, sigma=0.01) - result = compare_theta([r1, r2], overdispersion_threshold=2.0) + result = compare_feature([r1, r2], overdispersion_threshold=2.0) row = get_row(result, "g") expected = (d ** 2 / (2 * 0.01 ** 2)) / 2 np.testing.assert_allclose(row["overdispersion"], expected) @@ -228,7 +228,7 @@ def test_axis2_zero_sigma_skipped(): # A zero-width interval must not blow up axis 2 (division by zero). r1 = make_estimate({"g": [0.5, 0.5]}, sigma=0.0) r2 = make_estimate({"g": [0.5, 0.6]}, sigma=0.0) - result = compare_theta([r1, r2]) + result = compare_feature([r1, r2]) row = get_row(result, "g") # All chi2 terms invalid -> overdispersion is NaN, not inf, and not flagged. assert np.isnan(row["overdispersion"]) @@ -243,7 +243,7 @@ def test_reference_mode_pooled_deviation(): a, b = 0.1, 0.3 e1 = make_estimate({"g": [0.5, 0.5 + a]}, sigma=0.1) e2 = make_estimate({"g": [0.5, 0.5 - b]}, sigma=0.1) - result = compare_theta([e1, e2], reference_df=ref) + result = compare_feature([e1, e2], reference_df=ref) row = get_row(result, "g") assert row["mode"] == "reference" assert row["spread_estimator"] == "rms_dev" @@ -261,7 +261,7 @@ def test_reference_mode_overdispersion_includes_ref_sigma(): ref = make_estimate({"g": [0.5]}, concs=(0.0,), sigma=0.1) a = 0.2 e1 = make_estimate({"g": [0.5 + a]}, concs=(0.0,), sigma=0.1) - result = compare_theta([e1], reference_df=ref) + result = compare_feature([e1], reference_df=ref) row = get_row(result, "g") # single term: dev^2 / (sigma^2 + sigma_ref^2); dof = 1 term (fixed target) expected = a ** 2 / (0.1 ** 2 + 0.1 ** 2) @@ -271,7 +271,7 @@ def test_reference_mode_overdispersion_includes_ref_sigma(): def test_reference_mode_drops_genotype_absent_from_reference(capsys): ref = make_estimate({"g": [0.5, 0.5]}) e1 = make_estimate({"g": [0.5, 0.6], "extra": [0.1, 0.2]}) - result = compare_theta([e1, e1.copy()], reference_df=ref) + result = compare_feature([e1, e1.copy()], reference_df=ref) assert "extra" not in set(result["genotype"]) assert "g" in set(result["genotype"]) out = capsys.readouterr().out @@ -287,7 +287,7 @@ def test_low_coverage_tier_and_n_present(): r3 = make_estimate(base) # 'c' appears in only 1 of 4 runs r4 = make_estimate({"a": [0.1, 0.2], "b": [0.3, 0.4], "c": [0.9, 0.9]}) - result = compare_theta([r1, r2, r3, r4], min_coverage=0.5) + result = compare_feature([r1, r2, r3, r4], min_coverage=0.5) c = get_row(result, "c") assert c["n_present"] == 1 assert c["tier"] == "low_coverage" @@ -301,20 +301,20 @@ def test_grid_mismatch_raises(): r1 = make_estimate({"g": [0.1, 0.2]}, concs=(0.0, 1.0)) r2 = make_estimate({"g": [0.1, 0.2]}, concs=(0.0, 2.0)) with pytest.raises(ValueError, match="condition grid"): - compare_theta([r1, r2]) + compare_feature([r1, r2]) def test_key_mismatch_raises(): r1 = make_estimate({"g": [0.1, 0.2]}, titrant_name="iptg") r2 = make_estimate({"g": [0.1, 0.2]}) # no titrant_name with pytest.raises(ValueError, match="key columns"): - compare_theta([r1, r2]) + compare_feature([r1, r2]) def test_mean_mode_requires_two_runs(): r1 = make_estimate({"g": [0.1, 0.2]}) with pytest.raises(ValueError, match="at least 2"): - compare_theta([r1]) + compare_feature([r1]) # --- titrant_name handling --------------------------------------------------- @@ -325,7 +325,7 @@ def test_titrant_name_dynamic_range_is_per_name_max(): iptg = make_estimate({"g": [0.5, 0.5]}, titrant_name="iptg") atc = make_estimate({"g": [0.1, 0.9]}, titrant_name="atc") r1 = pd.concat([iptg, atc], ignore_index=True) - result = compare_theta([r1, r1.copy()]) + result = compare_feature([r1, r1.copy()]) row = get_row(result, "g") np.testing.assert_allclose(row["dynamic_range"], 0.8) # per-grid sd columns exist for both titrants @@ -343,7 +343,7 @@ def test_stability_crosstabs_populates_cells(): unstable_b = make_estimate({"u": [0.5, 0.9]}, sigma=0.001) r1 = pd.concat([stable, unstable_a], ignore_index=True) r2 = pd.concat([stable.copy(), unstable_b], ignore_index=True) - result = compare_theta([r1, r2]) + result = compare_feature([r1, r2]) tabs = stability_crosstabs(result) assert "tier_vs_overdispersion" in tabs assert "tier_vs_dynamic_range" in tabs @@ -354,11 +354,30 @@ def test_stability_crosstabs_populates_cells(): def test_result_sorted_by_rms_sd(): r1 = make_estimate({"a": [0.5, 0.5], "b": [0.5, 0.5]}) r2 = make_estimate({"a": [0.5, 0.55], "b": [0.5, 0.9]}) - result = compare_theta([r1, r2]) + result = compare_feature([r1, r2]) assert list(result["rms_sd"]) == sorted(result["rms_sd"]) -# --- aggregate_theta --------------------------------------------------------- +def test_sd_tier_edges_rescale_the_grading(): + """Custom sd_tier_edges re-grade the same feature (the generalization knob). + + Two runs disagree by a fixed 0.10 at every grid point; with N=2 the spread + estimator is the half-range, so rms_sd == 0.05 exactly. That value lands on + the C side of the default edges but the B side of a slightly looser cut, + proving the edges thread through and set the feature's grading scale. + """ + r1 = make_estimate({"g": [0.30, 0.30]}) + r2 = make_estimate({"g": [0.40, 0.40]}) + + default = compare_feature([r1, r2]) # edges (0.02, 0.05, 0.10) + assert default.loc[0, "rms_sd"] == pytest.approx(0.05) + assert default.loc[0, "tier"] == "C" + + loosened = compare_feature([r1, r2], sd_tier_edges=(0.02, 0.06, 0.10)) + assert loosened.loc[0, "tier"] == "B" + + +# --- aggregate_feature --------------------------------------------------------- def agg_row(agg, genotype, conc): sub = agg[(agg["genotype"] == genotype) & (agg["titrant_conc"] == conc)] @@ -415,7 +434,7 @@ def test_mixture_quantiles_point_mass_mixed_is_monotone_between(): def test_aggregate_identical_runs_matches_input(): df = make_estimate_ladder({"wt": [0.2, 0.8], "m1": [0.5, 0.5]}) - agg = aggregate_theta([df, df.copy(), df.copy()]) + agg = aggregate_feature([df, df.copy(), df.copy()]) # schema: keys + ladder + n_present for lvl in _LEVELS: assert f"q{lvl}" in agg.columns @@ -430,7 +449,7 @@ def test_aggregate_identical_runs_matches_input(): def test_aggregate_between_variance_widens(): r1 = make_estimate_ladder({"g": [0.4, 0.4]}, sigma=0.05) r2 = make_estimate_ladder({"g": [0.6, 0.6]}, sigma=0.05) - agg = aggregate_theta([r1, r2]) + agg = aggregate_feature([r1, r2]) row = agg_row(agg, "g", 0.0) agg_width = row["q0.841"] - row["q0.159"] single_width = ( @@ -447,14 +466,14 @@ def test_aggregate_coverage_reported(): r1 = make_estimate_ladder(base) r2 = make_estimate_ladder(base) r3 = make_estimate_ladder({"a": [0.2, 0.8], "b": [0.5, 0.5]}) - agg = aggregate_theta([r1, r2, r3]) + agg = aggregate_feature([r1, r2, r3]) assert agg_row(agg, "a", 0.0)["n_present"] == 3 assert agg_row(agg, "b", 0.0)["n_present"] == 1 def test_aggregate_with_titrant_name(): r1 = make_estimate_ladder({"g": [0.2, 0.8]}, titrant_name="iptg") - agg = aggregate_theta([r1, r1.copy()]) + agg = aggregate_feature([r1, r1.copy()]) assert "titrant_name" in agg.columns assert (agg["titrant_name"] == "iptg").all() @@ -462,11 +481,11 @@ def test_aggregate_with_titrant_name(): def test_aggregate_requires_two_runs(): r1 = make_estimate_ladder({"g": [0.2, 0.8]}) with pytest.raises(ValueError, match="at least 2"): - aggregate_theta([r1]) + aggregate_feature([r1]) def test_aggregate_key_mismatch_raises(): r1 = make_estimate_ladder({"g": [0.2, 0.8]}, titrant_name="iptg") r2 = make_estimate_ladder({"g": [0.2, 0.8]}) with pytest.raises(ValueError, match="key columns"): - aggregate_theta([r1, r2]) + aggregate_feature([r1, r2]) diff --git a/tests/tfscreen/tfmodel/analysis/test_extract_theta_epistasis.py b/tests/tfscreen/tfmodel/analysis/test_extract_theta_epistasis.py new file mode 100644 index 0000000..d384184 --- /dev/null +++ b/tests/tfscreen/tfmodel/analysis/test_extract_theta_epistasis.py @@ -0,0 +1,264 @@ +import pytest +import numpy as np +import pandas as pd +from unittest.mock import MagicMock + +from tfscreen.tfmodel.model_orchestrator import ModelOrchestrator +from tfscreen.tfmodel.analysis.extraction import extract_theta_epistasis +from tfscreen.analysis.extract_epistasis import extract_epistasis + + +# A four-genotype mutant cycle: wt, two singles, and their double. Groups are +# assigned so that compute_theta_samples indexes theta_* posterior columns by +# map_theta_group: wt->0, A10G->1, C20D->2, A10G/C20D->3. +_GENOTYPES = ["wt", "A10G", "C20D", "A10G/C20D"] + + +def _make_model(concs=(1.0,)): + """Minimal orchestrator mock driving the real hill_geno component.""" + rows = [] + for g_idx, g in enumerate(_GENOTYPES): + for c in concs: + rows.append({"genotype": g, + "titrant_name": "iptg", + "titrant_conc": float(c), + "map_theta_group": g_idx}) + model = MagicMock(spec=ModelOrchestrator) + model._theta = "hill_geno" + tm = MagicMock() + tm.df = pd.DataFrame(rows) + model.training_tm = tm + model.growth_tm = tm + return model + + +def _flat_posteriors(theta_per_group): + """ + Build posterior samples that make theta == theta_low for every genotype + (theta_high == theta_low collapses the Hill curve to a constant), so the + per-genotype theta is exactly ``theta_per_group``. + + Parameters + ---------- + theta_per_group : np.ndarray, shape (S, 4) + Desired theta for (wt, A10G, C20D, A10G/C20D) in each of S draws. + """ + theta_per_group = np.asarray(theta_per_group, dtype=float) + S = theta_per_group.shape[0] + return { + "theta_hill_n": np.ones((S, 4)), + "theta_log_hill_K": np.zeros((S, 4)), + "theta_theta_high": theta_per_group.copy(), + "theta_theta_low": theta_per_group.copy(), + } + + +def _logit(x): + return np.log(x / (1.0 - x)) + + +def test_epistasis_value_matches_hand_computation(): + """ep_q0.5 equals the logit difference-of-differences of the corner thetas.""" + # Constant across draws so the median is exact. + S = 50 + theta = np.tile([0.5, 0.6, 0.7, 0.9], (S, 1)) + model = _make_model() + post = _flat_posteriors(theta) + + result = extract_theta_epistasis(model, post, scale="logit") + + assert len(result) == 1 + row = result.iloc[0] + assert row["genotype"] == "A10G/C20D" + + # Quantile columns follow the library-wide bare-q convention (no ep_ prefix). + assert "q0.5" in result.columns + assert not any(c.startswith("ep_") for c in result.columns) + + # 00=wt(0.5), 01=A10G(0.6), 10=C20D(0.7), 11=double(0.9) + expected = (_logit(0.9) - _logit(0.7)) - (_logit(0.6) - _logit(0.5)) + assert np.isclose(row["q0.5"], expected, atol=1e-9) + + +def test_joint_covariance_shrinks_uncertainty_vs_marginal(): + """ + Perfectly correlated corners -> joint epistasis is exactly zero in every + draw (zero-width posterior), while the marginal path (which assumes the + four corners are independent) reports a substantial nonzero ep_std. + """ + rng = np.random.default_rng(0) + S = 2000 + # Every corner shares the SAME theta in each draw -> logit ddd == 0 exactly. + shared = rng.uniform(0.2, 0.8, size=S) + theta = np.repeat(shared[:, None], 4, axis=1) + + model = _make_model() + post = _flat_posteriors(theta) + + joint = extract_theta_epistasis(model, post, q_to_get=[0.5, 0.159, 0.841], + scale="logit") + joint_row = joint.iloc[0] + + # Joint: ep is identically zero -> point estimate 0, zero-width interval. + assert np.isclose(joint_row["q0.5"], 0.0, atol=1e-9) + joint_width = joint_row["q0.841"] - joint_row["q0.159"] + assert np.isclose(joint_width, 0.0, atol=1e-9) + + # Marginal path on the SAME samples: build a per-genotype theta table with a + # median and a std from the same posterior draws, then run the independent + # error-propagation path. + theta_med = np.quantile(theta, 0.5, axis=0) + theta_std = (np.quantile(theta, 0.841, axis=0) + - np.quantile(theta, 0.159, axis=0)) / 2.0 + marg_df = pd.DataFrame({ + "genotype": _GENOTYPES, + "titrant_name": "iptg", + "titrant_conc": 1.0, + "theta": theta_med, + "theta_std": theta_std, + }) + marg = extract_epistasis(marg_df, y_obs="theta", y_std="theta_std", + group_by=["titrant_name", "titrant_conc"], + scale="logit") + + # The marginal path cannot see the correlation, so it reports real spread. + assert marg.iloc[0]["ep_std"] > 0.1 + assert marg.iloc[0]["ep_std"] > joint_width + + +def test_in_regime_column_present_and_int(): + S = 20 + theta = np.tile([0.5, 0.6, 0.7, 0.9], (S, 1)) + result = extract_theta_epistasis(_make_model(), _flat_posteriors(theta)) + assert "in_regime" in result.columns + assert result["in_regime"].dtype.kind in ("i", "u") + # every corner comfortably inside [0.01, 0.99] -> in regime + assert result.iloc[0]["in_regime"] == 1 + + +def test_in_regime_false_when_a_corner_saturates(): + # Double mutant pinned at 0.999 -> above 1 - eps=0.99 -> out of regime. + S = 20 + theta = np.tile([0.5, 0.6, 0.7, 0.999], (S, 1)) + result = extract_theta_epistasis(_make_model(), _flat_posteriors(theta)) + assert result.iloc[0]["in_regime"] == 0 + + +def test_in_regime_uses_interval_not_median(): + # A corner whose posterior MEDIAN is in-band but whose upper tail crosses the + # boundary is out of regime (the flag checks the central interval). + rng = np.random.default_rng(1) + S = 4000 + wide = rng.uniform(0.95, 0.9999, size=S) # median ~0.975 (< 0.99), q0.975 > 0.99 + theta = np.column_stack([np.full(S, 0.5), np.full(S, 0.6), + np.full(S, 0.7), wide]) + assert np.median(wide) < 0.99 # median is inside the band + result = extract_theta_epistasis(_make_model(), _flat_posteriors(theta), + regime_ci=0.95) + assert result.iloc[0]["in_regime"] == 0 + + +def test_in_regime_flags_tight_but_saturated_epistasis(): + # The subtle case: all four corners perfectly correlated near saturation. + # Epistasis is identically zero (a *tight* posterior), yet the corners are + # saturated -> in_regime must flag it as model-conditional, not data-backed. + rng = np.random.default_rng(2) + S = 3000 + shared = rng.uniform(0.98, 0.9999, size=S) + theta = np.repeat(shared[:, None], 4, axis=1) + result = extract_theta_epistasis(_make_model(), _flat_posteriors(theta), + q_to_get=[0.5, 0.159, 0.841]) + row = result.iloc[0] + assert np.isclose(row["q0.5"], 0.0, atol=1e-9) # tight + assert np.isclose(row["q0.841"] - row["q0.159"], 0.0, atol=1e-9) + assert row["in_regime"] == 0 # but flagged + + +def test_in_regime_respects_regime_eps(): + # A corner at 0.995 is out of the default band but inside a looser eps=0.001. + S = 20 + theta = np.tile([0.5, 0.6, 0.7, 0.995], (S, 1)) + strict = extract_theta_epistasis(_make_model(), _flat_posteriors(theta), + regime_eps=0.01) + loose = extract_theta_epistasis(_make_model(), _flat_posteriors(theta), + regime_eps=0.001) + assert strict.iloc[0]["in_regime"] == 0 + assert loose.iloc[0]["in_regime"] == 1 + + +def test_in_regime_map_single_draw_is_point_check(): + # One "draw" (MAP): the interval collapses to the point value. + theta = np.array([[0.5, 0.6, 0.7, 0.999]]) # S=1, double saturated + result = extract_theta_epistasis(_make_model(), _flat_posteriors(theta), + q_to_get=[0.5]) + assert result.iloc[0]["in_regime"] == 0 + + +@pytest.mark.parametrize("bad", [-0.1, 0.5, 0.7]) +def test_bad_regime_eps_raises(bad): + with pytest.raises(ValueError, match="regime_eps"): + extract_theta_epistasis(_make_model(), _flat_posteriors( + np.tile([0.5, 0.6, 0.7, 0.9], (5, 1))), regime_eps=bad) + + +@pytest.mark.parametrize("bad", [0.0, 1.0, 1.5]) +def test_bad_regime_ci_raises(bad): + with pytest.raises(ValueError, match="regime_ci"): + extract_theta_epistasis(_make_model(), _flat_posteriors( + np.tile([0.5, 0.6, 0.7, 0.9], (5, 1))), regime_ci=bad) + + +def test_no_complete_cycle_returns_empty(): + """Without a double mutant, no cycle exists; output is empty but well-formed.""" + model = _make_model() + # Drop the double mutant from the training data. + model.training_tm.df = model.training_tm.df[ + model.training_tm.df["genotype"] != "A10G/C20D" + ].reset_index(drop=True) + + result = extract_theta_epistasis(model, _flat_posteriors( + np.tile([0.5, 0.6, 0.7], (5, 1))), scale="logit") + + assert result.empty + assert "genotype" in result.columns + assert any(c.startswith("q") for c in result.columns) + assert "in_regime" in result.columns + + +def test_double_with_missing_single_is_dropped(): + """A double whose single parent has no theta row yields no cycle (not a crash).""" + model = _make_model() + # Remove one single parent; the double + wt + other single remain. + keep = model.training_tm.df["genotype"] != "C20D" + model.training_tm.df = model.training_tm.df[keep].reset_index(drop=True) + + # 3 groups remain (wt=0, A10G=1, A10G/C20D=3 in the original ids -> but the + # trimmed frame carries its own map_theta_group values); use a matching post. + S = 5 + n_groups = model.training_tm.df["map_theta_group"].nunique() + theta = np.tile(np.linspace(0.4, 0.8, n_groups), (S, 1)) + # Remap groups to a contiguous 0..n-1 range so compute_theta_samples indexes + # cleanly into the posterior columns. + remap = {g: i for i, g in + enumerate(sorted(model.training_tm.df["map_theta_group"].unique()))} + model.training_tm.df["map_theta_group"] = \ + model.training_tm.df["map_theta_group"].map(remap) + + result = extract_theta_epistasis(model, _flat_posteriors(theta), scale="logit") + assert result.empty + + +def test_wrong_theta_component_raises(): + model = _make_model() + model._theta = "no_such_component" # not registered -> module is None + with pytest.raises(ValueError, match="build_calc_df"): + extract_theta_epistasis(model, _flat_posteriors( + np.tile([0.5, 0.6, 0.7, 0.9], (5, 1)))) + + +def test_mult_scale_constant_rejected(): + model = _make_model() + with pytest.raises(ValueError, match="scale_constant"): + extract_theta_epistasis(model, _flat_posteriors( + np.tile([0.5, 0.6, 0.7, 0.9], (5, 1))), + scale="mult", scale_constant=2.0) From 873847c7a50ce623352072dfa55050b9676772f2 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Wed, 22 Jul 2026 18:04:55 -0400 Subject: [PATCH 25/26] added docs/manuscript/ --- docs/manuscript/methods.md | 245 +++++++++++++++++++++++++++++++++++++ reports/flake.txt | 20 ++- reports/junit/junit.xml | 2 +- 3 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 docs/manuscript/methods.md diff --git a/docs/manuscript/methods.md b/docs/manuscript/methods.md new file mode 100644 index 0000000..d7b9ea5 --- /dev/null +++ b/docs/manuscript/methods.md @@ -0,0 +1,245 @@ +# Methods (draft) + +> **Draft status.** This file covers the inference protocol only: the model, the +> fitting workflow, and the multi-seed aggregation used for all downstream +> analyses. Sections marked `[TODO]` are placeholders for material we still need +> to write (k-fold cross validation, comparisons against independent +> measurements, library construction, sequencing). + +## Software overview + +We analyzed the screen with `tfscreen`, a Python package we wrote for +simulating and analyzing high-throughput screens of transcription factor (TF) +libraries [TODO: version, DOI, repository URL]. `tfscreen` treats the screen as +a single generative process that runs from TF–operator occupancy through +bacterial growth to sequencing read counts, and inverts that process by +hierarchical Bayesian inference. The package covers the whole path — read +counts to occupancy — but the analyses reported here used three of its stages: +conversion of read counts into per-genotype colony-forming-unit (CFU) +trajectories, joint inference of the generative model, and posterior +prediction of the quantities we interpret biologically (fractional occupancy +θ and second-order epistasis in θ). + +We implemented the generative model in NumPyro (v0.19) on JAX (v0.8), and fit +it by stochastic variational inference (SVI) with the Adam optimizer as +implemented in `optax` (v0.2). + +## The generative model + +### What the model says + +The central quantity is θ, the fractional occupancy of the TF on its operator. +θ is not observed directly in the screen; it is observed through growth. Each +genotype in the library carries a TF variant that regulates a selection marker, +so a genotype's growth rate under selection reports on how well its TF occupies +the operator at a given effector (titrant) concentration. We modeled the log +population size of every genotype in every tube as + +``` +ln_cfu = ln_cfu0 + (k_pre + dk_geno + m_pre · A · θ) · t_pre + + (k_sel + dk_geno + m_sel · A · θ) · t_sel +``` + +where + +- `ln_cfu0` is the genotype's log abundance at the start of the experiment, +- `k_pre` / `k_sel` are per-condition baseline growth rates (pre-selection and + selection phases), +- `m_pre` / `m_sel` are the per-condition slopes coupling occupancy to growth, +- `dk_geno` is the pleiotropic growth effect of the genotype, independent of TF + activity, +- `A` is per-genotype TF activity (how strongly occupancy translates into + regulation), and +- `t_pre` / `t_sel` are the durations of the two phases. + +The model is modular: each term above is supplied by an interchangeable +component, and the components we selected are listed below. This structure let +us test alternative functional forms (e.g. different growth linkages or +occupancy parameterizations) against the same data without rewriting the model. + +### Components we used + +| Model axis | Choice | What it does | +|---|---|---| +| `theta` | `hill_mut` | θ follows a Hill curve in titrant concentration. Each of the four Hill parameters — `logit(θ_low)`, `logit(θ_high − θ_low)`, `log K`, `log n` — is written as a wild-type value plus additive per-mutation deltas in the transformed space, plus pairwise epistasis terms (`--epistasis`). Per-mutation deltas are hierarchical (`Normal(0, σ_d)`, σ_d inferred); pairwise terms carry a regularized horseshoe prior, so epistasis is sparse by default and only supported where the data demand it. | +| `condition_growth` | `linear` | Growth rate is linear in occupancy: `g = k_condition + dk_geno + m · A · θ`, with per-condition `k` and `m`. | +| `growth_transition` | `instant` | Genotypes switch from the pre-selection to the selection growth rate instantaneously at the split. | +| `ln_cfu0` | `hierarchical_factored` | `ln_cfu0[r, c, g] = geno_baseline[r, g] + tube_offset[r, c]`, separating genotype abundance in the library from tube-to-tube dilution differences. | +| `dk_geno` | `hierarchical_geno` | Per-genotype pleiotropic growth effect drawn from a pooled, left-skewed prior (a shifted negative log-normal), encoding the expectation that most mutations are neutral, a few beneficial, and a long tail deleterious. Wild type is pinned to `dk_geno = 0`. | +| `activity` | `fixed` | `A ≡ 1`. The TF in this system is a repressor that either blocks or does not; residual leaky repression is absorbed into `θ_low`. | +| `transformation` | `empirical` | Corrects for congression — cells that took up more than one plasmid during transformation. | +| `theta_rescale` | `passthrough` | θ enters the growth model directly (identity). | +| `theta_growth_noise` | `logit_normal` | Additive `Normal(0, σ_logit)` noise on `logit(θ)`, so occupancy noise is largest near θ = 0.5 and vanishes at saturation or depletion. σ_logit is a single global scalar inferred from the data. | +| `theta_binding_noise` | `zero` | The binding measurements carry their own reported errors; no extra noise term. | +| `growth_noise` | `normal_kt` | A single global `σ_k`, added in quadrature to the per-observation `ln_cfu` error, capturing biological growth variation not explained by θ or `dk_geno`. | + +### Congression correction + +Transformation of the library placed more than one plasmid in some cells. Since +the tightest-binding TF variant in a cell sets the effective occupancy at the +operator, a cell's growth reports the *maximum* occupancy over its plasmid +complement rather than the focal genotype's own occupancy — an attenuation that +pulls every measured curve toward the population's upper tail. We modeled this +explicitly: given the co-transformation rate λ, the corrected occupancy is +`E[max(θ_focal, background)]`, where the background distribution is the +empirical distribution of θ over the full genotype population at that +concentration. Because the background is itself an inferred quantity, we +evaluated it over all genotypes on every forward pass rather than over the +training minibatch. + +We measured λ independently [TODO: how] and supplied it as a moment-matched +log-normal prior (`--transformation_lambda 0.3572 0.1296`). Genotypes that were +spiked into the library as clean monoclonal controls (`wt`, `M42I`, `H74A`, +`K84L`, `M42I/H74A`, `M42I/K84L`, `H74A/K84L`, `D88A`) are congression-free by +construction, and we exempted them from the correction. + +### Observation channels + +We fit four data sets jointly, each entering the likelihood through its own +observation layer: + +1. **Growth** (`growth.csv`) — per-genotype `ln_cfu` trajectories across + replicates, timepoints, conditions, and titrant concentrations. Observed + under a Student-*t* likelihood with scale + `sqrt(ln_cfu_std² + σ_k²)`, with a boolean mask excluding + low-quality or missing cells. +2. **Binding** (`binding.csv`) — directly measured θ values for genotypes with + independent binding curves. These pin the occupancy scale that growth alone + cannot fix. +3. **Pre-split** (`presplit.csv`) — sequencing observations taken at + `t = −t_pre`, before the culture was split into conditions. These constrain + `ln_cfu0` directly. +4. **Base growth** (`base_growth.csv`) — direct reference-condition growth-rate + measurements for a subset of genotypes, entering as + `rate_obs ~ Normal(k_ref + dk_geno, rate_std)`. Together with the wild-type + `dk_geno = 0` pin, these anchor the otherwise-degenerate additive slack + between the per-condition baselines `k` and the per-genotype `dk_geno`. + +The identifiability problem the last two channels address is worth stating +explicitly: the growth likelihood is invariant to `k += C, dk_geno −= C`, so +without anchors the whole system slides by a global constant, inflating every +condition baseline. We closed this in two complementary ways — the base-growth +channel above, and per-condition priors on the baselines set by the calibration +pre-fit described next. + +## Fitting workflow + +We ran the following pipeline once per random seed. Each step is a `tfscreen` +command-line entry point; the full script is reproduced in +[TODO: supplementary file / repository path]. + +### 1. Configure the model (`tfs-configure-model`) + +We assembled the model specification from the four input tables and the +component choices above. This step wrote a YAML configuration plus two CSVs — +one holding the prior distribution for every parameter, one holding initial +values — which every later step read. It also built the mutation-by-genotype and +mutation-pair-by-genotype incidence matrices that `hill_mut` uses to decompose +genotype phenotypes into per-mutation and pairwise terms, declared the spiked +(congression-free) genotypes, and set the genotype minibatch size (65,536). + +### 2. Calibrate the growth linking function (`tfs-prefit-calibration`) + +Fitting the per-condition growth parameters simultaneously with the full +hierarchy is poorly conditioned, so we calibrated them first. We ran a MAP fit +of a deliberately collapsed model in which θ was pinned to the measured binding +values and every non-growth component was reduced to its simplest form +(`dk_geno` pinned from the base-growth measurements, `activity` and `ln_cfu0` +hyperparameters pinned to their prior locations, no noise components), against +the subset of cells observed in both the growth and binding data. We then wrote +the resulting per-condition MAP estimates of `k` and `m` back into the +production priors as per-condition prior locations, with scales floored from +the Hessian at the MAP point, and used them as warm-start values. + +Because a soft prior in SVI is only a KL penalty — one the growth likelihood +can outvote across millions of observations — we additionally hard-clamped the +occupancy slope `m` to its calibrated value (`--pin_m`). The calibration +estimate of `m` is unbiased (`dk_geno` is uncorrelated with θ), whereas the +baseline `k` carries genuine per-experiment tube-to-tube variance and sits in +the additive slide described above; we therefore pinned `k`'s prior location but +left it free to move. + +### 3. Fit the full model (`tfs-fit-model`) + +We fit the full joint model by SVI with a structured (per-component) variational +guide, optimizing the ELBO with Adam over genotype minibatches. We ran to a +relative-ELBO convergence tolerance of 5 × 10⁻⁷ [TODO: report typical epoch +counts and wall time]. + +### 4. Draw the posterior (`tfs-sample-posterior`) + +We drew 500 samples from the fitted variational posterior. Because these are +joint draws over all latent parameters, every downstream quantity inherits the +full cross-genotype posterior covariance rather than a per-genotype marginal +summary. + +### 5. Predict occupancy, growth, and epistasis + +From the posterior we computed: + +- **Occupancy** (`tfs-predict-theta`) — θ for every genotype at every titrant + concentration, summarized as posterior quantiles. +- **Second-order epistasis** (`tfs-predict-epistasis`) — for every double + mutant, epistasis on the mutant cycle formed by the double, its two + single-mutant parents, and wild type. We computed epistasis *within* each + posterior draw and then took quantiles across draws, so the reported + uncertainty reflects the posterior covariance among the four corners of each + cycle rather than treating them as independent. We worked on the logit scale, + which is the natural additive scale for an occupancy in [0, 1]: since + `logit(θ) = −ΔG/RT`, we multiplied by `−RT = −0.6159` kcal mol⁻¹ (310.15 K) + to report epistasis as an interaction free energy in kcal mol⁻¹. + Each cycle also carries an `in_regime` flag, set only when all four corners + have their central 95% θ interval inside [0.01, 0.99]. Outside that band + `logit(θ)` saturates and the linear-in-θ growth likelihood constrains it + weakly, so those estimates lean on the functional form of the θ model; we + treated them as model-conditional throughout. +- **Growth and parameters** (`tfs-predict-growth`, `tfs-extract-params`) — + posterior predictions of the observed `ln_cfu` data and per-parameter + posterior summaries, used for the fit diagnostics in + [TODO: figure reference]. + +## Aggregating across seeds + +SVI finds a local optimum, so a single fit conflates the posterior with the +particular basin the optimizer reached. We therefore repeated the entire +workflow — configure through prediction — for 10 independent random seeds, and +combined the results with `tfs-compare-feature`. + +For each feature (θ, epistasis) and each `(genotype, titrant, concentration)` +cell, we reconstructed each run's marginal posterior from its stored quantile +ladder and formed the **equal-weight mixture** across the 10 runs. By the law of +total variance the mixture's width folds in both each run's own posterior width +and the run-to-run spread of the point estimates, and — correctly — it does not +shrink with the number of runs, since different-seed fits are not independent +replicates of the experiment. All downstream analyses use these aggregate +posteriors. Any bias shared by all 10 runs is invisible to this procedure; it +captures optimization variability, not model misspecification. + +The same tool also graded every genotype on two independent axes, which we used +to filter the results: + +- **Reproducibility** — the run-to-run spread (RMS standard deviation over the + concentration grid) of the median estimate, in the feature's own units, + binned into tiers A–D at cutlines [TODO: report the `--sd_tier_edges` we + used for each feature]. +- **Self-consistency** — an overdispersion statistic (χ² per degree of freedom) + asking whether the run-to-run disagreement is explained by each run's own + reported uncertainty. This does not enter the tier; it separates genotypes + that are honestly uncertain from those that are confidently inconsistent + across runs. + +[TODO: report how many genotypes fell in each tier, and the reproducibility × +self-consistency crosstab.] + +## [TODO] k-fold cross validation + +We held out [TODO] and refit ... `tfs-compare-feature` in reference mode scores +each dropout fit by its deviation from the full-data fit rather than against the +cross-run mean. + +## [TODO] Comparison against independent measurements + +[TODO: which measurements, how they were matched to model predictions, what +agreement statistic.] + +## [TODO] Data and code availability diff --git a/reports/flake.txt b/reports/flake.txt index 5203ff1..b95e383 100644 --- a/reports/flake.txt +++ b/reports/flake.txt @@ -1,3 +1,15 @@ +./notebooks/thermo_corr/.ipynb_checkpoints/plots-checkpoint.py:12:1: F401 'pandas as pd' imported but unused +./notebooks/thermo_corr/.ipynb_checkpoints/plots-checkpoint.py:69:84: E261 at least two spaces before inline comment +./notebooks/thermo_corr/.ipynb_checkpoints/plots-checkpoint.py:69:85: E262 inline comment should start with '# ' +./notebooks/thermo_corr/assemble.py:26:1: F401 'glob' imported but unused +./notebooks/thermo_corr/plots.py:12:1: F401 'pandas as pd' imported but unused +./notebooks/thermo_corr/plots.py:69:84: E261 at least two spaces before inline comment +./notebooks/thermo_corr/plots.py:69:85: E262 inline comment should start with '# ' +./notebooks/thermo_corr/test_confound.py:14:1: F401 'thermo_corr as tc' imported but unused +./notebooks/thermo_corr/test_confound.py:41:22: E228 missing whitespace around modulo operator +./notebooks/thermo_corr/test_core.py:8:1: F401 'numpy as np' imported but unused +./notebooks/thermo_corr/tiers.py:20:1: F401 'dataclasses.field' imported but unused +./notebooks/thermo_corr/tiers.py:28:1: F401 '.confound.between_group_corr' imported but unused ./scripts/generate_struct_ensemble.py:266:11: E221 multiple spaces before operator ./scripts/generate_struct_ensemble.py:272:24: E221 multiple spaces before operator ./scripts/generate_struct_ensemble.py:433:18: E221 multiple spaces before operator @@ -7493,10 +7505,12 @@ 1839 E221 multiple spaces before operator 26 E222 multiple spaces after operator 7 E225 missing whitespace around operator +1 E228 missing whitespace around modulo operator 1097 E231 missing whitespace after ',' 103 E251 unexpected spaces around keyword / parameter equals 14 E252 missing whitespace around parameter equals -207 E261 at least two spaces before inline comment +209 E261 at least two spaces before inline comment +2 E262 inline comment should start with '# ' 2 E265 block comment should start with '# ' 2 E266 too many leading '#' for block comment 149 E272 multiple spaces before keyword @@ -7514,7 +7528,7 @@ 1 E722 do not use bare 'except' 2 E731 do not assign a lambda expression, use a def 21 E741 ambiguous variable name 'l' -230 F401 'tfscreen.tfmodel.generative.components.theta.thermo.O2_C12_K5_U0_a.thermo.run_model' imported but unused +237 F401 'pandas as pd' imported but unused 11 F541 f-string is missing placeholders 4 F811 redefinition of unused 'np' from line 1 40 F841 local variable 'e' is assigned to but never used @@ -7522,4 +7536,4 @@ 81 W292 no newline at end of file 1498 W293 blank line contains whitespace 28 W391 blank line at end of file -7477 +7489 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index 15c91a7..40f5e7b 100644 --- a/reports/junit/junit.xml +++ b/reports/junit/junit.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 56bd5ca58d4b8ade8619afb67eb09bc1a9d7ad16 Mon Sep 17 00:00:00 2001 From: Michael Harms Date: Wed, 22 Jul 2026 18:05:24 -0400 Subject: [PATCH 26/26] version bump to 0.4.3 --- src/tfscreen/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tfscreen/__version__.py b/src/tfscreen/__version__.py index 9480c73..4509340 100644 --- a/src/tfscreen/__version__.py +++ b/src/tfscreen/__version__.py @@ -5,5 +5,5 @@ Version string. """ -VERSION = (0, 4, 2) +VERSION = (0, 4, 3) __version__ = '.'.join(map(str, VERSION))