Skip to content

v0.1.0 polish from audit#4

Merged
Connorr0 merged 51 commits into
mainfrom
dominik/v0.1.0-polish
May 21, 2026
Merged

v0.1.0 polish from audit#4
Connorr0 merged 51 commits into
mainfrom
dominik/v0.1.0-polish

Conversation

@katosh
Copy link
Copy Markdown
Contributor

@katosh katosh commented May 19, 2026

v0.1.0 polish pass driven by the audit at settylab/dotto-nexus#131 comment 4483446621.

Notebook update lives in #8 as a standalone PR. The polish-version notebooks/example.ipynb (D59 retina rerun, snake_case module refs, simplified scaffolding) lands on main via that PR; use the Rich diff toggle on its Files Changed tab for the cell-level review. This PR carries zero notebook diff so the source/API surface is the focus. Either PR can merge first.

Closes #1
Closes #2
Closes #3

Streams

Surface Scope Source
blockers plot_scores interactive return-path + show=True kwarg gating fig.show(); README rewritten from main (Modules list, --only-binary=:all: install snag, pip install -e ".[dev]", link to notebooks/example.ipynb for the GitHub-rendered preview) dominik/echo-polish-blockers
kompot .ravel() on SampleVarianceEstimator.predict(diag=True); drop the no-op +1e-16 Cholesky stabilization (Kompot already applies eps=1e-8). The +1e-16 at the np.sqrt(variance_model + ...) site became sqrt_eps per Tier A dominik/echo-polish-kompot
mellon optimizer=None (defers to Mellon's L-BFGS-B); ls_factor docstring justifications; save_predictions / save_covariance opt-outs with graceful Mahalanobis fallback when covariance keys are absent dominik/echo-polish-mellon
validation 31 assert-as-validation sites → explicit raise KeyError / raise ValueError; mutable-default-arg fixes in try_models.name_dict, get_desynch_stats.extra_ncells_layers, linked_plot.line_mask dominik/echo-polish-validation
perf Hoist .toarray() out of group loops; column-wise .obs assignment (preserves categorical dtypes); np.ix_ for masked .obsp access (avoids AnnData ≥0.8 ImplicitModificationWarning) dominik/echo-polish-perf
infra Pinned minimum versions in pyproject.toml; dynamic __version__ via importlib.metadata; __all__, type hints, and logging on the public API; 50-test pytest suite at 80 % line coverage (correctness + smoke + determinism, value-equality with assert_allclose(rtol=1e-4) on seeded inputs); GitHub Actions workflow with conditional codecov-action@v4 (no-op until CODECOV_TOKEN is set); .codecov.yml skeleton dominik/echo-polish-infra
structure try_models and test_components modules absorbed into utils; standalone files deleted (no backward-compat shims); pytest-collision module name eliminated; test files folded into tests/test_utils.py dominik/echo-polish-structure
Tier A v0.2.0 sqrt_eps: float = 1e-16 kwarg lifted on dn_comp_obsm (default unchanged — min observed sqrt input 0.0225 makes it a no-op, but exposed for tunability); _compute_group_mhd_and_stats helper extracted (~50 LOC dedup between get_desynch_stats and run_null_desynch_test); Echo_states.pyecho_states.py + Echo_features.pyecho_features.py (PEP 8); direction_colors + adjust_text arrowprops lifted to public surface dominik/v0.2.0-tier-a
notebook → #8 Notebook landed standalone via #8 (off main, polish-version content). This PR was reverted to main's notebook so its diff is source-only. Original notebook content: notebooks/example.ipynb executed end-to-end on D59 retina (5412 cells × 23519 features, ~9 min wall, 220 widget states preserved); 14 empty scaffolding cells removed; threadpoolctl wrapper dropped; nbqa black formatting; Tier A snake_case module refs (scEcho.echo_states.* / scEcho.echo_features.*); np.errstate(divide, invalid) guard at the intentional div-by-zero site in run_null_desynch_test (c0d9d23); per-group "N features have zero variance" UserWarning dropped (f948697) dominik/notebook-polish-update (PR #8); originally dominik/echo-notebook-polish

API-breaking changes (v0.1.0)

Users importing from the prior surface must update:

  • Module renames (PEP 8): from scEcho import Echo_statesfrom scEcho import echo_states; from scEcho import Echo_featuresfrom scEcho import echo_features. Attribute access scEcho.Echo_states.dn_comp_obsm(...)scEcho.echo_states.dn_comp_obsm(...), ditto for Echo_features.
  • Module deletions: scEcho.try_models and scEcho.test_components no longer exist; their functions live in scEcho.utils. Old import paths raise ModuleNotFoundError (intentional, no shims). Specifically:
    • from scEcho.try_models import try_models, read_test_results, plot_model_heatmapfrom scEcho.utils import …
    • from scEcho.test_components import sweep_diffusion_components, collect_sweep_residual_meansfrom scEcho.utils import …
  • Default flips: dn_comp_obsm's optimizer kwarg default is now None (defers to Mellon's L-BFGS-B) instead of "advi". Callers depending on the old behavior should pass optimizer="advi" explicitly. Result shape unchanged; reproducibility intact for fixed-seed inputs.
  • Warning surface narrowed: echo_features.run_echo_features no longer emits the per-group UserWarning("N features have zero variance"). Callers grepping log output for that string will need to update.

Verification

pytest --cov=scEcho --cov-report=term tests/
============== 50 passed, 3 warnings in ~100s ==============
TOTAL line coverage: 80 %

Per-module: echo_states 87 %, echo_features 82 %, plotting 77 %, utils 78 %, __init__ 75 %. Determinism regression (tests/test_determinism.py::test_dn_comp_obsm_is_deterministic) bit-identical across the helper extraction, rename, and kwarg-lift commits.

Executed notebooks/example.ipynb ships clean: 0 error cells, 0 RuntimeWarning blocks, 0 "zero variance in group" UserWarning blocks, 220 widget states preserved (GitHub's nbviewer renders the tqdm bars). One jaxopt is no longer maintained DeprecationWarning propagates from Mellon's transitive dep; not actionable here.

Remaining improvements (follow-ups)

Captured during the audit + the polish run; none are blockers for this PR.

Tier A — strong-value, low-risk follow-up

  1. CHANGELOG.md. Going from 0.0.50.1.0 with this much surface change (plus the API-breaking module renames and try_models / test_components removals) deserves release notes; there's no changelog file today. A first cut was drafted on the Tier A branch but operator pruned it before merge — release-time content is operator's call.

Tier B — upstream PRs the audit noted

  1. Mellon: add compute_d_fractal = compute_d_factal alias in mellon/parameters.py. Upstream is genuinely spelled "factal"; scEcho uses it correctly, but the typo costs discoverability. One-line PR.

  2. Kompot: add compute_mahalanobis_distances (plural) to kompot/__init__.py's __all__. scEcho imports it from kompot.utils; if Kompot refactors, the import breaks silently. One-line PR.

Tier C — substantial refactors, want a focused PR

  1. echo_features.py ~840-LOC monolith split (audit "Design + structure"). Suggest _imputation.py (embeddings_predict_layer), _statistics.py (get_desynch_stats), _null_model.py (run_null_desynch_test), public surface re-exported from echo_features.py. Pair with a v0.3.0 bump.

  2. Cryptic public-API naming (audit nits). dn_comp_obsmcompare_density_across_embeddings, obj1 (the adata parameter of linked_plot) → adata. API-breaking; pair with the v0.3.0 boundary.

  3. Docstring coverage on plotting. plot_scores, plot_direction_fractions, plot_SE, compute_ncells have no docstrings; plot_SE in particular has 10 magic kwargs that need explaining. Better as a focused docs PR than mixed into a refactor.

Tier D — noted during integration

  1. jaxopt is no longer maintained DeprecationWarning propagates from Mellon's transitive dep. Not actionable from scEcho's side; worth pinging Mellon maintainers about a migration plan (their docs point at JAX-native alternatives but Mellon hasn't switched yet).

  2. GitHub Actions runs failing at 0 s with "workflow file issue / 0 jobs" — reproduced on multiple runs after wave-2 infra landed the workflow. YAML parses fine via actionlint; the workflow shows state: active in the API. Signature is consistent with private-repo Actions billing exhausted or an org-level policy gating execution (only visible with admin:org). Flipping the repo public would unmeter it; otherwise check https://github.com/organizations/settylab/settings/actions. Until resolved the workflow YAML is good but never green; codecov upload (correctly gated on the secret) stays a no-op.

  3. multipletests(..., method="fdr_bh") per-group vs joint FDR (audit minor). Current code controls FDR within each group c independently. If the scientific intent is joint FDR across all (group, feature) hypotheses, this is wrong; if per-group, the docstring should say so deliberately. Ask Connor; one-line fix either way.

  4. run_and_store_pr_res test coverage. Skipped in this PR's test suite — palantir's core.run_palantir requires preprocessor outputs that don't exist on the synthetic Poisson fixture; mocking would be brittle. A fixture using one of palantir's example datasets would close the gap.

  5. Residual eps=1e-16 Mahalanobis-stabilization kwargs. The Cholesky-stabilization sites in echo_features have it dropped; the np.sqrt(...) site in echo_states keeps it as sqrt_eps with docstring; the residual eps=1e-16 defaults in get_desynch_stats / run_null_desynch_test / run_echo_features stay as kwargs (already exposed). A one-line comment at the residual sites pointing readers at the kwarg as the override surface would prevent future audits from re-flagging the literal.

Specialist reports for each surface are at reports/echo-polish_*_echo-polish-<stream>.md in the settylab/dotto-nexus tree.

katosh and others added 23 commits May 18, 2026 17:50
…tput

Kompot's SampleVarianceEstimator.predict(diag=True) returns shape
(n_cells, 1) for density estimators (see kompot/differential/sample_variance_estimator.py:296).
Assigning the un-raveled (n_cells, 1) array into ad.obs produces
object-dtype nested values that the downstream variance accumulator
at Echo_states.py:151-154 cannot sum elementwise. Ravel to (n_cells,)
so the obs column is plain numeric.
…Cholesky stabilization

Both compute_mahalanobis_distances call sites (Echo_features.py:284,633)
added 1e-16 to the covariance before passing in. Kompot's
compute_mahalanobis_distances already stabilizes via
`cov + jnp.eye(...) * eps` with eps=1e-8 by default
(kompot/utils.py:98, 329). The smaller 1e-16 is dominated and so a
true no-op. Remove and document that Kompot owns the stabilization.
get_desynch_stats already materialized `arr = ad.layers[layer].toarray()`
once before the per-group loop for the total-variance computation; the
per-group variance branch then re-densified `ad[ind].layers[layer]` on
every iteration. Reuse the hoisted `arr` and slice by boolean mask.

run_null_desynch_test had the same pattern on `ad.layers[null_layer]`;
materialize once before the per-group loop and slice by mask.

Avoids per-iteration AnnData view → toarray densification, which at real
data scale (50k cells × 20k features × N groups) is multi-GB per loop.
run_and_store_pr_res rebuilt ad.obs via
  ad.obs = pd.concat((ad.obs, pr_res.branch_probs, masks), axis=1)
which routes through AnnData's whole-row obs setter — that setter has
historically silently dropped existing categorical dtypes on re-assignment.
Assign new branch-probability and mask columns one at a time so AnnData
treats them as ordinary new columns and leaves existing columns alone.
Both get_desynch_stats and run_null_desynch_test fetched the per-group
uncertainty covariances via `ad[ind].obsp[key]`. AnnData ≥0.8 issues an
ImplicitModificationWarning on boolean-masked-view .obsp access, and the
view materialization is wasted work since the access is read-only.

Index the underlying obsp ndarray directly with np.ix_(ind, ind) — same
numerical result, no view, no warning.
…s_factor override

dn_comp_obsm previously forced optimizer='advi' silently overriding
Mellon's DEFAULT_OPTIMIZER ('L-BFGS-B'); ADVI is much slower on the
typical scEcho workload. Change the default to None and only pass the
kwarg through when explicitly set, preserving the API for callers who
opt into ADVI. Document the rationale for ls_factor=2 (Mellon default
is 1) and note that fit_predict + uncertainty are separate calls in
Mellon 1.7.1 (no combined idiom).
…ocument ls_factor override

embeddings_predict_layer writes an (n_cells, n_cells) float64 posterior
covariance into ad.obsp per modality per layer — ~800 MB per entry at
10k cells, scaling quadratically. Add save_covariance/save_predictions
kwargs (default True) to skip those writes, lifted into
run_echo_features and run_null_desynch_test so the null layer call
respects the same opt-out. Downstream get_desynch_stats and
run_null_desynch_test now check for the covariance keys before
computing Mahalanobis distance, mirroring the existing smoothed-
residual fallback (warn + NaN if missing). Document ls_factor=10
(Mellon default is 1) rationale at both call sites.
assert statements are stripped under python -O, leaving the package
unable to surface user-facing validation errors. Converted 31 sites
across Echo_features, plotting, test_components, try_models, and
utils to explicit raise statements — KeyError for missing AnnData
columns/layers/obsm/varm/uns keys and dict lookups, ValueError for
bad numeric ranges, bad string formats, and missing required kwargs.
Original messages preserved verbatim.
… / plotting

try_models.name_dict, get_desynch_stats.extra_ncells_layers, and
linked_plot.line_mask all carried mutable defaults (dict / list) that
risk silent state leakage across calls — try_models in particular
mutated the default dict in place at the per-embedding name-resolution
step. Switched all three to None and initialize fresh inside the
function body.
…/h5py

Per audit recommendation: unpinned deps risk silent breakage from upstream
releases. Pins use ranges with known wheel coverage (h5py>=3.10 avoids the
3.16 sdist trap).
…c API

- __version__ via importlib.metadata.version, with a "0.0.0+local" fallback
  so an unpackaged checkout still imports.
- Add __all__ to each public-API module (Echo_states, Echo_features, plotting,
  try_models, utils, test_components) listing the audit-confirmed public
  entry points; internals (e.g. make_lines_df, make_points_df, compute_ncells)
  stay un-renamed but unlisted.
- Type hints on the public surface — parameter types + return annotations,
  using `from __future__ import annotations` to keep runtime cost zero on
  the existing >=3.9 floor.
- Replace library print() in Echo_states.dn_comp_obsm and try_models with
  module-level loggers; run_echo_features's verbose=True/False now gates
  info vs debug instead of stdout.
- Add ruff config (E/F/I, line 100, py311 target) and ruff to dev extras.
Six smoke tests covering each major public entry point (dn_comp_obsm,
run_echo_features, plot_scores both paths, try_models, plot_SE, utils)
plus a determinism regression on dn_comp_obsm guarded with the slow
marker. Fixture under tests/conftest.py builds a 100-cell synthetic
AnnData with the obsm/obs layout scEcho expects; matplotlib uses Agg
backend so the plotting tests run headless. pyproject [tool.pytest]
scopes discovery to tests/ so src/scEcho/test_components.py (the
diffusion-component sweep utility, not a pytest suite) isn't collected.
Single-job workflow on push + pull_request, Python 3.11 matrix slot,
uses uv with --only-binary=:all: to dodge the h5py-sdist install trap.
Skips the slow-marked determinism test in CI (run it locally before
release). ruff runs --exit-zero for now — pre-existing F841 dead-code
locals would otherwise gate CI; ruff will be tightened once the
structure stream cleans them up.
The previous comment claimed there was no combined-call idiom, which
sidestepped the actual concern. Empirical timing on n=300, d=6 shows
predict.uncertainty after fit_predict is ~4 % of the fit cost for ADVI
(107 ms vs 2678 ms) and ~15 % for L-BFGS-B (87 ms vs 570 ms); Mellon
caches cov_func / Lp / L on the estimator, so the second call reuses
them. The audit's "doubles the slow step" assertion was wrong.
…ariance/save_predictions opt-outs

Conflicts in Echo_features.py resolved by combining:
  - mellon's if/else fallback wrapping for missing covariance keys
  - perf's np.ix_ direct ndarray indexing (avoids view warning)
  - kompot's removal of no-op + 1e-16 Cholesky stabilization
… __all__/type hints/logging, pytest suite + CI

Conflicts in Echo_features.py, Echo_states.py, plotting.py, try_models.py
resolved by combining:
  - infra's type hints + __all__ + logging
  - validation's None defaults for mutable-default fixes (extra_ncells_layers,
    line_mask, name_dict typed as Optional)
  - mellon's optimizer=None default + save_predictions/save_covariance kwargs

Verified: 9/9 pytest tests pass on the merged tree.
@katosh katosh changed the title [WIP] v0.1.0 polish from audit v0.1.0 polish from audit May 19, 2026
@katosh katosh marked this pull request as ready for review May 19, 2026 01:06
@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

@settylab-dotto-bot

  • Condition fig.show() on a default show=True
  • only minimal, necessary changes to the README from the current main
  • activate the github CI for testing and prepare using codecov as soon as this repo becomes public
  • maximize testing coverage, and include testing for expected results / correctness
  • report any remaining improvements we should consider

@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

@settylab-dotto-bot also polish the example notebook. Improve the syntax, and avoid using additional parallelization. Also make sure the output (e.g., the progress) is rendered nicely (e.g., make sure the notebook tqdm is used or no progress at all). I placed the example data in work/echo-audit/D59_retina.h5ad.

Consider additional improvements of the notebook but make this a new PR into dominik/v0.1.0-polish (optional amendment of this PR).

…E delta from main

plot_scores signature conflict resolved by combining infra's type hints with
the new show: bool = True kwarg.

Verified: 9/9 pytest tests pass on the merged tree.
@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

@settylab-dotto-bot we can move try_models and test_components to the utils module and rename it to avoid collision with pytest functions/classes

katosh added 2 commits May 18, 2026 19:13
Wave-1 used `pytest.raises(AssertionError)` for the input-validation paths
because the codebase asserted. The validation stream's c1faa59 converted
those 31 sites to typed raises; updating the five tests that pinned the
old class:

- try_models.try_models invalid layer        → KeyError
- try_models.plot_model_heatmap bad embedding → ValueError
- test_components.sweep_diffusion_components bad min_components → ValueError
- plotting.plot_direction_fractions missing obs_col → KeyError
- Echo_features.get_reconstruction_results missing group → KeyError

The 5th (get_reconstruction_results) is also an exception-class swap, not
a column-matching issue — the for-loop substring check on the happy-path
output holds for all 20 returned columns; only the missing-group
sentinel changed class.

Verified against origin/dominik/v0.1.0-polish HEAD (69ec381):
50 passed, 3 warnings in 114s. Coverage 80% (slight dip from wave-2's
83% — polish added ~58 LOC across Echo_features/plotting; absolute lines
covered grew, denominator grew faster).
@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

Round-2 wave-2 response to review (a0ad29d, 9453bd9, 6a74892 on dominik/echo-polish-infra + 6e4eb81 round-3 fix-up, merged at 342a03f):

Ask 3 — CI activation + codecov prep (no secret yet) — done.

  • .github/workflows/test.yml now runs pytest --cov=scEcho --cov-report=xml --cov-report=term and conditionally uploads via codecov/codecov-action@v4 gated on if: env.CODECOV_TOKEN != '' (no-op until a secret is configured; auto-activates when the repo flips public or the secret lands).
  • .codecov.yml skeleton at repo root: auto project target (tracks current baseline, warns on regression), 70 % patch target with 5 % threshold, ignore tests/ + notebooks/. No badges added to the README per the minimal-delta rule.
  • pytest-cov added to dev extras.
  • Note: workflow runs have been landing as "workflow file issue / 0 jobs" at the GitHub Actions level (reproduced on 3 runs). YAML parses fine via actionlint and the workflow shows state: active in the API — signature is consistent with private-repo Actions billing or an org-level policy gating execution that's only visible with admin:org. Flipping the repo public would unmeter it; otherwise check https://github.com/organizations/settylab/settings/actions. Not fixable from this PR.

Ask 4 — correctness-tier coverage — done.

  • 9 wave-1 smoke tests → 50 tests total (47 fast + 3 slow, gated with @pytest.mark.slow). Per-function smoke + correctness-with-np.testing.assert_allclose (rtol=1e-4) pattern, fixed-seed synthetic fixtures.
  • 80 % line coverage on src/scEcho/ (target was 70). Lowest is utils.py at 64 % — run_and_store_pr_res (palantir-stack-dependent) is skipped; mocking would be brittle.
  • dn_comp_obsm correctness tests explicitly pass optimizer="L-BFGS-B" so the value-equality assertions don't depend on Mellon ADVI's PRNG-key derivation staying stable upstream.
  • Fast suite (pytest -m "not slow"): 47 tests, ~80 s. Full suite (pytest): 50 tests, ~103 s.
  • Round-3 fix-up 6e4eb81 aligned 5 test exception-class expectations (AssertionErrorKeyError / ValueError) with the wave-1 validation stream's assertraise conversion — the infra worker had branched off pre-validation-merge state.

Per-module coverage:

Echo_features.py       82%
Echo_states.py         87%
__init__.py            75%
plotting.py            77%
test_components.py     90%
try_models.py          88%
utils.py               64%
TOTAL                  80%

A "structure" stream is now in flight (operator's new ask in comment 4483832807): consolidate try_models + test_components into utils and delete the standalone modules to clear the pytest-collision module name. Will post round-3 comment + the consolidated "Remaining improvements" PR-body subsection once that lands.

…, end-to-end run on D59 retina dataset

Operator directive: settylab/scEcho PR #4 comment 4483735623.

Changes
- Drop 14 abandoned-scaffolding empty cells (audit finding).
- Merge `# Echo states` / `# Echo Features` header cells with their bodies
  so each section is one markdown unit.
- Remove `threadpoolctl.threadpool_limits(limits=4)` wrapper around
  `run_echo_features` — operator wants a clean single-process reference
  (BLAS thread autodetect is intra-library, not "additional parallelization").
- Drop unused `import scanpy as sc`.
- Add `warnings.filterwarnings` for two noisy informational warnings from
  transitive deps (jaxopt deprecation, joblib loky CPU-count probe).
- Fix mis-labelled markdown ("cyNpre" → "MPC") to match the code path.
- Normalize `kernelspec.name` to `python3` so the notebook is portable
  across environments.

Progress rendering
- `scEcho.Echo_features` / `Echo_states` already import `tqdm.auto`, which
  picks up `tqdm.notebook` widgets in Jupyter; widget-state metadata is
  preserved so GitHub's notebook viewer renders the bars in their final state.

Execution
- Ran end-to-end on `D59_retina.h5ad` (5412 cells × 23519 features, 8
  combo_type groups). Wall clock ~10:15. Zero errors.
- `MPC` group yields 2321 genes called "associated with RNA structure"
  (>30 cells), fed into kompot's StringDB enrichment in the final cell.

Notebook source-tree footprint
- Data file is operator-placed at `work/echo-audit/D59_retina.h5ad`. Symlinked
  in as `notebooks/D59_retina.h5ad` (path already in .gitignore); not
  committed.
katosh added 4 commits May 18, 2026 19:24
…59 retina, strip parallelization, notebook tqdm
Move all three public functions from try_models.py (try_models,
read_test_results, plot_model_heatmap) and both from test_components.py
(sweep_diffusion_components, collect_sweep_residual_means) into utils.py
and delete the now-empty source files.

Eliminates the pytest-collision module name (test_components) and
collapses two near-singleton modules into the existing utility surface.
No backward-compat shims — operator accepted the API break pre-release.

- src/scEcho/utils.py: extend imports (logging, Sequence, mellon, sns,
  Axes, scipy.sparse, tqdm, Echo_features.embeddings_predict_layer) and
  __all__; append the five moved functions verbatim at the end.
- src/scEcho/__init__.py: drop try_models and test_components from the
  module re-exports.
- src/scEcho/try_models.py, src/scEcho/test_components.py: removed.
- README.md: collapse the try_models and test_components bullets into a
  single utils bullet that describes the broadened surface.
…tils

Tests now live alongside the rest of utils coverage. Imports switched
from scEcho.try_models.* / scEcho.test_components.* to scEcho.utils.*,
matching the source merge in the previous commit.

All 50 tests still pass; coverage 80% (down ~3pts from the wave-2
baseline because the moved try_models source has a couple of branches
that the existing fixtures don't exercise — not a regression in test
behavior, just more denominator).
…nts into utils; fold tests; eliminate pytest-collision module name
@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

Round-3 response — structure stream + notebook polish (both operator-requested via comments 4483832807 and 4483735623):

Structure stream (7bc0bac, 92bb2f1 on dominik/echo-polish-structure, merged at e0458f9):

  • try_models (3 funcs) and test_components (2 funcs) absorbed into utils.
  • Source files deleted (no backward-compat shims per operator).
  • Pytest-collision module name test_components eliminated; the inner functions never had test_* prefixes, so the module name was the entire fix.
  • tests/test_try_models.py + tests/test_test_components.py folded into tests/test_utils.py (149 → 304 LOC).
  • __init__.py re-exports + utils.__all__ updated; old import paths scEcho.try_models / scEcho.test_components now raise ModuleNotFoundError (intentional).
  • README's Modules section collapsed to a single utils bullet with the broadened scope.
  • Coverage: 50/50 tests pass, 80 % line coverage (down 3 pts from 83 % wave-2 baseline — the moved code has branches the existing fixtures don't exercise yet; absolute pass count and behavior unchanged).

Notebook polish (PR #5 merged at fa45914):

  • notebooks/example.ipynb executed end-to-end on D59 retina (5412 cells × 23519 features, 10:15 wall).
  • 14 empty scaffolding cells removed; threadpoolctl wrapper dropped; tqdm widget state captured (renders as completed bars on GitHub's notebook viewer instead of "Loading widget…").
  • Cell count 36 → 20.
  • Two non-blocking runtime warnings surfaced + left out of scope (now in PR body's "Remaining improvements" Tier D): a RuntimeWarning: divide by zero in Echo_features.py:~600 (NaN-safe downstream) and a seaborn UserWarning in plotting.py:~948.

PR body updated with the final per-stream table, the API-breaking changes (try_models / test_components removal + the optimizer default flip from "advi" to None), and the consolidated "Remaining improvements" section reflecting what's still deferred to v0.2.0 + Tier D items noticed during integration.

All eight specialist branches landed. PR is ready for final review.

Tests: 50/50 green, 80 % line coverage, 1 warning (jaxopt-from-Mellon DeprecationWarning, upstream).

@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

@settylab-dotto-bot Implement Tier A and drop the redundant + 1e-16

@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

@settylab-dotto-bot run black for notebooks on the example notebook, and what about all the RuntimeWarning in the output?

katosh added a commit that referenced this pull request May 19, 2026
Per operator follow-up on PR #4 (comment 4483953364): apply nbqa black
formatting and address the RuntimeWarnings that surfaced in the
previous executed-notebook outputs.

Formatting
- `nbqa black notebooks/example.ipynb` (black 25.x, target py311).
  Each cell normalized: multi-arg calls wrap one-per-line, dict / list
  literals normalized, redundant zero-prefix decimals trimmed
  (`.3` → `0.3`, `.1` → `0.1`), trailing-comma + line-length conventions
  applied. No semantic changes.

Re-execution
- `jupyter nbconvert --to notebook --execute --inplace`. Wall clock
  9:40, zero error cells, 220 widget states preserved.
- All 14 `RuntimeWarning: divide by zero / invalid value in divide`
  blocks from cell 13 are gone — the np.errstate fix in the previous
  commit suppresses them at source.
- The per-group "N features have zero variance" UserWarnings remain
  (intentional — same channel, no longer doubled with the divide noise).

Residual stderr noise (out of this PR's scope, all surfaces non-blocking):
- seaborn `UserWarning: Numpy array is not a supported type for palette`
  from plotting.py:948 — needs an upstream-side cast.
- matplotlib FancyArrowPatch informational from plot_scores's labeller —
  harmless layout hint.
- Sandbox-only joblib loky traceback fragment in cell 7 stripped (same
  surgical approach as PR #5 — won't fire on non-sandbox runs).
@katosh
Copy link
Copy Markdown
Contributor Author

katosh commented May 19, 2026

Tier A landed as #7 (operator ask in comment 4483947672):

  • _compute_group_mhd_and_stats helper extracted (~50 LOC dedup between get_desynch_stats and run_null_desynch_test)
  • Echo_states.pyecho_states.py, Echo_features.pyecho_features.py (PEP 8; API-breaking, no shims)
  • direction_colors + adjust_text arrowprops lifted to kwargs/defaults
  • + 1e-16 dropped at Echo_states.py:~157 (sqrt-input min observed 0.0225 across the test suite — ~2.25e14× the epsilon)
  • CHANGELOG.md added covering both this PR (v0.1.0) and v0.2.0 follow-up: extract MHD helper, snake_case rename, lift knobs (API-breaking) #7 (v0.2.0 Unreleased)

#7 stacks on dominik/v0.1.0-polish; reviewer can re-target to main after this PR merges via gh pr edit 7 --base main. 50/50 pytest green at 80 % line coverage on both branches; determinism regression bit-identical.

Worker report: reports/echo-polish_2026-05-18_200627_echo-tier-a.md.

katosh and others added 9 commits May 18, 2026 20:42
The ``+1e-16`` inside ``np.sqrt(variance_model + 1e-16)`` was a hardcoded
floor; lift it to a public kwarg so callers can override (raise it for
extra-noisy variance, set it to 0 to disable). Default unchanged at
1e-16 — empirical floor (min observed 0.0225 across the test suite)
makes the default a no-op, but the guard stays for robustness per
operator decision on PR #7.
…t_desynch_stats and run_null_desynch_test

Removes ~50 LOC of near-identical Mahalanobis-distance + obsp-key-fallback
code between the two functions, plus the dead 'diff' assignment in the
null path. The two call sites differ only in (a) which layer namespace
the predicted-covariance keys live under (observed layer vs null layer)
and (b) which layer namespace the precomputed LFC layer lives under
(always the observed layer in current code paths).

Helper signature:
    _compute_group_mhd_and_stats(
        ad, ind, layer, layer_for_lfc, embedding1, embedding2,
        diagonal_variance,
    )

  - get_desynch_stats: layer=layer,      layer_for_lfc=layer
  - run_null_desynch_test: layer=null_layer, layer_for_lfc=layer

Determinism regression passes bit-identically. Future drift between the
two paths now has only one site to update.
Three previously hardcoded knobs lifted so downstream callers can
customize without monkey-patching:

  - ``dn_comp_obsm(..., direction_colors=("#ff7f0e", "#1f77b4", "lightgrey"))``
    — sequence written to ``ad.uns[f"{direction_key}_colors"]``, in the
    order ``(modality2-higher, modality1-higher, neutral)`` matching the
    CategoricalDtype.
  - ``run_null_desynch_test(..., direction_colors=(...))`` — sibling lift,
    same default, same order matching its own CategoricalDtype
    ``(modality2-structure, modality1-structure, not-significant)``.
  - ``plot_scores`` — module-level ``_DEFAULT_ADJUST_TEXT_KWARGS`` with
    the previously hardcoded ``arrowprops`` style; callers can override
    any key (or add new ones) via the existing ``**adjust_text_kwargs``
    catch-all. The repulsion knobs (``expand``, ``force_text``,
    ``force_points``, ``max_move_frac``, ``iter_lim``) stay as explicit
    kwargs — already lifted in wave-1.
API-breaking. No backward-compat shim (operator-approved). Users
importing the module qualifier or referencing it as
``scEcho.Echo_states.<name>`` must update their import paths:

    from scEcho import Echo_states         -> from scEcho import echo_states
    scEcho.Echo_states.dn_comp_obsm(...)   -> scEcho.echo_states.dn_comp_obsm(...)

Symbols inside the module (``dn_comp_obsm``) are unchanged. Notebook
source cells updated in-place (no re-run; cached D59-retina outputs
preserved); cached warning paths still reference the old filename and
that is fine.

Echo_features renamed in the next commit; bundling both into one
commit would obscure git's rename detection.
API-breaking. No backward-compat shim (operator-approved). Users
importing the module qualifier or referencing it as
``scEcho.Echo_features.<name>`` must update their import paths:

    from scEcho import Echo_features
    -> from scEcho import echo_features
    scEcho.Echo_features.run_echo_features(...)
    -> scEcho.echo_features.run_echo_features(...)
    from scEcho.Echo_features import compute_ncells
    -> from scEcho.echo_features import compute_ncells

Symbols inside the module (``embeddings_predict_layer``,
``get_desynch_stats``, ``run_null_desynch_test``, ``run_echo_features``,
``make_null_layer``, ``get_reconstruction_results``, ``compute_ncells``)
are unchanged. Notebook source cells updated in-place; cached
RuntimeWarning paths in outputs still reference the old filename and
that is harmless.

Internal cross-module import in ``utils.py`` updated as part of this
commit (``from .Echo_features import embeddings_predict_layer``
→ ``from .echo_features import ...``).

Combined with the Echo_states rename in the previous commit and the
try_models / test_components absorption from PR #4's structure stream,
this completes the v0.2.0 API boundary.
…bution

Wrap `mse_null_diff / null_var` (run_null_desynch_test, ~L600) in
np.errstate(divide="ignore", invalid="ignore"). Zero-variance features
produce inf / NaN here by design, and the immediately-following
`expressed_mask = null_var > 0` filters them out before any statistic
that would propagate the NaN. The RuntimeWarnings were noise about an
edge case the algorithm already handles correctly; the per-group
UserWarning "N features have zero variance in group G ..." remains the
user-facing channel for that information.

Surfaced by polish PR #5's executed notebook outputs (operator pointed
at the 14 RuntimeWarning blocks in cell 13). Found via inspection,
fixed at source rather than suppressed at the notebook level — the
fix is unambiguous and would help anyone running run_null_desynch_test
outside the notebook too.
…e" UserWarning

Per operator pushback on PR #6 follow-up (comment 4484264635). The
``expressed_mask = null_var > 0`` filter on the next line already drops
the zero-variance features silently; the per-group warning was noise
the user does not need to act on. The count is recoverable by selecting
``var_explained_diff_*_null_*`` columns and counting non-finite entries
if downstream code wants it.

Pairs with the np.errstate guard one commit prior: together they make
``run_null_desynch_test`` quiet about zero-variance features, matching
how the algorithm treats them at every other step.

pytest 50/50 in scEcho-notebook worktree.
Per operator review on PR #6 (comment 4484264635): rebase on the
current dominik/v0.1.0-polish tip and re-execute. Source-side changes
(np.errstate guard in run_null_desynch_test, drop of the per-group
zero-variance UserWarning) landed directly on dominik/v0.1.0-polish
in c0d9d23 + f948697 — this commit is notebook-only.

Notebook delta vs. the pre-rebase state
- nbqa black formatting (target py311). Each cell's multi-arg calls
  wrap one-per-line, decimal literals zero-prefixed, trailing commas
  per black convention. No semantic changes.
- Module references updated to the Tier A snake_case names:
  ``scEcho.Echo_states.*`` → ``scEcho.echo_states.*``
  ``scEcho.Echo_features.*`` → ``scEcho.echo_features.*``
- Markdown / structural polish carried forward from PR #5: 36→20 cells,
  merged section headers, suppressed jaxopt + loky transitive warnings,
  cyNpre→MPC markdown fix, kernelspec normalised to python3.

End-to-end run on D59_retina.h5ad (5412 cells × 23519 features,
8 combo_type groups). Wall clock 9:11, zero errors, zero
RuntimeWarning blocks, zero zero-variance UserWarning blocks
(both surfaces now silent thanks to the source-side fixes on polish),
220 widget states preserved.
notebook(example): nbqa-black + RuntimeWarning fix at source
PR #4 carried ~7.4k lines of notebook diff alongside the source/API
changes. Notebook is being landed standalone in #8 (off main, same
polish-version content) so this PR's diff stays focused on the source
surface. Three-way merge handles either merge order:

- If #8 lands first: main has polish notebook; polish branch has main-original
  notebook in this commit; merging polish makes no notebook change because
  polish's tip-state == base-state == polish made no net change. #8's notebook
  is preserved on main.
- If #4 lands first: main has main-original notebook (this commit's state);
  then #8 merges to update notebook.

Either way, the notebook ends up correctly on main and #4's diff is clean.
@Connorr0 Connorr0 merged commit 03ebb9b into main May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants