Skip to content

Adopt main as the fork's shipping branch - #3

Merged
Marius1311 merged 21 commits into
mainfrom
integration
Aug 27, 2026
Merged

Adopt main as the fork's shipping branch#3
Marius1311 merged 21 commits into
mainfrom
integration

Conversation

@Marius1311

Copy link
Copy Markdown
Member

Makes main the fork's shipping branch and retires the force-pushed integration branch.

Upstream is maintained but does not take patches: of the 100 most recently merged PRs on
lilab-bcb/cirrocumulus — back to Jan 2023 — every one came from lilab-bcb itself, and the only
outside contributor PR has been open since Oct 2024. The mirror-plus-integration model was built
on the assumption that our PRs would land upstream. They will not, so the fork's own main becomes
what we build, test and release.

This PR carries the seven topic branches that were already merged into integration, in merge
order, with their commits and merge commits intact:

branch upstream PR what it fixes
fork/tooling FORK.md, scripts/check_wheel.py, the release workflow
fix/x-stats-sparse-dense lilab-bcb#230 X_stats returned 2-D columns for sparse X, 500ing the composition view; dense X unhandled
fix/dotplot-aggregator-anndata lilab-bcb#231 DotPlotAggregator was written for a DataFrame but is passed an AnnData, 500ing the dot plot view
fix/anndata-013-none-layer lilab-bcb#236 anndata >= 0.13 exposes None in layers as an alias for X; six call sites treated it as a layer
fix/zarr-categorical-obs lilab-bcb#233 elements anndata writes as groups rather than arrays could not be read at all
fix/zarr3 lilab-bcb#237 zarr 3 support; replaces the vendored 2021-era anndata zarr writer with anndata.io.write_elem
fix/pandas3 lilab-bcb#238 pandas 3 ArrowStringArray overflowed the JSON encoder; broke prepare_data --format jsonl and 500'd /api/data for categorical obs

All six upstream PRs stay open. Their branches are kept, since deleting a head branch closes its PR.

Merge this with "Create a merge commit" — not squash, not rebase — so the seven merge commits
and their SHAs survive. If upstream ever merges one of those PRs, shared SHAs keep the next
git merge upstream/main trivial.

Follow-ups, in order: retarget release.yml and rewrite FORK.md for the new model; apply ruff
formatting (the fork's code predates upstream's black-100 → ruff-88 switch in lilab-bcb#234); add a ruleset
on main; delete integration.

Marius1311 and others added 21 commits August 24, 2026 11:16
FORK.md documents the branch model; fetch_client.py unpacks the prebuilt web
client from a published wheel, since we have no node toolchain. Neither is
upstreamable, so both live on a topic branch rather than on the main mirror.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYsKhgGuNCFtxqFhSLGtCK
`X.sum(axis=0)` and `X.mean(axis=0)` on a scipy sparse matrix return `np.matrix`,
whose `.flatten()` yields a (1, n) matrix rather than a 1-D array, so building the
DataFrame raised:

    ValueError: Data must be 1-dimensional, got ndarray of shape (1, 1)

This breaks every request that carries `stats`, i.e. the composition/summary views.
Dense `X` failed too, on the `.toarray()`/`.getnnz()` calls that only exist on
sparse matrices.

Reduce over each layout explicitly and use `np.asarray(...).ravel()`, which is 1-D
for both `np.matrix` and `ndarray`. Adds `tests/test_stats.py`, covering `X_stats`
and `FeatureAggregator` over sparse and dense inputs -- neither had any coverage,
which is how this survived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6uSHPpkfbneXYsprQ3wJ6
Both call sites in `data_processing` pass an AnnData -- the second one even names
the local `df` after `apply_filter(adata, ...)` returns one -- but `execute` was
written against a DataFrame, indexing measures as columns. So every request
carrying `groupedStats` fails:

    KeyError: '<grouping column>'

which is the whole dot plot / distribution view.

Gather the requested dimensions from `adata.obs` and the measures from `adata.X`
into a frame first, matching how `FeatureAggregator` already consumes AnnData.
Only the requested genes are densified, so the cost stays proportional to the
request rather than the dataset.

Also restrict the groupby aggregation to the measure columns. With more than one
dimension the frame still holds the individual categorical columns, and
aggregating those raised `TypeError: 'Categorical' with dtype category does not
support reduction 'mean'`, breaking multi-dimension dot plots.

Adds `tests/test_dotplot.py` over sparse and dense inputs, checking the values
against a manual groupby -- the aggregator had no coverage at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6uSHPpkfbneXYsprQ3wJ6
anndata >= 0.13 exposes ``None`` in ``layers`` as an alias for ``X``, so every call
site that iterates the mapping directly sees a layer that is not one. The parquet
writer then does os.path.join(..., "layers", None) and raises TypeError, and the
zarr writer writes a layer literally named "None" that duplicates X.

Route the six call sites through one ``layer_names`` helper so the bug class is gone
rather than the instances.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYsKhgGuNCFtxqFhSLGtCK
anndata labels what it writes with an encoding-type, and several of those are
groups holding several nodes -- a categorical (codes + categories since 0.8), a
nullable string, a nullable integer. The backed readers assume every obs/var node
answers node[...], so such a column could not be read at all, and a var index
written as a nullable string took the whole store down.

Decode anything carrying an encoding-type with read_elem, which handles all of
them for h5py and zarr alike, and drop the two remaining calls into the private
anndata._io.zarr.read_attribute for the same reason.

Also skip the obs index in dataset_schema (it is not a column) and tolerate
DataFrame-valued obsm, which has no .shape when the store is read backed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYsKhgGuNCFtxqFhSLGtCK
Removing the zarr<3 pin is not enough on its own: zarr 3 dropped attribute access
to group members, so ZarrDataset needs a small adapter, and the vendored 2021-era
copy of anndata's zarr writer uses the v2 creation API throughout.

Delete that copy (250 lines) and write through anndata.io.write_elem instead. It
is what anndata itself uses, it tracks the encoding versions the readers expect,
and it removes the duplicated encoder entirely.

Reads keep going through the caller's fsspec mapper, which zarr 3 still accepts.
Writes take the path, because zarr creates the directories it writes into and an
fsspec LocalFileSystem mapper does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYsKhgGuNCFtxqFhSLGtCK
An octopus merge refuses the moment any pair of branches conflicts, and these
touch the same files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent breakages, both from pandas 3 defaults.

pandas 3 backs string columns with ArrowStringArray, which the JSON encoder
recurses into until it raises "OverflowError: Maximum recursion level reached".
Every categorical obs column hits this, so `prepare_data --format jsonl` cannot
write a dataset at all. Coerce extension arrays in `dumps` itself rather than at
each call site, so the next serialised column does not reintroduce it.

Separately, `test_de_4_groups` builds its fixture with Series.replace on a
categorical, which pandas 3 refuses because the new values are not existing
categories. rename_categories expresses what the test means.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYsKhgGuNCFtxqFhSLGtCK
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cirrocumulus/client` symlinks to gitignored `build/`, so installing from git
gives a server with no UI and consumers cannot declare cirro as a dependency.
CI now builds the client and attaches a wheel to a GitHub Release.

Drops `scripts/fetch_client.py`, which scavenged the client out of a published
upstream wheel. That shortcut also pinned us to upstream's client and forbade
any change under `src/`. FORK.md's claim that Euler has no node module was
wrong: `node-js/22.4.0` under `stack/2025-06` builds it fine.

`scripts/check_wheel.py` is the gate -- an empty `build/` otherwise yields a
client-less wheel with no error at all, only a 404 on `/` at runtime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQTJRwSGPC77SCzqraZ92T
Both restated facts the other already owned -- the release section walked
through the workflow file step by step, and the docstring re-explained the
silent-empty-build failure that FORK.md now covers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQTJRwSGPC77SCzqraZ92T
The same `dict(values=..., categories=series.cat.categories.values)` construct
appears twice; only the jsonl_io one was coerced. Under pandas 3 the categories
are an ArrowStringArray, so every categorical colour-by returned 500 and the UI
just said "Unable to retrieve data". The `dumps` wrapper cannot catch this one:
the array sits three levels down and the wrapper only walks the top level.

Test fails without the fix on the bundled pbmc3k data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQTJRwSGPC77SCzqraZ92T
The row described only the jsonl_io site; the /api/data one is what actually
blocked the viewer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQTJRwSGPC77SCzqraZ92T
# Conflicts:
#	cirrocumulus/anndata_util.py
#	tests/test_prepare_data.py
# Conflicts:
#	cirrocumulus/zarr_output.py
# Conflicts:
#	cirrocumulus/jsonl_io.py
@Marius1311 Marius1311 closed this Aug 26, 2026
@Marius1311 Marius1311 reopened this Aug 26, 2026
@Marius1311
Marius1311 merged commit 8425a85 into main Aug 27, 2026
2 of 3 checks passed
@Marius1311
Marius1311 deleted the integration branch August 27, 2026 06:35
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