Skip to content

Feedback/santi changes - #1

Open
phisanti wants to merge 32 commits into
masterfrom
feedback/santi-changes
Open

Feedback/santi changes#1
phisanti wants to merge 32 commits into
masterfrom
feedback/santi-changes

Conversation

@phisanti

Copy link
Copy Markdown
Contributor

I reviewed the package against more datasets than mox-brca and found a few issues:

  1. Makefile pointed at the wrong package — PKGNAME was WGCNAplus, a copy-paste leftover. make build/make check were silently building and checking the sibling package instead of lasagna, no error.
  2. Test suite was fully broken. Every test errored at setup on a wrong argument name, and a stray dev script under tests/ made R CMD check fail before the real suite even ran — effectively zero coverage was actually executing.
  3. Core pipeline crashed under default or near-default settings: solve(sp.weight=TRUE) crashed with create_model()'s own default (add.sink=FALSE), and plot_3d()'s default layout path required two undeclared dependencies (Rtsne, uwot).
  4. A feature-selection filter silently dropped the highest-variance features whenever data had missing values — no error, just wrong output.

Besides that, I also tackled a number of minor issues, but those are mostly cosmetic. After all changes, I verified the output is identical to before.

tests/test-simple.R referenced an unrelated sibling package
(WGCNAplus) and other undeclared dependencies, and was picked up
by R CMD check as a standalone test since it lived directly under
tests/ rather than tests/testthat/. It errored immediately, so the
real testthat suite never even got a chance to run under check.
Both tests in test-model.R passed pheno="pheno" to create_model(),
but the function's phenotype-mode parameter is meta.type, not pheno.
Every test in the suite was erroring at setup as a result.
apply(x, 1, stats::sd) without na.rm=TRUE returns NA for any feature
with missing values, and order()'s default na.last=TRUE pushes those
to the bottom, so head(..., ntop) silently excluded them regardless
of true variance. Common with real proteomics/metabolomics layers.
sp_edge_weight() calls igraph::shortest_paths() from SOURCE/SINK,
which only exist when the model was built with add.sink=TRUE (not
the default). Fail fast with a clear message instead of surfacing
igraph's opaque 'Invalid vertex names' error.
layout_multipartite_3d()'s tsne/umap branches called Rtsne::Rtsne()
and uwot::umap() without either package declared anywhere, so a
clean install failed with 'there is no package called uwot' the
moment plot_3d()'s default path hit that branch. Add them as
Suggests with requireNamespace() guards and clear error messages,
matching how other optional heavy deps are usually handled here.
clust=c("svd","tsne","umap") was never resolved with match.arg(),
so omitting the argument left clust as the full length-3 vector and
the first clust=='tsne' comparison crashed trying to coerce a
length-3 logical to a scalar. Also gives a proper error for invalid
values instead of silently falling through to svd.
expandPhenoMatrix() returns NULL when no phenotype column has
resolvable/varying groups (e.g. a single-condition dataset), and
create_model() didn't check before doing t(Y) three lines later,
crashing with an unrelated t.default() error instead of explaining
what was actually wrong.
- PKGNAME was a copy-paste leftover from the wgcnaplus Makefile template
- on this case-insensitive filesystem it resolved to the sibling wgcnaplus directory, so make build/check silently targeted the wrong package instead of failing loudly
- do.plot=TRUE only warned on an empty graph then crashed several
  lines later with an unrelated 'names attribute' coercion error
- do.plot=FALSE silently returned instead, giving no signal the
  result was empty
- replace both with one clear stop() the moment the graph is empty
- A[ii, jj] without drop=FALSE silently turns into a plain vector
  whenever a merged layer has exactly one row or one column
- same class of implicit-dimension-collapse bug already fixed
  elsewhere; add drop=FALSE so a future refactor of the accumulation
  logic below can't silently start producing wrong values
- both the inter-layer mask loop and the edge-reduction loop used
  seq_len(length(x) - 1), which becomes seq_len(-1) and errors when
  there are 0-1 layer types (e.g. data$X supplied as an empty list)
- guard each loop with length(x) >= 2 so a degenerate/minimal model
  is still constructible instead of erroring on an internal detail
svd(ff[[k]])$u on a 1-row matrix has only 1 column, so the [,1:2]
subset threw 'subscript out of bounds' -- e.g. a PHENO layer
collapsed to one contrast column with add.revpheno=FALSE. Place
single-feature layers at a fixed (0,0) instead of a meaningless
1-feature SVD.
- rowMeans() on a 0-column matrix returns NaN with no warning when a
  phenotype is entirely one-sided (all samples same group)
- that NaN silently propagated into fold changes and edge weights
- stop early with a clear message instead
Rtsne requires perplexity < (nrow-1)/3 strictly, but the perplexity
was computed as nn/3, which is always greater than that bound -- so
clust='tsne' errored on every call past the nn>5 threshold instead
of only at genuine edge cases.
- non-paired multi-omics (each layer measured on a different sample
  subset) merges through mofa.merge_data2 without error, but can
  leave most of the result NA with no signal to the caller
- warn when fewer than half the samples have data in every layer,
  naming the NA fraction and complete-sample fraction
plotly is already a declared Import and every call site is properly
namespaced with plotly::, so these did nothing. R CMD check flagged
them: use :: or requireNamespace() instead of library/require.
plot_visgraph()/plot_3d() both honor a layout attribute pre-attached
to the graph object, but nothing in this package ever sets one --
create_model()/solve() don't. Not dead code: it's a low-cost way for
a caller to inject a precomputed layout once and have later plot
calls pick it up automatically. Document it as such.
Had only @export, no title/description/param/return, so roxygen
emitted no Rd page and R CMD check flagged it as an undocumented
exported object.
create_model, plot_3d, plot_multipartite, plot_visgraph, and
plotlyLasagna each had roxygen blocks missing @PARAM entries for
some of their real arguments, flagged by R CMD check as
undocumented. Add the missing entries, matching each function's
existing doc style.
man/*.Rd had drifted from R/ source for some time (missing params,
missing examples, and in plot_3d.Rd a stale \item{pos} entry for a
parameter that no longer exists). Sync now that source docs are
complete.
- %>% is used throughout plot.R, but only worked because igraph
  transitively re-exports magrittr's pipe -- an implementation
  detail that could break silently if igraph ever drops it
- add magrittr to Imports and importFrom it directly
- the inline apply(x, 1, stats::sd, na.rm=TRUE) top-ntop filter and
  the following mofa.topSD(xx, ntop) call did identical work
- collapse to the single mofa.topSD call; verified selected features
  are unchanged on a toy dataset with injected NAs
tests/multipartite-moxbrca.pdf was a generated side effect of the
now-removed test-simple.R dev script, not meant to be tracked.
Ignore tests/*.pdf going forward.
- Makefile has GNU-only ifdef extensions R CMD check flags as a
  portability warning, but it's a dev convenience wrapper, not part
  of the installed package -- exclude it, like wgcnaplus already
  does for its own equivalent Makefile
- also exclude other local-only files (CLAUDE.md/AGENTS.md,
  .semquery, .beads, local_data) that have no business in a tarball
apply(etype, 2, match, gr$layers) relies on etype being a 2-column
matrix, but apply() silently collapses to a plain vector when there
are 0 or 1 edges (not just 0, as first suspected -- a degenerate
model with a single edge hits this on ordinary input). Handle those
cases directly instead of relying on apply()'s matrix shape.
- all three were maintainability-audit hotspots (plot_3d cyclomatic
  29, MI 39.6, worst in the file); the file as a whole failed its
  maintainability floor
- extract data-prep (layout resolution, node frame building, edge
  selection, subgraph filtering, styling) into small internal
  helpers; leave each exported function as a thin call-then-render
  wrapper
- zero behavior change: exported signatures untouched, verified via
  a 25-case snapshot harness comparing resolved plotly/visNetwork
  output before and after, plus byte-identical error messages
  (including call attribution) on all three stop() paths
- second-highest complexity hotspot in the package (cyclomatic 22,
  MI 46.4 risky), after plot_3d which was already split
- extract one helper per filtering concern (layer resolution, node
  selection, regex filtering, top-N, weight normalization, min-rho
  zeroing, sign filtering, type filtering, final delete+prune)
- zero behavior change: verified across 466 parameter combinations
  via identical() comparison of graph structure/attributes
- core per-phenotype function was the package's third complexity
  hotspot (cyclomatic 14, MI 46.6 risky); its complexity is likely
  why the sp.weight/add.sink and degenerate-phenotype bugs fixed
  earlier this session went unguarded for so long
- extract sample masking, correlation, fold-change, node/edge value
  assignment, shortest-path weighting, and edge capping into 7
  helpers; both crash guards fixed earlier stay in their own helper
- zero behavior change: verified across 731 parameter combinations,
  including both guarded error paths
- layout_multipartite_3d, multisolve, plot_3d, plot_multipartite,
  plot_visgraph, plotlyLasagna, prune_graph had zero test coverage
  despite being where every real crash bug found this session lived
- add regression tests for the specific fixed bugs (clust match.arg
  default, single-feature svd, empty-filter plot_multipartite,
  plot_3d's three stop() paths) so they can't silently regress
- shared toy-model fixtures in helper-toy.R keep the suite fast
- both do the same thing (repeat each element twice consecutively)
  but mapply's per-call dispatch overhead makes it ~68x slower than
  the equivalent vectorized rep() for this
- byte-identical output, verified via identical() on real pipeline
  data as well as isolated benchmarks
- matrixStats is already a declared Import; rowMaxs is a single
  vectorized C call vs apply()'s per-row dispatch overhead, ~27x
  faster in isolated benchmarks
- rowMaxs doesn't name its output like apply() does, and the next
  line greps names(maxrho) for SOURCE/SINK -- restore names
  explicitly so that lookup keeps working
- verified identical() on maxrho and every graph/X component, for
  both add.sink=TRUE and add.sink=FALSE
- rii/rjj found the nc-th largest |correlation| per row/column via
  apply(abs(R1), 1/2, function(r) tail(sort(r), nc)[1]), duplicated
  4x across the inter- and intra-layer edge-reduction loops
- matrixStats::rowOrderStats/colOrderStats compute the k-th smallest
  directly; k = pmax(length - nc + 1, 1) gives the nc-th largest,
  matching tail()'s behavior even when nc exceeds the row/col length
- this is the largest apply()-related contributor to create_model()'s
  wall time per profiling; verified identical() output including the
  nc-exceeds-length edge case, across full graph/X comparisons
@phisanti
phisanti requested a review from ESCRI11 August 11, 2026 13:00
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.

1 participant