diff --git a/CLAUDE.md b/CLAUDE.md index 5c8d8995..e3fc6462 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,12 +46,16 @@ 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 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-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 @@ -91,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) | +| `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) | @@ -266,6 +270,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. @@ -273,6 +279,21 @@ 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: + +**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`. 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/`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. 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 `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 All YAML files in this codebase follow these conventions. Apply them when creating or modifying any YAML file. @@ -390,7 +411,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 @@ -404,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/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/tests-badge.svg b/docs/badges/tests-badge.svg index e8bb384a..aace045a 100644 --- a/docs/badges/tests-badge.svg +++ b/docs/badges/tests-badge.svg @@ -1 +1 @@ -tests: 4015tests4015 \ No newline at end of file +tests: 4328tests4328 \ No newline at end of file diff --git a/docs/manuscript/methods.md b/docs/manuscript/methods.md new file mode 100644 index 00000000..d7b9ea5a --- /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/docs/source/analysis.rst b/docs/source/analysis.rst index bbf1fabc..a18d84c5 100644 --- a/docs/source/analysis.rst +++ b/docs/source/analysis.rst @@ -224,20 +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 \ - --workers 8 + titrant_conc \ + q0.5 \ + --group_by titrant_name \ + --num_workers -1 + +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/examples/simulate-empirical/simulate_config.yaml b/examples/simulate-empirical/simulate_config.yaml new file mode 100644 index 00000000..72c95306 --- /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 c4b9261f..ea86e6e5 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/examples/toy_thermo/toy_thermo.ipynb b/examples/toy_thermo/toy_thermo.ipynb new file mode 100644 index 00000000..beac71eb --- /dev/null +++ b/examples/toy_thermo/toy_thermo.ipynb @@ -0,0 +1,1325 @@ +{ + "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": {}, + "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": {}, + "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": {}, + "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": {}, + "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": {}, + "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=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", + " observable_std=0.02)\n", + "print(\"genotypes:\", sorted(df['genotype'].unique()))\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a61fc694", + "metadata": {}, + "outputs": [ + { + "data": { + "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": [ + "
" + ] + }, + "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": {}, + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + "
genotypebest_modelshapefittablenonzero_q
0wtrepressorstepTrue0.0
1A1VrepressorstepTrue0.0
2A2VrepressorstepTrue0.0
3A1V/A2VrepressorstepTrue0.0
\n", + "
" + ], + "text/plain": [ + " 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, + "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\", \"shape\", \"fittable\", \"nonzero_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": {}, + "outputs": [ + { + "data": { + "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": [ + "
" + ] + }, + "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": {}, + "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." + ] + }, + { + "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": 10, + "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": 11, + "id": "77e62a5d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAssAAAG1CAYAAAAPyLn3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAvqBJREFUeJzs3Xd8FEX/wPHPXsml94QkkEbvvUkLoBRBiqKABSmCYkUFC1gQ5XmsqI/8LFgogiIKKCigoITeu/SW0AJJSO+5sr8/Qg6OS+OSEMr3/SIvbndmZ2f37va+Nzc7o6iqqiKEEEIIIYSwo6nqCgghhBBCCHGjkmBZCCGEEEKIYkiwLIQQQgghRDEkWBZCCCGEEKIYEiwLIYQQQghRDAmWhRBCCCGEKIYEy0IIIYQQQhRDgmUhhBBCCCGKIcGyEEIIIYQQxZBgWYhy6Nq1K4qilDn/mjVrUBSFt956q/IqdQuYPXs2iqIwe/bsSt1GURS6du16zfUT18fN+n65nV5XsbGxKIrCiBEjrvu+33rrLRRFYc2aNdd1vyNGjEBRFGJjY6/rfitLVZ3Hm4kEy8IhhRfIkv4iIiKqupriFnOrBiEVHXDcah/mjrrWL7OiaHIeS3ezfrETZaOr6gqIm1utWrV45JFHikzz9va+vpURt4x7772X9u3bExwcXKn7OXToEK6urpW6D+G4tm3bcujQIfz9/au6KtdEXle3tnfffZdXX32V6tWrV3VVxHUiwbIol9q1a8s3aVHhvLy88PLyqvT91K9fv9L3IRzn6up6Uz5HN2OdRdkFBwdX+hd5cWORbhjiuin8CT0+Pp7hw4fj7++Pi4sL7du3L7Kv1Pnz5xk3bhx16tTBxcUFb29vGjRowNixY0lLS7PJm5+fz8cff0zLli1xc3PDw8ODzp07s3TpUrtyC3+iPnnyJB999BF169bFxcWFhg0b8tNPP1nLe+2114iIiMDZ2ZmmTZuyYsWKYo8tNzeXV199lbCwMJydnWnQoAHTp09HVdUyn5+EhAReeOEFateujcFgwN/fn0GDBrF///4ybf/YY4+hKArr1q0rMv3jjz9GURS++eYb67ro6GjuvvtuQkJCMBgMVKtWjc6dO/P111+Xud5XW7JkCYqi8NFHH9ms//TTT1EUhRo1atisz83NxdnZmW7dulnXXd3/uPAnToC1a9fadPcpqo/yypUr6dChA66urvj5+TF8+HCSkpLs8hXVraPw9RETE8Nnn31G/fr1MRgMhIeHM2XKFCwWyzWdj0WLFhEVFUVgYCDOzs6EhIRw1113sWjRIuuxRkZGAjBnzhybYyt8X8TFxTF58mTat29PYGAgBoOBiIgInnrqKRISEmz2FxERwZw5cwCIjIy0lnX1ccbExDB69GjCwsIwGAwEBwczYsQITp06dU3Hl5GRweTJk2nUqJH1fdqrVy82bNhgl7fw5/yyvl+K+2n72LFjjBw5ksjISAwGA76+vjRr1oznn3/eWoaiKKxdu9b6uPDvyq4uM2fOZMCAAdb3ua+vL7169SI6OrrIYy3tuSxU1PlOS0vjzTffpGHDhri7u+Pp6Unt2rUZPnz4NZ3zWbNm0a5dO9zd3XF3d6ddu3ZFvgeuPHc7duygR48eeHh44OXlxb333lvmLjplOY+Fjh8/zr333ouPjw9ubm7cdddd7N27t8hyy3u9K83vv/9Ot27d8PLywsXFhWbNmvHxxx9jMpmKzD9jxgwaNWqEs7MzoaGhvPzyy+Tm5pZ4jSg8h2+99Zb1+jVlyhSb81TaebZYLHz77be0bdsWX19fXFxcqFGjBv369Svyc3HdunUMHDiQatWqYTAYCA0N5b777rN5v13L9aI0+/btY+jQoQQHB+Pk5ER4eDjPPvtskdfTW5m0LIvrKjU1lU6dOuHl5cWwYcNISEhgwYIF9OrVi507d9K4cWMAsrOz6dixI7GxsfTs2ZN7772X/Px8YmJimDt3LhMmTLC2PObl5dG7d2/WrFlD8+bNeeyxxzAajSxbtowBAwYwffp0nnnmGbu6vPjii2zdupV+/fqh1Wr56aefeOihh/Dx8WH69OkcPHiQvn37kpuby48//siAAQM4dOgQtWrVsitr8ODB7N69m0GDBgEFH6rPPfccsbGxTJs2rdTzcuLECbp27crZs2fp2bMnAwcOJCEhgUWLFvHXX3/xzz//0K5duxLLGDZsGDNnzmTevHl06dLFLn3u3LkYDAYeeOABAJYtW0a/fv3w9vZmwIABBAcHk5iYyN69e5k7dy6PP/54qfUuSpcuXdBoNERHRzNhwgTr+sIA5Ny5cxw7dow6deoAsHnzZvLy8myC5atFREQwefJkpkyZQnh4uM0HdfPmzW3yLl261HpsHTp0YN26dXz//fecOHGiyACuOC+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///qsC6v33329dd99996mA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", + "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": "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": "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", + "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, + "id": "19010b77-023f-4719-876e-efcf15800178", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfb3ef8-fcad-484c-be76-f3145fc718c1", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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/pyproject.toml b/pyproject.toml index 47cb0586..26947332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,12 +76,16 @@ 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" 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-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-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 2bd51b9c..b95e3836 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 @@ -6,101 +18,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: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: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: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/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 -./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: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: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: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: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/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_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 ',' +./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: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 ',' @@ -348,150 +294,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: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: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: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: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: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: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: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: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/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: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: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 @@ -525,14 +488,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 @@ -566,12 +521,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 @@ -595,9 +548,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) @@ -766,9 +719,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 @@ -1479,14 +1429,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 @@ -3621,8 +3571,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: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 +3679,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: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) @@ -3755,7 +3705,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 @@ -3841,13 +3791,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 @@ -4066,107 +4014,7 @@ ./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 -./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_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/cat_response/test_cat_assess.py:7:1: F401 'scipy.stats.norm' 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 @@ -4204,20 +4052,21 @@ ./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 ',' -./tests/tfscreen/analysis/test_extract_epistasis.py:229:55: 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 @@ -4473,47 +4322,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: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: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: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 @@ -4585,32 +4433,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 @@ -4671,7 +4515,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 @@ -7624,7 +7467,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 @@ -7643,52 +7487,53 @@ ./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) -15 E111 indentation is not a multiple of 4 +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) -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 -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 '[' +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 -1110 E231 missing whitespace after ',' +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 -216 E261 at least two spaces before inline comment -1 E262 inline comment should start with '# ' +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 1 E301 expected 1 blank line, found 0 -780 E302 expected 2 blank lines, found 1 -50 E303 too many blank lines (2) +767 E302 expected 2 blank lines, found 1 +48 E303 too many blank lines (2) 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 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 +2 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 +237 F401 'pandas as pd' 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 -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 -7645 +40 F841 local variable 'e' is assigned to but never used +587 W291 trailing whitespace +81 W292 no newline at end of file +1498 W293 blank line contains whitespace +28 W391 blank line at end of file +7489 diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml index ed025d18..40f5e7bd 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 ff3c18a2..4509340b 100644 --- a/src/tfscreen/__version__.py +++ b/src/tfscreen/__version__.py @@ -5,5 +5,5 @@ Version string. """ -VERSION = (0, 4, 1) +VERSION = (0, 4, 3) __version__ = '.'.join(map(str, VERSION)) 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 00000000..61a8f526 --- /dev/null +++ b/src/tfscreen/analysis/cat_response/cat_assess.py @@ -0,0 +1,391 @@ +""" +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 + ``[-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 +from scipy.stats import chi2, norm + +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, y_obs, y_std, + alpha=0.05): + """ + 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 + ---------- + 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 (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 + 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. + + Returns + ------- + 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`` 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), + ``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) + + # 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_model = np.asarray(y_model, dtype=float) + y_model_std = np.asarray(y_model_std, dtype=float) + + # 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_obs / y_std + sig_nonzero = np.isfinite(z) & (np.abs(z) > z_crit) + + per_point = { + "x": x, + "y_model": y_model, + "y_model_std": y_model_std, + "z": z, + "sig_nonzero": sig_nonzero, + } + + # 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) + + 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, + "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 _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. + + 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 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. + + 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_rope(pred_std, rope_multiplier=2.0): + """ + Auto region-of-practical-equivalence (ROPE) half-width from the error bars. + + ``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 -- 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 + 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(rope_multiplier * np.median(finite)) + + +def classify_equiv(y_est, y_std, rope_cutoff, alpha=0.05): + """ + 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 <= 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. + rope_cutoff : float + ROPE half-width (see :func:`compute_rope`). + 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(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 <= rope_cutoff) + 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 1f56f33c..407cfd8f 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, SHAPE_MODELS from tfscreen.mle import ( run_least_squares, @@ -7,26 +7,250 @@ ) from tfscreen.util.numerical import xfill +from .cat_assess import ( + assess_best_model, residual_runs_p, residual_autocorr, goodness_of_fit_p, +) import numpy as np import pandas as pd -def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): +# 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": "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.""" + if not np.isfinite(runs_p): + return "unassessable" + return "adequate" if runs_p >= adequacy_alpha else "misfit" + + +def select_by_adequacy(models, adequacy_alpha): + """ + 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). Non-empty. + adequacy_alpha : float + Runs-test threshold. ``runs_p`` below this flags systematic residuals. + + Returns + ------- + dict + The selected model's record (an element of ``models``). + """ + 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"] + +# 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 +# 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"] + +# Non-float dtypes for the empty-frame path. +_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. +_ROLLUP_KEYS = ["nonzero_chi2", "nonzero_df", "nonzero_p", + "omnibus_W", "omnibus_df", "omnibus_p", "n_nonzero", + "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["best_model_autocorr"] = np.nan + flat_output["best_model_autocorr_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 + }) + + +def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, best_only=True, + 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 of all results, suitable for aggregation. + Model selection (``select_by``): + + - ``"aicc"`` (default): ``best_model`` is the lowest-AICc model (the small- + 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, 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 ---------- 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. + 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 + 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 + (``|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 + 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. @@ -34,21 +258,31 @@ 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 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 + ``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 + 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`` (= y_obs/y_std), and + ``sig_nonzero``. Empty if no model could be fit. (``fittable`` is + added downstream in ``cat_response``.) """ - if models_to_run is None: - models_to_run = list(MODEL_LIBRARY.keys()) + if select_by not in ("aicc", "adequacy", "shape"): + raise ValueError( + "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 = {} @@ -59,40 +293,39 @@ def cat_fit(x, y, y_std, x_pred=None, models_to_run=None, verbose=False): 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) - 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 + _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 + 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 - # 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_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() # Iterate through models and fit n = len(y) @@ -105,107 +338,233 @@ 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}") + # 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. + # 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)) + + # 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) + 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 + + # 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, "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, + "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, "k": k, "R2": np.nan, + "chi2": np.nan, "AIC": np.nan, + "AICc": np.nan, "gof_p": np.nan, + "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), "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 + 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']): 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: - best_model_row = summary_df.iloc[0] - flat_output['best_model'] = best_model_row['model'] - flat_output['best_model_R2'] = best_model_row['R2'] - flat_output['best_model_AIC_weight'] = best_model_row['AIC_weight'] + 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: + # 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', '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"] + + best_row = summary_df.set_index('model').loc[best_model] + flat_output['best_model'] = best_model + 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['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) else: - 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}) + best_model = None + _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. + 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_model": y_pred, + "y_model_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_model": pd.Series(dtype=float), + "y_model_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 + 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, + y_obs_a, y_std_a, alpha=alpha + ) + assess_df = pd.DataFrame({c: per_point[c] for c in _PER_POINT_COLS}) + 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] + + 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 c345ee72..032c100b 100644 --- a/src/tfscreen/analysis/cat_response/cat_response.py +++ b/src/tfscreen/analysis/cat_response/cat_response.py @@ -1,141 +1,350 @@ +""" +Generic per-group categorical-response engine. -from .cat_fit import cat_fit -from tfscreen.mle.curve_models import MODEL_LIBRARY +Fits a family of candidate models to a ``y_obs`` vs ``x_obs`` curve independently +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. -from tfscreen.util.numerical import xfill +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 the ``fittable`` +bool (distinguishable from zero) plus ``all_equiv_zero``. +""" + +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_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 + +# 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, best_only, alpha, + 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, + models_to_run=models_to_run, + best_only=best_only, + 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 + pred_df[col] = val + assess_df[col] = val + return flat_out, pred_df, assess_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, + best_only=True, + alpha=0.05, + select_by="shape", + adequacy_alpha=0.05, + curvy_cutoff=0.1, + rope_cutoff=None, + rope_multiplier=2.0, + num_workers=1, + progress=True, 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 AICc + weight. Fits are embarrassingly parallel across groups. A post-hoc pass then + grades each best curve against zero. 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), 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. + alpha : float, optional + 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. + 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. + 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. + progress : bool, optional + If True (default), show a tqdm progress bar over the per-group model + fits. 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. Group-key columns, then the flat cat_fit output + (``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 + (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``, 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 + diagnostic on the selected model's residuals (adequate / misfit / + 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``, + ``is_best_model``. Restricted to the best model per group unless + ``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``, ``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``. + rope_cutoff : float + The ROPE half-width actually used (resolved from ``rope_cutoff`` / + ``rope_multiplier`` if not supplied). """ - 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 + # 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: + 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 + + # A shared prediction grid spanning all observed x-values, so every group's + # 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 = [] + 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, + best_only, alpha, select_by, adequacy_alpha, + curvy_cutoff, verbose)) + + n_total = len(work_items) + if n_total == 0: + empty = pd.DataFrame(columns=group_cols) + 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)) + + if verbose: + print(f"Fitting {n_total} group(s) with {workers} worker(s)...", + flush=True) + + results = [] + 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) + + 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] + + assessment_df = pd.concat(assess_list, ignore_index=True) + + # --- 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_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): + equiv = classify_equiv( + assessment_df["y_obs"].to_numpy(dtype=float), + assessment_df["y_std"].to_numpy(dtype=float), + resolved_rope, alpha=alpha, + ) + 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" + ) + else: + results_df["all_equiv_zero"] = pd.Series(dtype=bool) + + # 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)) ) + results_df["omnibus_q"] = benjamini_hochberg( + results_df.get("omnibus_p", pd.Series(np.nan, index=results_df.index)) + ) + + # 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 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 + ["fittable"]], + on=group_cols, how="left", + ) + else: + 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 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] - # 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 + return results_df, predictions_df, assessment_df, resolved_rope + + +def _fittable(results_df, alpha): + """ + 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() + return np.isfinite(q) & (q < alpha) 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 236f58fb..00000000 --- a/src/tfscreen/analysis/cat_response/scripts/cat_response_cli.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -CLI for fitting cat_response models to theta-vs-titrant data per genotype. -""" - -import pandas as pd -from concurrent.futures import ProcessPoolExecutor, as_completed - -from tfscreen.analysis.cat_response.cat_fit import cat_fit -from tfscreen.mle.curve_models import MODEL_LIBRARY -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 - 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 cat_response(theta_file, - out_prefix="tfs_cat_response", - theta_col=None, - sigma_col=None, - models=None, - 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. - workers : int, optional - Number of parallel worker processes (default 1). - """ - 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) - results = [None] * n_total - idx_map = {id(item): i for i, item in enumerate(work_items)} - - 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) - - 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={"sigma_col": str, - "models": str}, - manual_arg_nargs={"models": "+"}) - - -if __name__ == "__main__": - main() diff --git a/src/tfscreen/analysis/compare_feature.py b/src/tfscreen/analysis/compare_feature.py new file mode 100644 index 00000000..d6c2086e --- /dev/null +++ b/src/tfscreen/analysis/compare_feature.py @@ -0,0 +1,743 @@ +""" +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 + 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 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 + 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-feature`` 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 ``value`` and ``sigma``. + + ``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 + ---------- + 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 + ['value', '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["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 + + +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_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 feature stability across N independent estimate runs. + + 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 + ---------- + 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. + + 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 + 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={"value": "value_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["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)["value"].transform("mean") + long["_dev"] = long["value"] - 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 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 "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["value"].std(ddof=1) + else: + spread = (grp["value"].max() - grp["value"].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 = "value_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_feature`` result. + + ``low_coverage`` genotypes are excluded (they were never graded). + + Parameters + ---------- + result : pandas.DataFrame + Output of :func:`compare_feature`. + 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, + } + + +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 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_feature(estimate_dfs, *, progress_every=200_000): + """ + 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 + (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_feature 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/extract_epistasis.py b/src/tfscreen/analysis/extract_epistasis.py index 616b2d1e..745f956d 100644 --- a/src/tfscreen/analysis/extract_epistasis.py +++ b/src/tfscreen/analysis/extract_epistasis.py @@ -3,12 +3,13 @@ import pandas as pd import numpy as np +import warnings from typing import List, Optional 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 +29,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 +75,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: @@ -126,13 +127,88 @@ 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, 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 + scale_constant: float = 1.0, + keep_extra: bool = False, + logit_eps: float = 1e-9, ) -> pd.DataFrame: """ Calculate epistasis between pairs of mutations for a given observable. @@ -154,18 +230,42 @@ 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. + 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. + 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 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 ------- @@ -177,14 +277,25 @@ 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 - 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) - + + # 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: @@ -193,16 +304,23 @@ 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 + # attempting to select/compute on columns that do not exist. + if cycles.empty: + return cycles + # Drop extra columns 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"]]) @@ -221,16 +339,32 @@ def extract_epistasis( std_01 = cycles[f"01_{y_std}"] std_11 = cycles[f"11_{y_std}"] - # Additive scale - if scale == "add": - 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) - - # Multiplicative scale - else: - ep_obs = (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: 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: 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) @@ -240,4 +374,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/cat_response/scripts/__init__.py b/src/tfscreen/analysis/scripts/__init__.py similarity index 100% rename from src/tfscreen/analysis/cat_response/scripts/__init__.py rename to src/tfscreen/analysis/scripts/__init__.py 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 00000000..2b7ecb09 --- /dev/null +++ b/src/tfscreen/analysis/scripts/cat_response_cli.py @@ -0,0 +1,240 @@ +""" +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, DEFAULT_MODELS +from tfscreen.util import resolve_obs_columns +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=None, + out_prefix="tfs_cat_response", + y_std=None, + group_by=None, + models=None, + alpha=0.05, + select_by="shape", + adequacy_alpha=0.05, + curvy_cutoff=0.1, + rope_cutoff=None, + rope_multiplier=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 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 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, 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 + when --write_all_predictions). + - {out_prefix}_assessment.csv per (group, x) best-model assessment. + + 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 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 + 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. 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 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. + 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. + 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. + 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(DEFAULT_MODELS) + + 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 []) + + # 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] + + ([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, assessment_df, resolved_rope = _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, + select_by=select_by, + adequacy_alpha=adequacy_alpha, + curvy_cutoff=curvy_cutoff, + rope_cutoff=rope_cutoff, + rope_multiplier=rope_multiplier, + num_workers=num_workers, + ) + + print(f" Using ROPE half-width rope_cutoff = {resolved_rope:.6g}", + flush=True) + + # 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) + + # 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, + manual_arg_types={"y_obs": str, + "y_std": str, + "group_by": str, + "models": str, + "rope_cutoff": float}, + manual_arg_nargs={"group_by": "+", + "models": "+"}) + + +if __name__ == "__main__": + main() diff --git a/src/tfscreen/analysis/scripts/compare_feature_cli.py b/src/tfscreen/analysis/scripts/compare_feature_cli.py new file mode 100644 index 00000000..7a95def0 --- /dev/null +++ b/src/tfscreen/analysis/scripts/compare_feature_cli.py @@ -0,0 +1,135 @@ +""" +CLI for grading per-genotype stability of any quantile-summarized feature across +N estimate runs. +""" + +import pandas as pd + +from tfscreen.analysis.compare_feature import ( + compare_feature as _compare_feature, + aggregate_feature, + 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_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 feature stability across N independent estimate runs. + + 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). 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 + ---------- + 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_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 + 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. + 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 + 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: + 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_feature( + estimate_dfs, + reference_df=reference_df, + min_coverage=min_coverage, + sd_tier_edges=tuple(sd_tier_edges), + 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) + + if write_aggregate: + 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}", + flush=True) + + +def main(): + generalized_main(compare_feature, + manual_arg_types={"reference": str, + "sd_tier_edges": float}, + manual_arg_nargs={"sd_tier_edges": 3}) + + +if __name__ == "__main__": + main() 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 00000000..74fdd693 --- /dev/null +++ b/src/tfscreen/analysis/scripts/extract_epistasis_cli.py @@ -0,0 +1,205 @@ +""" +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 import resolve_obs_columns +from tfscreen.util.cli import generalized_main + + +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 ``--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 + would resolve it. + + Parameters + ---------- + df : pandas.DataFrame + The raw input table (with a 'genotype' column). + 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 + 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(group_by) if group_by 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: --group_by {' '.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=None, + out_prefix="tfs_epistasis", + y_std=None, + group_by=None, + scale="add", + scale_constant=1.0, + keep_extra=False, + logit_eps=1e-9): + """ + 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 + group_by. + y_obs : str or None, optional + Name of the column holding the observable for which epistasis is + 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. 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, + the whole table is treated as a single condition. + scale : {"add", "mult", "logit"}, optional + 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 + 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) + + # 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: + required.append(y_std) + if group_by is not None: + required.extend(group_by) + + 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, + group_by=group_by, + scale=scale, + scale_constant=scale_constant, + keep_extra=keep_extra, + logit_eps=logit_eps) + + out_file = f"{out_prefix}.csv" + 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, group_by, 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) + + +def main(): + generalized_main(extract_epistasis, + manual_arg_types={"y_obs": str, + "y_std": str, + "group_by": str, + "scale": str}, + manual_arg_nargs={"group_by": "+"}) + + +if __name__ == "__main__": + main() diff --git a/src/tfscreen/mle/curve_models/__init__.py b/src/tfscreen/mle/curve_models/__init__.py index 9e226857..261beca3 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, @@ -110,11 +138,52 @@ "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])}, } + +# 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", +] + +# 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/src/tfscreen/mle/curve_models/guesses.py b/src/tfscreen/mle/curve_models/guesses.py index ae73a14d..a84b56e1 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. @@ -38,8 +40,37 @@ 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) + 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. """ - X = np.vander(x,2)[:,::-1] + z = _to_log10_x(x) + X = np.vander(z, 2) return X def guess_repressor(x, y): @@ -193,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 4546d6e4..f0bb5d3b 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 c817087c..e0897126 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/mle/predict_with_error.py b/src/tfscreen/mle/predict_with_error.py index 55605ab9..f2b505ad 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/src/tfscreen/plot/cat_fits.py b/src/tfscreen/plot/cat_fits.py index 83bc3291..bae05212 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/plot/heatmap/epistasis_heatmap.py b/src/tfscreen/plot/heatmap/epistasis_heatmap.py index 00ff42e4..9b7a912f 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/src/tfscreen/simulate/empirical/congression.py b/src/tfscreen/simulate/empirical/congression.py index 031657db..c9b4b557 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 fa9fb9db..3ecb3cce 100644 --- a/src/tfscreen/simulate/empirical/fit_phenotypes.py +++ b/src/tfscreen/simulate/empirical/fit_phenotypes.py @@ -1,561 +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) - std_t = np.sqrt(np.diag(gf.cov_t))[gf.pheno_slice] - 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/empirical/population.py b/src/tfscreen/simulate/empirical/population.py index 76b87afa..6baa8937 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/src/tfscreen/simulate/scripts/build_empirical_cli.py b/src/tfscreen/simulate/scripts/build_empirical_cli.py index a3ea0183..7dafa384 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/simulate/toy_thermo/__init__.py b/src/tfscreen/simulate/toy_thermo/__init__.py new file mode 100644 index 00000000..c2fab560 --- /dev/null +++ b/src/tfscreen/simulate/toy_thermo/__init__.py @@ -0,0 +1,37 @@ +""" +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 +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 00000000..e2bdce90 --- /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/src/tfscreen/simulate/toy_thermo/core.py b/src/tfscreen/simulate/toy_thermo/core.py new file mode 100644 index 00000000..0c67257d --- /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 00000000..10e47686 --- /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 00000000..bd92c365 --- /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/src/tfscreen/tfmodel/analysis/batch_sizing.py b/src/tfscreen/tfmodel/analysis/batch_sizing.py index b070f3d6..b5c1c61a 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 diff --git a/src/tfscreen/tfmodel/analysis/extraction.py b/src/tfscreen/tfmodel/analysis/extraction.py index 4f20aa83..9bb94281 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/genotype_fit/__init__.py b/src/tfscreen/tfmodel/genotype_fit/__init__.py new file mode 100644 index 00000000..a6f3a1e6 --- /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 00000000..e7fa640c --- /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 00000000..c2c85de8 --- /dev/null +++ b/src/tfscreen/tfmodel/genotype_fit/fit.py @@ -0,0 +1,634 @@ +""" +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 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 +# 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 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 00000000..e94aea6a --- /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/src/tfscreen/tfmodel/scripts/predict_epistasis_cli.py b/src/tfscreen/tfmodel/scripts/predict_epistasis_cli.py new file mode 100644 index 00000000..9c514d3b --- /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/src/tfscreen/tfmodel/scripts/predict_growth_cli.py b/src/tfscreen/tfmodel/scripts/predict_growth_cli.py index ebd44982..556abf2a 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 @@ -7,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", @@ -16,7 +71,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 +142,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 +207,44 @@ 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) + q_to_get = [0.5] if is_map else None print("Running growth predictions...", flush=True) @@ -151,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) @@ -197,7 +309,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/src/tfscreen/util/__init__.py b/src/tfscreen/util/__init__.py index e33eba31..575b23eb 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 ) @@ -68,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 edec5aa9..79fb50d7 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 00000000..f7267bab --- /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/src/tfscreen/util/numerical/xfill.py b/src/tfscreen/util/numerical/xfill.py index 0e7427ce..c7ca74cd 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/src/tfscreen/util/parallel.py b/src/tfscreen/util/parallel.py new file mode 100644 index 00000000..8fe0951c --- /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 deleted file mode 100644 index d47051a8..00000000 --- a/tests/tfscreen/analysis/cat_response/scripts/test_cat_response_cli.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -Tests for cat_response_cli.py — theta_col auto-detection and error handling. -""" -import pytest -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(), -) - -_FLAT_RESULT = {"genotype": "wt", "titrant_name": "IPTG", - "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: - - def test_autodetects_q0_5(self, tmp_path): - """Values from q0.5 column are passed to the fitter.""" - 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") - 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")) - - 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")) - - 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) - - 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")) - - 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")) - - assert captured["y"] == pytest.approx([0.3, 0.7]) - - 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")) 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 00000000..641572b5 --- /dev/null +++ b/tests/tfscreen/analysis/cat_response/test_cat_assess.py @@ -0,0 +1,276 @@ +""" +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_rope, + classify_equiv, + benjamini_hochberg, + residual_runs_p, + 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: + 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: + 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 + + +# --- 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): + # 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]) + 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 rollup["n_nonzero"] == 3 + assert rollup["any_nonzero"] is True + 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, + 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 + + +# --- equivalence ------------------------------------------------------------- + +class TestClassifyEquiv: + + def test_ci_inside_region_is_equiv(self): + # |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]), 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]), rope_cutoff=0.5) + assert not equiv[0] + + def test_nan_std_and_bad_rope(self): + assert not classify_equiv(np.array([0.0]), np.array([np.nan]), + rope_cutoff=0.5)[0] + assert not classify_equiv(np.array([0.0]), np.array([0.01]), + rope_cutoff=np.nan)[0] + + def test_boundary_uses_alpha(self): + # Larger alpha -> narrower CI -> easier to be equivalent. + 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, r, alpha=0.05)[0] + + +# --- compute_rope ------------------------------------------------------------ + +class TestComputeRope: + + 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_rope(stds, rope_multiplier=2.0) == pytest.approx(0.4) + + def test_all_nan_returns_nan(self): + assert np.isnan(compute_rope([np.nan, np.nan])) + + def test_empty_returns_nan(self): + assert np.isnan(compute_rope([])) + + +# --- 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 ca06cae3..324cd4bc 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_fit.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_fit.py @@ -1,252 +1,471 @@ - -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 - -from tfscreen.analysis.cat_response.cat_fit import cat_fit - -# 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 +import pytest + +from tfscreen.analysis.cat_response.cat_fit import ( + cat_fit, select_by_adequacy, select_by_shape, _shape_status, +) +from tfscreen.mle.curve_models import MODEL_LIBRARY, DEFAULT_MODELS, SHAPE_MODELS + + +# --- 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_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)["model"] == "bell" + + 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)["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" + + +class TestShapeStatus: + def test_adequate(self): + assert _shape_status(0.5, 0.05) == "adequate" + + def test_misfit(self): + assert _shape_status(0.01, 0.05) == "misfit" + + 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]) + +# 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"]) + # 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", + "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 "step", adequate. + assert flat_output["best_model"] == "hill_inducer" + assert flat_output["shape"] == "step" + assert flat_output["shape_status"] == "adequate" + + +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_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, select_by="aicc") + assert flat_output["best_model"] != "flat" + 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_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"): + cat_fit(x, y, ys, models_to_run=["flat"], select_by="bogus") + + +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) == ["model", "x", "y_obs", "y_std", + "y_model", "y_model_std", "z", + "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. + 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 + 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_model", "y_model_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 + + +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) # 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) + assert "linear_log" in fit_models + assert "biphasic_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/analysis/cat_response/test_cat_response.py b/tests/tfscreen/analysis/cat_response/test_cat_response.py index 79fe6042..5ab6f610 100644 --- a/tests/tfscreen/analysis/cat_response/test_cat_response.py +++ b/tests/tfscreen/analysis/cat_response/test_cat_response.py @@ -1,119 +1,405 @@ - -import pytest +""" +Tests for the generic cat_response core: grouping, column selection, validation, +prediction tagging, the post-hoc assessment pass, 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 +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", **extra): + """A minimal flat cat_fit result dict with the assessment rollups.""" + d = {"status": "success", "best_model": best, + "nonzero_p": 0.5, "omnibus_p": 0.5, + "n_nonzero": 0, "any_nonzero": False} + d.update(extra) + return d + + +def _pred(n=2): + """A minimal cat_fit prediction frame.""" + return pd.DataFrame({ + "model": ["flat"] * n, + "x": np.arange(n, dtype=float), + "y_model": np.zeros(n), + "y_model_std": np.zeros(n), + "is_best_model": [True] * n, }) - - # 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 + + +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_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), }) - - 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, **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="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, + "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 + + +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) + + 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) + + 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) + + 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 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"] == "shape" 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 -------------------------------------------------------------- + +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_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() + + +# --- post-hoc assessment pass ------------------------------------------------ + +class TestAssessmentPass: + + 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 "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_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([]) + + 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", rope_cutoff=0.5) + # not distinguishable from zero -> fittable False; model name preserved. + assert set(assess["fittable"]) == {False} + assert set(assess["model"]) == {"flat"} + cols = list(assess.columns) + 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. + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([])): + _, _, _, rope = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", rope_multiplier=2.0) + assert rope == pytest.approx(0.2) + + def test_explicit_rope_cutoff_is_used(self): + with patch.object(cat_response_mod, "cat_fit", + side_effect=_capturing_fit([])): + _, _, _, rope = cat_response( + _basic_df(), x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", rope_cutoff=0.75) + assert rope == 0.75 + + 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): + flat, pred, _ = fit(*a, **k) + x = a[0] + 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", + y_std="theta_std", rope_cutoff=0.5) + assert set(results["fittable"]) == {False} + assert set(results["all_equiv_zero"]) == {True} + + 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): + flat, pred, _ = fit(*a, **k) + 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) + 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", rope_cutoff=0.5) + assert set(results["fittable"]) == {True} + + 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): + 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", rope_cutoff=0.5) + assert set(results["fittable"]) == {False} + assert set(results["all_equiv_zero"]) == {False} + + 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["fittable"]) == {True} + assert (results["nonzero_q"] < 0.05).all() + + +# --- 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, serial_assess, serial_delta = cat_response( + df, x_obs="titrant_conc", y_obs="theta", + y_std="theta_std", num_workers=1) + 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 new file mode 100644 index 00000000..047b1816 --- /dev/null +++ b/tests/tfscreen/analysis/scripts/test_cat_response_cli.py @@ -0,0 +1,217 @@ +""" +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_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 {"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", + "fittable"}.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: + + 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": []}), \ + 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"): + 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_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 = {} + + def fake_core(df, **kwargs): + 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_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", + "--y_obs", "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 new file mode 100644 index 00000000..83ad7c71 --- /dev/null +++ b/tests/tfscreen/analysis/scripts/test_extract_epistasis_cli.py @@ -0,0 +1,264 @@ +""" +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_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: + 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", + group_by=["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_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", group_by=["nope"], + 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): + """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): + # 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 + + def test_forgot_group_by_hints_column(self, tmp_path, capsys): + # One row per genotype *per condition* (like titrant_conc), run without + # --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(): + 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 "--group_by titrant_conc" in captured + # With the suggested selector it succeeds. + extract_epistasis(data, y_obs="y", + group_by=["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 "--group_by" not 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_obs", "y", + "--out_prefix", out_prefix, + "--y_std", "y_err", + "--group_by", "condition", + "--scale", "add", + "--scale_constant", "-2.0", + "--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 + # --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_compare_feature.py b/tests/tfscreen/analysis/test_compare_feature.py new file mode 100644 index 00000000..2fb19c85 --- /dev/null +++ b/tests/tfscreen/analysis/test_compare_feature.py @@ -0,0 +1,491 @@ +""" +Unit tests for tfscreen.analysis.compare_feature. +""" + +import numpy as np +import pandas as pd +import pytest + +from tfscreen.analysis.compare_feature import ( + compare_feature, + aggregate_feature, + 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 ----------------------------------------------------- + +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["value"].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_feature([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_feature([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_feature(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_feature([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_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 + 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_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) + 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_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"]) + 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_feature([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_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) + 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_feature([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_feature([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_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_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_feature([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_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 + 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_feature([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_feature([r1, r2]) + assert list(result["rms_sd"]) == sorted(result["rms_sd"]) + + +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)] + 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_feature([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_feature([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_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_feature([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_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_feature([r1, r2]) diff --git a/tests/tfscreen/analysis/test_extract_epistasis.py b/tests/tfscreen/analysis/test_extract_epistasis.py index 36fe8b58..56e38b0c 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,14 +216,196 @@ 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") - assert "extra_col" not in result_false.columns \ No newline at end of file + 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): + """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 + + +# --- 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) + + +# --- 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/mle/curve_models/test_guesses.py b/tests/tfscreen/mle/curve_models/test_guesses.py index 70b3d434..e9e31054 100644 --- a/tests/tfscreen/mle/curve_models/test_guesses.py +++ b/tests/tfscreen/mle/curve_models/test_guesses.py @@ -3,10 +3,16 @@ import numpy as np from tfscreen.mle.curve_models import guesses +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), - (guesses.guess_linear, 2), (guesses.guess_poly_2nd, 3), (guesses.guess_poly_3rd, 4), (guesses.guess_poly_4th, 5), @@ -15,11 +21,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]) @@ -80,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 5c6a9ae4..a9a59506 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 a5d35a0a..658c7a59 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/mle/test_predict_with_error.py b/tests/tfscreen/mle/test_predict_with_error.py index bb23941c..2cff8afd 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 diff --git a/tests/tfscreen/plot/heatmap/test_heatmaps.py b/tests/tfscreen/plot/heatmap/test_heatmaps.py index eb51256e..28531ecf 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') diff --git a/tests/tfscreen/plot/test_cat_fits.py b/tests/tfscreen/plot/test_cat_fits.py index 50eef03a..5139d0a3 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/simulate/empirical/test_population.py b/tests/tfscreen/simulate/empirical/test_population.py index 608ad5f8..e22c0315 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) 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 00000000..3e5f4c95 --- /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 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 00000000..65b2e9ca --- /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 00000000..9c3f6f0c --- /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 00000000..0ef750a3 --- /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 "fittable" 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 00000000..9a227799 --- /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) 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 00000000..d384184a --- /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) diff --git a/tests/tfscreen/analysis/cat_response/scripts/__init__.py b/tests/tfscreen/tfmodel/genotype_fit/__init__.py similarity index 100% rename from tests/tfscreen/analysis/cat_response/scripts/__init__.py rename to tests/tfscreen/tfmodel/genotype_fit/__init__.py 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 00000000..4ea08205 --- /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 00000000..f377d257 --- /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 00000000..c7b964ba --- /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() diff --git a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py index 64658c9e..7069c69b 100644 --- a/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py +++ b/tests/tfscreen/tfmodel/scripts/test_predict_growth_cli.py @@ -639,3 +639,322 @@ 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()) + + +# --------------------------------------------------------------------------- +# --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 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 00000000..36d9bead --- /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 diff --git a/tests/tfscreen/util/numerical/test_xfill.py b/tests/tfscreen/util/numerical/test_xfill.py index f2ed4e63..82747e3c 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) diff --git a/tests/tfscreen/util/test_parallel.py b/tests/tfscreen/util/test_parallel.py new file mode 100644 index 00000000..063106bc --- /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