crispyx 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crispyx-0.2.1/src/crispyx.egg-info → crispyx-0.2.2}/PKG-INFO +1 -2
- {crispyx-0.2.1 → crispyx-0.2.2}/pyproject.toml +4 -2
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/__init__.py +1 -6
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_checkpoint.py +75 -3
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_disk.py +4 -5
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_irls.py +82 -7
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_kernels.py +94 -158
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_memory.py +30 -3
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_messages.py +21 -3
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_namespaces.py +9 -15
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_statistics.py +6 -20
- crispyx-0.2.2/src/crispyx/_version.py +5 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/batch.py +0 -6
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/data.py +453 -130
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/de.py +41 -91
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/glm.py +97 -145
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/hvg.py +1 -3
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/pseudobulk.py +81 -81
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/qc.py +10 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/residual.py +4 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/sample.py +4 -0
- {crispyx-0.2.1 → crispyx-0.2.2/src/crispyx.egg-info}/PKG-INFO +1 -2
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx.egg-info/SOURCES.txt +2 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx.egg-info/requires.txt +0 -3
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_convert_format.py +147 -1
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_de_resume.py +2 -2
- crispyx-0.2.2/tests/test_in_memory_input.py +124 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_irls.py +50 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_messages.py +35 -9
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_nb_glm.py +80 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_pseudobulk_memory.py +37 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_stream_on_fast_axis.py +17 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/LICENSE +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/README.md +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/setup.cfg +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_grouping.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_h5codec.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_preflight.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_provenance.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/_size_factors.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/dimred.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/plotting.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx/profiling.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx.egg-info/dependency_links.txt +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/src/crispyx.egg-info/top_level.txt +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_batch_process.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_cgroup_memory_budget.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_compress_h5ad.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_concat_h5ad.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_data_helpers.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_de_edge_cases.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_de_lowexpr_filter.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_dimred.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_disk_dispatch.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_downsample_counts.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_glm_structured.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_groupby_alias.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_hvg.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_memory_dispatch.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_normalisation.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_normalized_effects.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_normalized_effects_se.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_obs_writes.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_pickle.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_plotting.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_preflight_dispatch.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_profiling.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_pseudobulk_batch.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_pseudobulk_profiles.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_qc_parity.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_rank_genes_groups_outputs.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_read_backed.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_residual_effects.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_result_provenance.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_scanpy_parity.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_standardize.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_streaming_control.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_subsample.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_temp_file_cleanup.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_wilcoxon_dispatch.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_wilcoxon_labels.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_wilcoxon_stratified.py +0 -0
- {crispyx-0.2.1 → crispyx-0.2.2}/tests/test_workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crispyx
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Memory-efficient streaming analysis of large-scale CRISPR and Perturb-seq screens on disk-backed AnnData files
|
|
5
5
|
Author: Jin-Hong Du
|
|
6
6
|
License: Modified MIT License
|
|
@@ -130,7 +130,6 @@ Requires-Dist: sphinx-rtd-theme>=1.2; extra == "docs"
|
|
|
130
130
|
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
131
131
|
Requires-Dist: nbsphinx>=0.9; extra == "docs"
|
|
132
132
|
Requires-Dist: ipykernel>=6.0; extra == "docs"
|
|
133
|
-
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "docs"
|
|
134
133
|
Dynamic: license-file
|
|
135
134
|
|
|
136
135
|
# crispyx
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "crispyx"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Memory-efficient streaming analysis of large-scale CRISPR and Perturb-seq screens on disk-backed AnnData files"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
@@ -69,7 +69,6 @@ docs = [
|
|
|
69
69
|
"sphinx-copybutton>=0.5",
|
|
70
70
|
"nbsphinx>=0.9",
|
|
71
71
|
"ipykernel>=6.0",
|
|
72
|
-
"tomli>=2.0; python_version < '3.11'",
|
|
73
72
|
]
|
|
74
73
|
|
|
75
74
|
[project.urls]
|
|
@@ -78,6 +77,9 @@ Documentation = "https://crispyx.readthedocs.io"
|
|
|
78
77
|
Repository = "https://github.com/jinhongdu-lab/crispyx"
|
|
79
78
|
"Bug Tracker" = "https://github.com/jinhongdu-lab/crispyx/issues"
|
|
80
79
|
|
|
80
|
+
[tool.setuptools.dynamic]
|
|
81
|
+
version = {attr = "crispyx._version.__version__"}
|
|
82
|
+
|
|
81
83
|
[tool.setuptools.packages.find]
|
|
82
84
|
where = ["src"]
|
|
83
85
|
|
|
@@ -15,12 +15,7 @@ import os
|
|
|
15
15
|
# Override with NUMBA_THREADING_LAYER=omp if you manage the OMP conflict yourself.
|
|
16
16
|
os.environ.setdefault("NUMBA_THREADING_LAYER", "workqueue")
|
|
17
17
|
|
|
18
|
-
from
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
__version__ = version("crispyx")
|
|
22
|
-
except PackageNotFoundError:
|
|
23
|
-
__version__ = "0.0.0.dev"
|
|
18
|
+
from ._version import __version__
|
|
24
19
|
|
|
25
20
|
# ---------------------------------------------------------------------------
|
|
26
21
|
# Public API re-exports
|
|
@@ -13,7 +13,6 @@ import json
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import shutil
|
|
16
|
-
import warnings
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from typing import TYPE_CHECKING, Callable
|
|
19
18
|
|
|
@@ -21,6 +20,7 @@ import h5py
|
|
|
21
20
|
import numpy as np
|
|
22
21
|
import pandas as pd
|
|
23
22
|
|
|
23
|
+
from . import _messages
|
|
24
24
|
from ._memory import drop_file_cache
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
@@ -304,6 +304,79 @@ def _sha256(array: np.ndarray) -> str:
|
|
|
304
304
|
return hashlib.sha256(np.ascontiguousarray(array).tobytes()).hexdigest()
|
|
305
305
|
|
|
306
306
|
|
|
307
|
+
#: Elements per block when a result matrix is read or rewritten a block of
|
|
308
|
+
#: rows at a time (32 MB of float64), so no step holds a whole result.
|
|
309
|
+
_BLOCK_ELEMENTS = 1 << 22
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _row_blocks(n_rows: int, n_genes: int, chunk_rows: int | None = None):
|
|
313
|
+
"""Slices covering ``n_rows`` rows of ``n_genes`` columns, about
|
|
314
|
+
:data:`_BLOCK_ELEMENTS` elements each. With ``chunk_rows`` (the rows of
|
|
315
|
+
an HDF5 chunk), each slice is whole bands of chunks, so no chunk is read
|
|
316
|
+
twice."""
|
|
317
|
+
step = max(1, _BLOCK_ELEMENTS // max(n_genes, 1))
|
|
318
|
+
if chunk_rows:
|
|
319
|
+
step = max(1, round(step / chunk_rows)) * chunk_rows
|
|
320
|
+
for start in range(0, n_rows, step):
|
|
321
|
+
yield slice(start, min(start + step, n_rows))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _chunk_rows(dataset) -> int | None:
|
|
325
|
+
"""Rows in one HDF5 chunk of ``dataset`` (``None`` when contiguous or
|
|
326
|
+
not an HDF5 dataset)."""
|
|
327
|
+
chunks = getattr(dataset, "chunks", None)
|
|
328
|
+
return chunks[0] if chunks else None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _create_array(group: h5py.Group, name: str, **kwargs) -> h5py.Dataset:
|
|
332
|
+
"""``group.create_dataset`` tagged as an anndata dense array."""
|
|
333
|
+
ds = group.create_dataset(name, **kwargs)
|
|
334
|
+
ds.attrs["encoding-type"] = "array"
|
|
335
|
+
ds.attrs["encoding-version"] = "0.2.0"
|
|
336
|
+
return ds
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _create_unfilled_array(
|
|
340
|
+
group: h5py.Group, name: str, shape: tuple[int, ...], dtype, chunks: tuple[int, ...] | None = None,
|
|
341
|
+
) -> h5py.Dataset:
|
|
342
|
+
"""An anndata dense array whose storage is allocated now but never
|
|
343
|
+
filled: creating it writes nothing (on disk it is a sparse hole until
|
|
344
|
+
written), and the file's layout is final from the start, so filling it
|
|
345
|
+
later only writes data -- a kill mid-write leaves a readable file.
|
|
346
|
+
Unwritten elements read as zero. Contiguous, or in HDF5 ``chunks``
|
|
347
|
+
(every chunk allocated up front, so the layout is still final)."""
|
|
348
|
+
dcpl = h5py.h5p.create(h5py.h5p.DATASET_CREATE)
|
|
349
|
+
dcpl.set_alloc_time(h5py.h5d.ALLOC_TIME_EARLY)
|
|
350
|
+
dcpl.set_fill_time(h5py.h5d.FILL_TIME_NEVER)
|
|
351
|
+
if chunks is not None:
|
|
352
|
+
dcpl.set_chunk(tuple(int(c) for c in chunks))
|
|
353
|
+
return _create_array(group, name, shape=shape, dtype=dtype, dcpl=dcpl)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
#: Bytes in one HDF5 chunk of a result written in column blocks.
|
|
357
|
+
_COLUMN_BLOCK_CHUNK_BYTES = 1 << 20
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _column_block_chunks(n_groups: int, n_genes: int, chunk_size: int) -> tuple[int, int]:
|
|
361
|
+
"""HDF5 chunk shape for a result written one gene chunk (a block of
|
|
362
|
+
``chunk_size`` columns) at a time: ``(rows, chunk_size)``, ~1 MiB.
|
|
363
|
+
|
|
364
|
+
In a row-major contiguous dataset a gene chunk is one small write per
|
|
365
|
+
group row (10,922 x 4.6 KB per layer on a genome-wide screen), each
|
|
366
|
+
straddling partial pages that the filesystem must read before writing.
|
|
367
|
+
On a network filesystem that read is a round trip: on Lustre a
|
|
368
|
+
gene chunk took 219 s to write contiguously even with its pages cached,
|
|
369
|
+
and 918 s when they were evicted between chunks, against 22 s in these
|
|
370
|
+
chunks. On a local disk the two layouts write alike (the chunked one
|
|
371
|
+
slightly faster), so it is used everywhere; reading one row of a large
|
|
372
|
+
result reads its band of chunks (~2 ms locally), and small results are
|
|
373
|
+
read into memory whole anyway.
|
|
374
|
+
"""
|
|
375
|
+
width = max(1, min(int(chunk_size), n_genes))
|
|
376
|
+
rows = max(1, min(n_groups, _COLUMN_BLOCK_CHUNK_BYTES // (width * 8)))
|
|
377
|
+
return rows, width
|
|
378
|
+
|
|
379
|
+
|
|
307
380
|
class ResumableRun:
|
|
308
381
|
"""A result file filled in place, and a progress checkpoint, that outlive
|
|
309
382
|
an interruption.
|
|
@@ -353,10 +426,9 @@ class ResumableRun:
|
|
|
353
426
|
else "it cannot be read"
|
|
354
427
|
)
|
|
355
428
|
if checkpoint is not None and mismatch is not None:
|
|
356
|
-
|
|
429
|
+
_messages.emit(
|
|
357
430
|
f"Not resuming from the checkpoint at {checkpoint_path}: {mismatch}. "
|
|
358
431
|
"Discarding it and starting over.",
|
|
359
|
-
stacklevel=3,
|
|
360
432
|
)
|
|
361
433
|
self.resumed = (
|
|
362
434
|
checkpoint is not None and mismatch is None and _has_datasets(self.work_path, datasets)
|
|
@@ -11,7 +11,6 @@ resource allocator. There is no override parameter analogous to
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import shutil
|
|
14
|
-
import warnings
|
|
15
14
|
from dataclasses import dataclass
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
|
|
@@ -194,25 +193,25 @@ def warn_if_disk_space_low(
|
|
|
194
193
|
``verbose``-gated confirmation alongside this unconditional warning
|
|
195
194
|
without re-running the free-space lookup.
|
|
196
195
|
"""
|
|
196
|
+
from . import _messages # _messages imports this module
|
|
197
|
+
|
|
197
198
|
estimate = assess_bytes(required_bytes, output_path)
|
|
198
199
|
label = f"{context}: " if context else ""
|
|
199
200
|
|
|
200
201
|
if estimate.free_bytes is not None and not estimate.sufficient:
|
|
201
|
-
|
|
202
|
+
_messages.emit(
|
|
202
203
|
f"{label}estimated disk usage ({format_bytes(estimate.required_bytes)}) "
|
|
203
204
|
f"leaves less than {min_free_fraction:.0%} free space at {estimate.path} "
|
|
204
205
|
f"({format_bytes(estimate.free_bytes)} currently free). The operation may fail "
|
|
205
206
|
"with 'No space left on device'. Free up space or point output_path/TMPDIR at a "
|
|
206
207
|
"volume with more room before rerunning.",
|
|
207
|
-
stacklevel=3,
|
|
208
208
|
)
|
|
209
209
|
elif estimate.required_gb > large_file_gb:
|
|
210
|
-
|
|
210
|
+
_messages.emit(
|
|
211
211
|
f"{label}this operation will write approximately "
|
|
212
212
|
f"{format_bytes(estimate.required_bytes)} to disk. "
|
|
213
213
|
"Streaming keeps memory bounded, but the on-disk footprint (and the time to write "
|
|
214
214
|
"it) is not -- make sure the output volume has room before running large batches.",
|
|
215
|
-
stacklevel=3,
|
|
216
215
|
)
|
|
217
216
|
|
|
218
217
|
return estimate
|
|
@@ -30,6 +30,7 @@ from __future__ import annotations
|
|
|
30
30
|
from typing import Literal
|
|
31
31
|
|
|
32
32
|
import numpy as np
|
|
33
|
+
from scipy.linalg import cho_factor, cho_solve
|
|
33
34
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
"EPS",
|
|
@@ -40,11 +41,15 @@ __all__ = [
|
|
|
40
41
|
"eta_floor",
|
|
41
42
|
"gram_batched",
|
|
42
43
|
"irls_weights",
|
|
44
|
+
"PIVOT_FLOOR",
|
|
43
45
|
"precondition_columns",
|
|
46
|
+
"sandwich_variances_2x2",
|
|
44
47
|
"schur_complement",
|
|
45
48
|
"schur_solve",
|
|
46
49
|
"schur_variances",
|
|
50
|
+
"solve_spd_2x2",
|
|
47
51
|
"working_response",
|
|
52
|
+
"wls_solve",
|
|
48
53
|
]
|
|
49
54
|
|
|
50
55
|
#: Division guard. Not a modelling floor -- see the module docstring.
|
|
@@ -161,6 +166,74 @@ def gram_batched(
|
|
|
161
166
|
return out
|
|
162
167
|
|
|
163
168
|
|
|
169
|
+
#: Pivot floor of the 2x2 factorisation: a singular system gives large but
|
|
170
|
+
#: finite values rather than inf or nan.
|
|
171
|
+
PIVOT_FLOOR = 1e-12
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def solve_spd_2x2(a, b, c, r0, r1):
|
|
175
|
+
"""Solve ``[[a, b], [b, c]] x = [r0, r1]`` elementwise -- one system per
|
|
176
|
+
gene when the arguments are arrays -- through its ``LDL'``
|
|
177
|
+
factorisation, whose pivots ``a`` and ``c - b**2 / a`` are floored at
|
|
178
|
+
:data:`PIVOT_FLOOR`. Returns ``(x0, x1)``.
|
|
179
|
+
|
|
180
|
+
Only arithmetic and ufuncs, so a Numba kernel can compile it too.
|
|
181
|
+
"""
|
|
182
|
+
d0 = np.maximum(a, PIVOT_FLOOR)
|
|
183
|
+
lower = b / d0
|
|
184
|
+
d1 = np.maximum(c - lower * b, PIVOT_FLOOR)
|
|
185
|
+
x1 = (r1 - lower * r0) / d1
|
|
186
|
+
return (r0 - b * x1) / d0, x1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def sandwich_variances_2x2(m00, m01, m11, ridge):
|
|
190
|
+
"""Sandwich variances ``e_k' H M H e_k`` of a two-column fit's
|
|
191
|
+
coefficients, for Fisher information ``M = [[m00, m01], [m01, m11]]``
|
|
192
|
+
and ``H = (M + ridge I)^-1``, with ``H e_k`` from :func:`solve_spd_2x2`.
|
|
193
|
+
Returns ``(intercept, second column)``.
|
|
194
|
+
"""
|
|
195
|
+
a, c = m00 + ridge, m11 + ridge
|
|
196
|
+
|
|
197
|
+
def quadratic(v0, v1):
|
|
198
|
+
return v0 * v0 * m00 + 2 * v0 * v1 * m01 + v1 * v1 * m11
|
|
199
|
+
|
|
200
|
+
return (quadratic(*solve_spd_2x2(a, m01, c, 1.0, 0.0)),
|
|
201
|
+
quadratic(*solve_spd_2x2(a, m01, c, 0.0, 1.0)))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def wls_solve(
|
|
205
|
+
x_weighted: np.ndarray, z_weighted: np.ndarray | None = None, ridge: float = 0.0
|
|
206
|
+
) -> tuple[np.ndarray | None, np.ndarray]:
|
|
207
|
+
"""Coefficients and covariance ``(X'WX + ridge I)^-1`` of a weighted
|
|
208
|
+
least-squares fit, from its weighted design ``W^1/2 X`` (and response
|
|
209
|
+
``W^1/2 z``; without one the coefficients are ``None``).
|
|
210
|
+
|
|
211
|
+
A Cholesky factor of the normal matrix when it is positive definite;
|
|
212
|
+
otherwise an SVD of the weighted design, stacked on ``sqrt(ridge) I``,
|
|
213
|
+
gives the minimum-norm solution and the pseudo-inverse with singular
|
|
214
|
+
values below ``matrix_rank``'s tolerance dropped -- without forming a
|
|
215
|
+
normal matrix whose condition number is the design's squared.
|
|
216
|
+
"""
|
|
217
|
+
n_features = x_weighted.shape[1]
|
|
218
|
+
gram = x_weighted.T @ x_weighted
|
|
219
|
+
gram[np.diag_indices(n_features)] += ridge
|
|
220
|
+
try:
|
|
221
|
+
factor = cho_factor(gram, check_finite=False)
|
|
222
|
+
except np.linalg.LinAlgError:
|
|
223
|
+
design, response = x_weighted, z_weighted
|
|
224
|
+
if ridge:
|
|
225
|
+
design = np.vstack([x_weighted, np.sqrt(ridge) * np.eye(n_features)])
|
|
226
|
+
if z_weighted is not None:
|
|
227
|
+
response = np.concatenate([z_weighted, np.zeros(n_features)])
|
|
228
|
+
u, s, vt = np.linalg.svd(design, full_matrices=False)
|
|
229
|
+
keep = s > s.max(initial=0.0) * max(design.shape) * np.finfo(np.float64).eps
|
|
230
|
+
scaled = vt[keep].T / s[keep] # V S^-1
|
|
231
|
+
beta = None if response is None else scaled @ (u[:, keep].T @ response)
|
|
232
|
+
return beta, scaled @ scaled.T
|
|
233
|
+
beta = None if z_weighted is None else cho_solve(factor, x_weighted.T @ z_weighted, check_finite=False)
|
|
234
|
+
return beta, cho_solve(factor, np.eye(n_features), check_finite=False)
|
|
235
|
+
|
|
236
|
+
|
|
164
237
|
def irls_weights(mu: np.ndarray, alpha: np.ndarray | float | None) -> np.ndarray:
|
|
165
238
|
"""Log-link IRLS weights ``mu**2 / V(mu)``.
|
|
166
239
|
|
|
@@ -497,15 +570,17 @@ def schur_variances(
|
|
|
497
570
|
(H^-1)_XX = S^-1
|
|
498
571
|
(H^-1)_GG = D^-1 + D^-1 B' S^-1 B D^-1
|
|
499
572
|
|
|
500
|
-
so
|
|
501
|
-
|
|
502
|
-
|
|
573
|
+
so one solve of each ``S`` against ``[I | B]`` gives the covariate
|
|
574
|
+
variances and the quadratic forms ``B_k' S^-1 B_k`` the group variances
|
|
575
|
+
need.
|
|
503
576
|
"""
|
|
504
577
|
inverse_diagonal = 1.0 / diagonal # (a, p)
|
|
505
|
-
|
|
506
|
-
feature_variance = np.diagonal(schur_inverse, axis1=1, axis2=2)
|
|
507
|
-
|
|
578
|
+
n_genes, dx = schur.shape[:2]
|
|
508
579
|
blocks = np.ascontiguousarray(cross.transpose(2, 1, 0)) # (p, dx, a)
|
|
509
|
-
|
|
580
|
+
# One factorisation of each S, solved for [I | B] together.
|
|
581
|
+
rhs = np.concatenate([np.broadcast_to(np.eye(dx), (n_genes, dx, dx)), blocks], axis=2)
|
|
582
|
+
solved = np.linalg.solve(schur, rhs)
|
|
583
|
+
feature_variance = np.diagonal(solved[:, :, :dx], axis1=1, axis2=2)
|
|
584
|
+
quadratic = (blocks * solved[:, :, dx:]).sum(axis=1) # (p, a)
|
|
510
585
|
group_variance = inverse_diagonal.T + quadratic * inverse_diagonal.T**2
|
|
511
586
|
return feature_variance, group_variance
|