Skip to content

Commit 99787e7

Browse files
committed
Fix
1 parent 4361fc3 commit 99787e7

8 files changed

Lines changed: 43 additions & 32 deletions

File tree

‎docs/user_guide/python_package/c_data_preparation_api/07_connectivities_knn_graph.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ validator, so both pay the same cost:
169169
- **Row/col order mismatch**: graph edges connect the wrong cells (hard to detect visually).
170170
- **Negative weights, asymmetry, directionality, or self-edges**: rejected
171171
instead of transformed.
172-
- **Sparse stored zeros or duplicate coordinates**: rejected instead of
173-
coalesced or discarded.
172+
- **Duplicate sparse coordinates**: rejected instead of coalesced.
173+
- **Sparse stored zeros**: not edges; dropped, as if they were not stored.
174174
- **Very dense graphs**: huge `nnz` → export time and output size explode.
175175
- **Disconnected or empty graphs**: valid when the exact matrix contract holds.
176176

‎docs/user_guide/python_package/h_developer_docs/08_export_format_spec_and_invariants.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,9 @@ Three aligned arrays of equal length:
220220
Float64 weight
221221
- the source graph must already be finite, non-negative, exactly symmetric in
222222
topology and weight, and exactly zero on the diagonal
223-
- sparse inputs must contain neither stored zeros nor duplicate coordinates;
224-
producers reject rather than reinterpret them
223+
- sparse inputs must not contain duplicate coordinates, which producers
224+
reject rather than coalesce; a stored zero is not an edge, and producers and
225+
the web app's AnnData reader drop it
225226

226227
---
227228

‎docs/user_guide/r_package/c_data_preparation_api/07_connectivities_knn_graph.md‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ In the Cellucid ecosystem, this is typically a KNN/SNN graph derived from:
2424
- topology and weights must be exactly symmetric
2525
- every diagonal value must be exactly `0`
2626
- the `Matrix` package must be installed when the input is sparse
27-
- sparse matrices must not contain stored zero entries or duplicate
28-
coordinates
27+
- sparse matrices must not contain duplicate coordinates; a stored zero is
28+
not an edge and is dropped
2929

3030
```{note}
3131
If you do not need connectivity features, skip this entirely. Exports load fine without it.
@@ -141,8 +141,6 @@ A dense `(n_cells, n_cells)` matrix becomes huge quickly. Prefer sparse matrices
141141
install `Matrix`, or pass a dense base R matrix.
142142
- `Connectivity matrix shape must exactly match the cell axis: expected (…), got
143143
(…).` → you have an ordering or subset mismatch.
144-
- `Sparse connectivity storage must omit zero-weight coordinates.` → drop
145-
explicit zeros with `Matrix::drop0(conn)`.
146144
- `Connectivity values must be real numeric or logical values.` → the sparse
147145
matrix is not a real (`dMatrix`), logical (`lMatrix`), or pattern (`nMatrix`)
148146
`Matrix`; convert it first.

‎docs/user_guide/r_package/g_api_reference_coverage/02_error_messages_and_exceptions_document_patterns.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ Neighbouring messages cover the rest of the graph contract:
358358
`Connectivity contains duplicate sparse coordinates at (…)`.
359359

360360
Fix:
361-
- symmetrize the graph and drop stored zeros before the call
361+
- symmetrize the graph before the call
362362

363363
Docs:
364364
- {doc}`../c_data_preparation_api/07_connectivities_knn_graph`

‎docs/user_guide/web_app/p_developer_docs/09_data_loading_pipeline_and_caching.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,12 @@ Behavior:
213213
- A catalog then behaves like `local-demo` (loads `datasets.json` +
214214
`dataset_identity.json`). A file is opened by the AnnData reader worker with
215215
HTTP range requests (`HttpRangeSource`: synchronous single-range XHR in the
216-
worker, 1 MiB pages, read-ahead that grows on forward passes) and served
217-
through `h5ad://` / `zarr://` like a local file.
216+
worker, 256 KiB pages, read-ahead that grows on forward passes to 16 MiB)
217+
and served through `h5ad://` / `zarr://` like a local file. A cell-major
218+
(CSR) matrix is indexed by gene when the first gene is shown, not when the
219+
file opens, because that index reads the matrix's whole index array: over
220+
the network, opening downloads only what the first view draws. A local file
221+
starts the index at once, in the background.
218222
- Requests carry only a single `Range: bytes=a-b` header, which needs no CORS
219223
preflight. File sizes come from `HEAD` or the provider, because many hosts
220224
do not expose `Content-Range`.

‎src/cellucid/connectivity_contract.py‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,13 @@ def _validate_numeric_dtype(dtype: np.dtype[Any]) -> None:
105105
raise ValueError("Connectivity values must be numeric or boolean.")
106106

107107

108-
def _validated_float64_weights(
109-
values: np.ndarray,
110-
*,
111-
reject_zero: bool,
112-
) -> np.ndarray:
108+
def _validated_float64_weights(values: np.ndarray) -> np.ndarray:
113109
"""Validate weights and return their exact little-endian Float64 form."""
114110
_validate_numeric_dtype(values.dtype)
115111
if not np.all(np.isfinite(values)):
116112
raise ValueError("Connectivity values must all be finite.")
117113
if np.any(values < 0):
118114
raise ValueError("Connectivity values must all be non-negative.")
119-
if reject_zero and np.any(values == 0):
120-
raise ValueError("Sparse connectivity must not contain stored zero entries.")
121115

122116
with np.errstate(over="ignore", invalid="ignore"):
123117
weights = values.astype(little_endian_payload_dtype(np.float64), copy=False)
@@ -202,10 +196,13 @@ def _sparse_upper_pairs(
202196

203197
rows = np.asarray(coordinates.row, dtype=np.int64)
204198
columns = np.asarray(coordinates.col, dtype=np.int64)
205-
weights = _validated_float64_weights(
206-
np.asarray(coordinates.data),
207-
reject_zero=True,
208-
)
199+
weights = _validated_float64_weights(np.asarray(coordinates.data))
200+
# A stored zero is not an edge: it is dropped, as if it were not stored.
201+
stored = weights != 0
202+
if not np.all(stored):
203+
rows = rows[stored]
204+
columns = columns[stored]
205+
weights = weights[stored]
209206

210207
if np.any(rows == columns):
211208
raise ValueError("Connectivity diagonal values must all be exactly zero.")
@@ -230,8 +227,7 @@ def _dense_upper_pairs(
230227
if matrix.ndim != 2:
231228
raise ValueError(f"Connectivity matrix must be two-dimensional; got shape {matrix.shape}.")
232229
weights_matrix = _validated_float64_weights(
233-
matrix,
234-
reject_zero=False,
230+
matrix
235231
)
236232

237233
if np.any(np.diag(weights_matrix) != 0):

‎src/cellucid/prepare_data/_generation.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,8 @@ def _prepare_generation(
432432
Exact weighted undirected graph, shape ``(n_cells, n_cells)``. Values
433433
must be finite and non-negative, the topology and weights must be
434434
exactly symmetric, and the diagonal must be zero. Sparse inputs must
435-
not contain duplicate coordinates or stored zero entries.
435+
not contain duplicate coordinates; a stored zero is not an edge and is
436+
dropped.
436437
out_dir : Path
437438
Staging directory that receives the candidate generation. It is always
438439
supplied by :func:`prepare` and must be absent or empty.

‎tests/test_connectivity_contract.py‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,27 @@ def test_invalid_connectivity_is_rejected_without_reinterpretation(
130130

131131

132132
@pytest.mark.parametrize("sparse_format", ["coo", "csr", "csc", "lil", "bsr"])
133-
def test_sparse_stored_zero_entries_are_rejected(sparse_format: str) -> None:
134-
row = np.array([0, 1], dtype=np.int32)
135-
column = np.array([1, 0], dtype=np.int32)
133+
def test_sparse_stored_zero_entries_are_not_edges(sparse_format: str) -> None:
134+
# A stored zero is dropped, as if it were not stored: files written by
135+
# other tools keep them, and they carry no edge.
136+
row = np.array([0, 1, 0, 2], dtype=np.int32)
137+
column = np.array([1, 0, 2, 0], dtype=np.int32)
136138
candidate = sparse.coo_matrix(
137-
(np.zeros(2, dtype=np.float64), (row, column)),
138-
shape=(2, 2),
139+
(np.array([0.5, 0.5, 0.0, 0.0]), (row, column)),
140+
shape=(3, 3),
139141
).asformat(sparse_format)
140142

141-
with pytest.raises(ValueError, match="stored zero"):
142-
validate_connectivity_edges(candidate, n_cells=2)
143+
edges = validate_connectivity_edges(candidate, n_cells=3)
144+
assert edges.n_edges == 1
145+
assert edges.sources.tolist() == [0]
146+
assert edges.destinations.tolist() == [1]
147+
assert edges.weights.tolist() == [0.5]
148+
149+
only_zeros = sparse.coo_matrix(
150+
(np.zeros(2), (row[:2], column[:2])),
151+
shape=(3, 3),
152+
).asformat(sparse_format)
153+
assert validate_connectivity_edges(only_zeros, n_cells=3).n_edges == 0
143154

144155

145156
@pytest.mark.parametrize("sparse_format", ["coo", "csr", "csc", "lil", "bsr"])

0 commit comments

Comments
 (0)