Skip to content

fix: correct data corruption and MuData compatibility bugs - #1

Merged
zqfang merged 1 commit into
mainfrom
fix/correctness-and-mudata-compat
Jul 15, 2026
Merged

fix: correct data corruption and MuData compatibility bugs#1
zqfang merged 1 commit into
mainfrom
fix/correctness-and-mudata-compat

Conversation

@zqfang

@zqfang zqfang commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Fixes found during a repository audit. Every fix is pinned by a regression test that fails without it.

Why this matters

No .h5mu file this package produced could be opened by Python mudata. WriteH5MU created /obsm and /varm lazily inside the reductions loop, so an object without multimodal reductions never got them, and /varp, /obsmap and /varmap were never written at all. obsmap/varmap turn out to be part of MuData format 0.1.0 — the version we already declare — so this was incompleteness rather than a version mismatch.

Separately, two bugs silently corrupted metadata rather than failing loudly.

Data correctness

Bug Effect
Categorical decoding truncated the category list to the number of observed codes An unused category shifted the labels: "c" was read back as "b". Cell-type/cluster labels corrupted.
NA in character columns coerced to NaN Missing values were written as the literal text "NaN". Now stored as categorical (missing = code -1), since anndata has no nullable-string encoding.
WriteH5AD fell through to assay #1 on an unmatched assay A typo'd assay name silently wrote the wrong modality. Now errors.
which(!is.null(x)) applied is.null() to the list, not its elements Always selected counts; a data-only assay errored out.

MuData compatibility

  • Always write obsm, varm, obsp, varp, obsmap and varmap.
  • ReadH5MU used names(modalities)[2:length()], which yields c(NA, ...) for a single modality, so single-modality .h5mu files could not be read.

Tests

tests/testthat.R was missing, so R CMD check never ran the suite. As a result test_sparse.R had been failing unnoticed since the default sparse.type became csr_matrix (it still asserted csc_matrix); those expectations are corrected and both storage paths are now covered. Coverage extended to metadata, assay selection and .h5mu round-trips.

0 → 57 assertions.

Other

  • Use nchar() rather than length() when stripping modality prefixes from reduction names.
  • The anndata userblock reported the mudata format version.
  • Removed unreachable branches, a dead obs-name prefix guard, and four unused writers. Net: R/ shrank by ~30 lines.
  • Regenerated man pages, which omitted scale.data and sparse.type entirely.
  • CITATION.cff described muon, not this package.

Verification

  • R CMD check: Status: OK (0 errors / warnings / notes), [ FAIL 0 | WARN 0 | SKIP 0 | PASS 57 ].
  • Cross-checked end-to-end against Python anndata 0.12.0rc1 and mudata 0.3.1: NA, unused categories and matrix orientation all survive the round-trip.

Notes for review

  • The is("obs", "data.frame") guard was dead (it tested a string literal). It was removed rather than repaired — activating it would break ReadH5MU, which intersects obs names across modalities, so per-modality prefixes would empty the intersection.
  • Not addressed: the ~90-line duplicated reductions block between WriteH5AD and WriteH5MU, and the read path still returns a v3-style Assay rather than Assay5. Both are behaviour/refactor decisions rather than bug fixes.

Findings from a repository audit. Each fix is pinned by a regression test
that fails without it.

Data correctness:
- Categorical columns were relabelled by position when a category was
  unused, so e.g. "c" was read back as "b". Decode against the full
  category list instead of truncating it to the number of observed codes.
- NA in character columns was coerced to the literal string "NaN". Store
  those columns as categorical (missing = code -1), as anndata has no
  nullable-string encoding.
- WriteH5AD fell through to the first assay when `assay` did not match any
  name, silently writing the wrong modality. It now errors instead.
- The single-layer fallback always wrote `counts`, because is.null() was
  applied to the list rather than to its elements. A data-only assay failed.

MuData compatibility:
- /obsm and /varm were created lazily inside the reductions loop, and
  /varp, /obsmap and /varmap were never written at all, so files without
  multimodal reductions could not be opened by mudata. All of the groups
  readers require are now always written.
- ReadH5MU used names(modalities)[2:length()], which yields c(NA, ...) for
  a single modality.

Also: use nchar() rather than length() when stripping modality prefixes
from reduction names, report the anndata format version (not the mudata
one) in the anndata userblock, and drop unreachable branches, a dead
obs-name prefix guard, and four unused writers.

Tests:
- Add tests/testthat.R. Without it R CMD check never ran the suite, and
  test_sparse.R had been failing unnoticed since the default sparse.type
  became csr_matrix. Corrected those expectations and extended coverage to
  metadata, assay selection and .h5mu round-trips (57 assertions).

Docs:
- Regenerate man pages, which omitted the scale.data and sparse.type
  arguments, and correct CITATION.cff, which described muon.

Verified against anndata 0.12 and mudata 0.3.1. R CMD check is clean.
Copilot AI review requested due to automatic review settings July 15, 2026 04:28
@ecc-tools

ecc-tools Bot commented Jul 15, 2026

Copy link
Copy Markdown

Analyzing 200 commits...

@ecc-tools

ecc-tools Bot commented Jul 15, 2026

Copy link
Copy Markdown

Analysis Complete

Generated ECC bundle from 1 commits | Confidence: 50%

View Pull Request #2

Repository Profile
Attribute Value
Language TypeScript
Framework Not detected
Commit Convention conventional
Test Directory separate
Changed Files (15)
Metric Value
Files changed 15
Additions 444
Deletions 230

Top hotspots

Path Status +/-
tests/testthat/test_sparse.R modified +51 / -81
tests/testthat/test_assays.R added +128 / -0
R/WriteH5MU.R modified +52 / -59
R/WriteUtils.R modified +33 / -60
tests/testthat/test_metadata.R added +85 / -0

Top directories

Directory Files Total changes
tests/testthat 3 345
R 4 238
man 3 51
. 4 36
tests 1 4
Analysis Depth Readiness (commit-history, 7%)

ECC Tools uses this to decide whether recommendations should stay at commit-history/setup guidance or expand into CI, security, harness, reference-set, AI-routing, and team backlog work.

Area Status Evidence / Next Step
Commit history Partial 1 commits sampled
CI/CD signals Missing Add workflow files or CI troubleshooting evidence so ECC Tools can reason about pipeline setup.
Security evidence Missing Add AgentShield, audit, SARIF, SBOM, or security review evidence so recommendations can cover security posture.
Harness configuration Missing Add Claude, Codex, OpenCode, Zed, dmux, MCP, plugin, or cross-harness config evidence for harness-agnostic recommendations.
Reference/eval evidence Missing Add fixtures, golden traces, reference sets, or evaluator benchmarks so deeper recommendations have regression evidence.
AI routing and cost controls Missing Add model-routing, budget, usage, or cost-control files before relying on AI-heavy automation recommendations.
Team handoff and project tracking Missing Add roadmap, runbook, project, Linear, or follow-up tracking docs so generated work can land in a team queue.
Reference Set Readiness (0/7, 0%)
Area Status Evidence / Next Step
Deep analyzer corpus Missing Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions.
RAG/evaluator comparison Missing Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior.
PR salvage/review corpus Missing Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation.
Discussion triage corpus Missing Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications.
Harness compatibility Missing Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces.
Security evidence Missing Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs.
CI failure-mode evidence Missing Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes.
Generated Instincts (17)
Domain Count
git 4
code-style 9
testing 4

After merging, import with:

/instinct-import .claude/homunculus/instincts/inherited/MuDataSeurat-instincts.yaml

Files

  • .claude/ecc-tools.json
  • .claude/skills/MuDataSeurat/SKILL.md
  • .agents/skills/MuDataSeurat/SKILL.md
  • .agents/skills/MuDataSeurat/agents/openai.yaml
  • .claude/identity.json
  • .codex/config.toml
  • .codex/AGENTS.md
  • .codex/agents/explorer.toml
  • .codex/agents/reviewer.toml
  • .codex/agents/docs-researcher.toml
  • .claude/homunculus/instincts/inherited/MuDataSeurat-instincts.yaml

ECC Tools | Everything Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes several correctness and format-compliance issues in MuDataSeurat’s .h5ad/.h5mu read/write paths, and adds regression tests to prevent silent data corruption and MuData incompatibilities from reoccurring.

Changes:

  • Fix categorical decoding and NA handling to avoid silent metadata corruption on round-trips.
  • Make .h5mu output MuData-compatible by always writing required top-level groups (including obsmap/varmap) and fix single-modality .h5mu reading.
  • Add a full testthat harness plus new regression tests covering sparse storage types, metadata, assay selection, and .h5mu round-trips.

Reviewed changes

Copilot reviewed 12 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/testthat/test_sparse.R Updates sparse-matrix round-trip tests for CSR/CSC behavior and error handling.
tests/testthat/test_metadata.R Adds regression tests for factor/character metadata round-trips (unused levels, NA handling).
tests/testthat/test_assays.R Adds tests for assay selection errors, layer handling, and .h5mu required groups/maps.
tests/testthat.R Adds the testthat entrypoint so R CMD check runs the suite.
README.md Documents new correctness guarantees and fixes usage examples.
R/WriteUtils.R Fixes anndata userblock versioning and improves NA handling for character vectors via categorical encoding.
R/WriteH5MU.R Fixes assay selection behavior, reduction-name prefix stripping, and ensures MuData-required groups/maps are written.
R/ReadUtils.R Fixes categorical decoding to preserve full category lists (incl. unused levels) and corrects warning text.
R/ReadH5MU.R Fixes single-modality modality iteration bug ([-1] vs 2:length()).
man/WriteH5MU.Rd Regenerated docs for new args; contains minor parameter-doc typos (flagged).
man/WriteH5ADHelper.Rd Regenerated docs to include new args.
man/WriteH5AD.Rd Regenerated docs for new args; contains minor parameter-doc typos (flagged).
DESCRIPTION Adds testthat/fs to Suggests and updates RoxygenNote.
CITATION.cff Updates citation metadata to match this package instead of muon.
.Rbuildignore Ignores CITATION.cff in R builds.
Files not reviewed (3)
  • man/WriteH5AD.Rd: Generated file
  • man/WriteH5ADHelper.Rd: Generated file
  • man/WriteH5MU.Rd: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/WriteH5MU.R
Comment on lines +283 to +290
if (length(object@assays) > 1) {
h5$close()
stop(paste0(
"An assay to be written has to be provided, one of: ",
paste(names(object@assays), collapse = ", "),
".\nUse WriteH5MU() to write all the modalities."
))
}
Comment thread R/WriteH5MU.R
Comment on lines +292 to 300
} else if (!assay %in% names(object@assays)) {
# Never fall back to another assay: writing a different assay than the one
# that was asked for would silently produce a file with the wrong data.
h5$close()
stop(paste0(
"An assay to be written has to be provided, one of: ",
"Assay ", assay, " not found. Available assays: ",
paste(names(object@assays), collapse = ", "),
".\nUse WriteH5MU() to write all the modalities."
"."
))
Comment on lines +59 to +62
test_that("a csr_matrix .h5ad can be read", {
srt <- ReadH5AD(fileh5ad_r)
counts <- GetAssayData(srt, layer = "counts")

Comment on lines +71 to +73
test_that("a csc_matrix .h5ad can be read", {
srt <- ReadH5AD(fileh5ad_c)
counts <- GetAssayData(srt, layer = "counts")
Comment thread R/WriteH5MU.R
Comment on lines 351 to +354
names(var_names) <- modalities
write_data_frame(h5, "var", do.call(c, var_names))

write_mod_maps(h5, modalities, nrow(obs), var_names)
Comment thread man/WriteH5MU.Rd
Comment on lines +29 to +31
\item{scale.data}{Boolen, wether to export scale.data.}

\item{sparse.type}{String, save as csr_matrix or csc_matrix.}
Comment thread man/WriteH5AD.Rd
Comment on lines +33 to +36
\item{scale.data}{Boolen, wether to export scale.data.}

\item{sparse.type}{String, save as csr_matrix or csc_matrix.}

@zqfang
zqfang merged commit e69e27f into main Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants