diffbio 0.1.0__py3-none-any.whl
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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Splatter-style differentiable single-cell count simulator.
|
|
2
|
+
|
|
3
|
+
This module provides a fully differentiable implementation of the Splatter
|
|
4
|
+
simulation algorithm (Zappia et al., 2017) using a Gamma-Poisson model with
|
|
5
|
+
learnable parameters. The simulator generates realistic scRNA-seq count
|
|
6
|
+
matrices with group-specific differential expression, batch effects, and
|
|
7
|
+
expression-dependent dropout.
|
|
8
|
+
|
|
9
|
+
Key techniques:
|
|
10
|
+
- Gamma-distributed gene means with softplus-parameterized learnable logits
|
|
11
|
+
- LogNormal library sizes for cell-level sequencing depth variation
|
|
12
|
+
- Soft group assignments via learnable logits and softmax
|
|
13
|
+
- Logistic dropout model as a function of mean expression
|
|
14
|
+
- Continuous relaxation of Poisson sampling for differentiability
|
|
15
|
+
|
|
16
|
+
Applications: Benchmarking single-cell methods, data augmentation for
|
|
17
|
+
downstream analysis, parameter estimation via gradient-based optimization.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
from datarax.core.config import OperatorConfig
|
|
27
|
+
from datarax.core.operator import OperatorModule
|
|
28
|
+
from flax import nnx
|
|
29
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
30
|
+
|
|
31
|
+
from diffbio.constants import EPSILON
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class _SimulationSizeConfig:
|
|
38
|
+
"""Cell, gene, group, and batch sizing for the simulator."""
|
|
39
|
+
|
|
40
|
+
n_cells: int = 500
|
|
41
|
+
n_genes: int = 200
|
|
42
|
+
n_groups: int = 3
|
|
43
|
+
n_batches: int = 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class _SimulationDistributionConfig:
|
|
48
|
+
"""Sampling distributions for means, library sizes, and DE effects."""
|
|
49
|
+
|
|
50
|
+
mean_shape: float = 0.6
|
|
51
|
+
mean_rate: float = 0.3
|
|
52
|
+
lib_loc: float = 11.0
|
|
53
|
+
lib_scale: float = 0.2
|
|
54
|
+
de_prob: float = 0.1
|
|
55
|
+
de_fac_loc: float = 0.1
|
|
56
|
+
de_fac_scale: float = 0.4
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class _SimulationDropoutConfig:
|
|
61
|
+
"""Expression-dependent dropout parameters."""
|
|
62
|
+
|
|
63
|
+
dropout_mid: float = -1.0
|
|
64
|
+
dropout_shape: float = -0.5
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class SimulationConfig(
|
|
69
|
+
_SimulationSizeConfig,
|
|
70
|
+
_SimulationDistributionConfig,
|
|
71
|
+
_SimulationDropoutConfig,
|
|
72
|
+
OperatorConfig,
|
|
73
|
+
):
|
|
74
|
+
"""Configuration for DifferentiableSimulator."""
|
|
75
|
+
|
|
76
|
+
def __post_init__(self) -> None:
|
|
77
|
+
"""Set stochastic defaults and validate simulation assumptions."""
|
|
78
|
+
object.__setattr__(self, "stochastic", True)
|
|
79
|
+
if self.stream_name is None:
|
|
80
|
+
object.__setattr__(self, "stream_name", "sample")
|
|
81
|
+
if self.n_cells <= 0:
|
|
82
|
+
raise ValueError("n_cells must be positive")
|
|
83
|
+
if self.n_genes <= 0:
|
|
84
|
+
raise ValueError("n_genes must be positive")
|
|
85
|
+
if self.n_groups <= 0:
|
|
86
|
+
raise ValueError("n_groups must be positive")
|
|
87
|
+
if self.n_groups > self.n_cells:
|
|
88
|
+
raise ValueError("n_groups cannot exceed n_cells for even cell-group assignment")
|
|
89
|
+
if self.n_batches <= 0:
|
|
90
|
+
raise ValueError("n_batches must be positive")
|
|
91
|
+
if self.n_batches > self.n_cells:
|
|
92
|
+
raise ValueError("n_batches cannot exceed n_cells for even batch assignment")
|
|
93
|
+
if self.mean_shape <= 0.0:
|
|
94
|
+
raise ValueError("mean_shape must be positive")
|
|
95
|
+
if self.mean_rate <= 0.0:
|
|
96
|
+
raise ValueError("mean_rate must be positive")
|
|
97
|
+
if self.lib_scale < 0.0:
|
|
98
|
+
raise ValueError("lib_scale must be non-negative")
|
|
99
|
+
if not 0.0 <= self.de_prob <= 1.0:
|
|
100
|
+
raise ValueError("de_prob must be in [0, 1]")
|
|
101
|
+
if self.de_fac_scale < 0.0:
|
|
102
|
+
raise ValueError("de_fac_scale must be non-negative")
|
|
103
|
+
if self.dropout_shape >= 0.0:
|
|
104
|
+
raise ValueError("dropout_shape must be negative to model lower-expression dropout")
|
|
105
|
+
super().__post_init__()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DifferentiableSimulator(OperatorModule):
|
|
109
|
+
"""Splatter-style differentiable single-cell count simulator.
|
|
110
|
+
|
|
111
|
+
Generates realistic scRNA-seq count matrices following the Splatter
|
|
112
|
+
generative model (Zappia et al., 2017), with all steps implemented
|
|
113
|
+
as differentiable JAX operations.
|
|
114
|
+
|
|
115
|
+
Algorithm:
|
|
116
|
+
1. Gene means: softplus-transformed learnable logits, scaled by
|
|
117
|
+
Gamma(shape, rate) random perturbation.
|
|
118
|
+
2. Cell library sizes: LogNormal(lib_loc, lib_scale) sampling.
|
|
119
|
+
3. Group assignments: cells divided evenly across groups, with
|
|
120
|
+
learnable group logits enabling soft assignment.
|
|
121
|
+
4. DE fold-changes: per-group per-gene LogNormal fold-changes,
|
|
122
|
+
masked by a Bernoulli(de_prob) DE indicator.
|
|
123
|
+
5. Cell means: lib_sizes * gene_means * group_fold_change * batch_effect.
|
|
124
|
+
6. Batch effects: exp(learnable batch_shift) multiplicative scaling.
|
|
125
|
+
7. Dropout: sigmoid-based keep probability as function of log(cell_means).
|
|
126
|
+
8. Counts: cell_means * keep_prob (continuous relaxation of Poisson).
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
config: SimulationConfig with model parameters.
|
|
130
|
+
rngs: Flax NNX random number generators.
|
|
131
|
+
name: Optional operator name.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> config = SimulationConfig(n_cells=100, n_genes=50, n_groups=2)
|
|
135
|
+
>>> sim = DifferentiableSimulator(config, rngs=nnx.Rngs(0, sample=1))
|
|
136
|
+
>>> rng = jax.random.key(0)
|
|
137
|
+
>>> rp = sim.generate_random_params(rng, {})
|
|
138
|
+
>>> result, state, meta = sim.apply({}, {}, None, random_params=rp)
|
|
139
|
+
>>> result["counts"].shape
|
|
140
|
+
(100, 50)
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
config: SimulationConfig,
|
|
146
|
+
*,
|
|
147
|
+
rngs: nnx.Rngs | None = None,
|
|
148
|
+
name: str | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Initialize the differentiable simulator.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
config: Simulation configuration.
|
|
154
|
+
rngs: Random number generators for parameter initialization.
|
|
155
|
+
name: Optional operator name.
|
|
156
|
+
"""
|
|
157
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
158
|
+
|
|
159
|
+
safe_rngs = rngs or nnx.Rngs(0)
|
|
160
|
+
|
|
161
|
+
# Learnable gene-mean logits: softplus maps these to positive means
|
|
162
|
+
key = safe_rngs.params()
|
|
163
|
+
init_logits = jax.random.normal(key, (config.n_genes,)) * 0.5
|
|
164
|
+
self.gene_means_logits = nnx.Param(init_logits)
|
|
165
|
+
|
|
166
|
+
# Learnable group logits for soft cell-group assignment
|
|
167
|
+
key = safe_rngs.params()
|
|
168
|
+
init_group_logits = jax.random.normal(key, (config.n_groups,)) * 0.1
|
|
169
|
+
self.group_logits = nnx.Param(init_group_logits)
|
|
170
|
+
|
|
171
|
+
# Learnable batch shift (additive on log scale)
|
|
172
|
+
self.batch_shift = nnx.Param(jnp.zeros(config.n_batches))
|
|
173
|
+
|
|
174
|
+
def generate_random_params(
|
|
175
|
+
self,
|
|
176
|
+
rng: jax.Array,
|
|
177
|
+
data_shapes: PyTree,
|
|
178
|
+
) -> dict[str, jax.Array]:
|
|
179
|
+
"""Generate random keys for all stochastic sampling steps.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
rng: JAX random key.
|
|
183
|
+
data_shapes: PyTree with shapes (unused, kept for interface).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dictionary of JAX random keys for each sampling step.
|
|
187
|
+
"""
|
|
188
|
+
keys = jax.random.split(rng, 6)
|
|
189
|
+
return {
|
|
190
|
+
"gene_means_key": keys[0],
|
|
191
|
+
"lib_sizes_key": keys[1],
|
|
192
|
+
"group_key": keys[2],
|
|
193
|
+
"de_mask_key": keys[3],
|
|
194
|
+
"de_fold_key": keys[4],
|
|
195
|
+
"poisson_key": keys[5],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
def _sample_gene_means(
|
|
199
|
+
self,
|
|
200
|
+
key: jax.Array,
|
|
201
|
+
) -> Float[Array, "n_genes"]:
|
|
202
|
+
"""Sample gene means from Gamma distribution scaled by learnable logits.
|
|
203
|
+
|
|
204
|
+
The base gene means come from softplus(learnable_logits), then are
|
|
205
|
+
perturbed by Gamma(shape, 1) / rate to maintain the Splatter prior.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
key: JAX random key for Gamma sampling.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Positive gene mean expression levels of shape (n_genes,).
|
|
212
|
+
"""
|
|
213
|
+
config = self.config
|
|
214
|
+
|
|
215
|
+
# Learnable base means via softplus (always positive)
|
|
216
|
+
base_means = jax.nn.softplus(self.gene_means_logits[...])
|
|
217
|
+
|
|
218
|
+
# Stochastic Gamma perturbation: gamma(shape) / rate
|
|
219
|
+
gamma_samples = jax.random.gamma(key, config.mean_shape, shape=(config.n_genes,))
|
|
220
|
+
gamma_factor = gamma_samples / (config.mean_rate + EPSILON)
|
|
221
|
+
|
|
222
|
+
# Combine learnable and stochastic components
|
|
223
|
+
return base_means * gamma_factor + EPSILON
|
|
224
|
+
|
|
225
|
+
def _sample_library_sizes(
|
|
226
|
+
self,
|
|
227
|
+
key: jax.Array,
|
|
228
|
+
) -> Float[Array, "n_cells"]:
|
|
229
|
+
"""Sample cell library sizes from LogNormal distribution.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
key: JAX random key for Normal sampling.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Positive library sizes of shape (n_cells,).
|
|
236
|
+
"""
|
|
237
|
+
config = self.config
|
|
238
|
+
log_lib = config.lib_loc + config.lib_scale * jax.random.normal(key, (config.n_cells,))
|
|
239
|
+
return jnp.exp(log_lib)
|
|
240
|
+
|
|
241
|
+
def _assign_groups(
|
|
242
|
+
self,
|
|
243
|
+
key: jax.Array,
|
|
244
|
+
) -> tuple[Int[Array, "n_cells"], Float[Array, "n_cells n_groups"]]:
|
|
245
|
+
"""Assign cells to groups using even division with learnable logits.
|
|
246
|
+
|
|
247
|
+
Cells are divided evenly across groups. The soft assignment
|
|
248
|
+
probabilities are derived from learnable group logits via softmax,
|
|
249
|
+
enabling gradient flow through group membership.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
key: JAX random key (unused, kept for interface consistency).
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Tuple of (hard_labels, soft_assignments) where:
|
|
256
|
+
- hard_labels: Integer group labels of shape (n_cells,).
|
|
257
|
+
- soft_assignments: Soft probabilities of shape (n_cells, n_groups).
|
|
258
|
+
"""
|
|
259
|
+
config = self.config
|
|
260
|
+
|
|
261
|
+
# Even division of cells across groups
|
|
262
|
+
cells_per_group = config.n_cells // config.n_groups
|
|
263
|
+
hard_labels = jnp.repeat(jnp.arange(config.n_groups), cells_per_group)
|
|
264
|
+
# Handle remainder cells
|
|
265
|
+
remainder = config.n_cells - len(hard_labels)
|
|
266
|
+
if remainder > 0:
|
|
267
|
+
extra = jnp.full(remainder, config.n_groups - 1)
|
|
268
|
+
hard_labels = jnp.concatenate([hard_labels, extra])
|
|
269
|
+
|
|
270
|
+
# Soft assignments from learnable group logits
|
|
271
|
+
group_probs = jax.nn.softmax(self.group_logits[...]) # (n_groups,)
|
|
272
|
+
# Expand to full soft assignment matrix via one-hot weighting
|
|
273
|
+
one_hot = jax.nn.one_hot(hard_labels, config.n_groups) # (n_cells, n_groups)
|
|
274
|
+
# Scale by learnable logits for gradient flow
|
|
275
|
+
soft_assignments_full = one_hot * group_probs[None, :] # (n_cells, n_groups)
|
|
276
|
+
soft_assignments_full = soft_assignments_full / (
|
|
277
|
+
jnp.sum(soft_assignments_full, axis=-1, keepdims=True) + EPSILON
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return hard_labels, soft_assignments_full
|
|
281
|
+
|
|
282
|
+
def _compute_de_fold_changes(
|
|
283
|
+
self,
|
|
284
|
+
de_mask_key: jax.Array,
|
|
285
|
+
de_fold_key: jax.Array,
|
|
286
|
+
) -> tuple[Float[Array, "n_groups n_genes"], Float[Array, "n_groups n_genes"]]:
|
|
287
|
+
"""Compute per-group DE fold-changes with Bernoulli masking.
|
|
288
|
+
|
|
289
|
+
For each group, a subset of genes (determined by de_prob) receives
|
|
290
|
+
a LogNormal fold-change; non-DE genes have fold-change 1.0.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
de_mask_key: JAX random key for DE mask sampling.
|
|
294
|
+
de_fold_key: JAX random key for fold-change sampling.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Tuple of (fold_changes, de_mask) where:
|
|
298
|
+
- fold_changes: Multiplicative fold-changes (n_groups, n_genes).
|
|
299
|
+
- de_mask: Binary DE indicator (n_groups, n_genes).
|
|
300
|
+
"""
|
|
301
|
+
config = self.config
|
|
302
|
+
|
|
303
|
+
# Bernoulli mask: which genes are DE in each group
|
|
304
|
+
de_mask = jax.random.bernoulli(
|
|
305
|
+
de_mask_key, config.de_prob, shape=(config.n_groups, config.n_genes)
|
|
306
|
+
).astype(jnp.float32)
|
|
307
|
+
|
|
308
|
+
# LogNormal fold-changes for DE genes
|
|
309
|
+
log_fc = config.de_fac_loc + config.de_fac_scale * jax.random.normal(
|
|
310
|
+
de_fold_key, (config.n_groups, config.n_genes)
|
|
311
|
+
)
|
|
312
|
+
fold_changes_raw = jnp.exp(log_fc)
|
|
313
|
+
|
|
314
|
+
# Non-DE genes get fold-change of 1.0
|
|
315
|
+
fold_changes = jnp.where(de_mask > 0.5, fold_changes_raw, 1.0)
|
|
316
|
+
|
|
317
|
+
return fold_changes, de_mask
|
|
318
|
+
|
|
319
|
+
def _apply_batch_effects(
|
|
320
|
+
self,
|
|
321
|
+
cell_means: Float[Array, "n_cells n_genes"],
|
|
322
|
+
batch_labels: Int[Array, "n_cells"],
|
|
323
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
324
|
+
"""Apply multiplicative batch effects to cell means.
|
|
325
|
+
|
|
326
|
+
Batch effects are exp(learnable_shift), applied multiplicatively.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
cell_means: Pre-batch cell expression means.
|
|
330
|
+
batch_labels: Integer batch assignment per cell.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Batch-corrected cell means of shape (n_cells, n_genes).
|
|
334
|
+
"""
|
|
335
|
+
batch_factors = jnp.exp(self.batch_shift[...])[batch_labels] # (n_cells,)
|
|
336
|
+
return cell_means * batch_factors[:, None]
|
|
337
|
+
|
|
338
|
+
def _apply_dropout(
|
|
339
|
+
self,
|
|
340
|
+
cell_means: Float[Array, "n_cells n_genes"],
|
|
341
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
342
|
+
"""Apply expression-dependent dropout via logistic function.
|
|
343
|
+
|
|
344
|
+
keep_prob = sigmoid(dropout_shape * (log(cell_means) - dropout_mid))
|
|
345
|
+
|
|
346
|
+
At low expression, keep_prob is low (more zeros); at high expression,
|
|
347
|
+
keep_prob approaches 1.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
cell_means: Cell expression means (positive values).
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Dropout-adjusted cell means.
|
|
354
|
+
"""
|
|
355
|
+
config = self.config
|
|
356
|
+
log_means = jnp.log(cell_means + EPSILON)
|
|
357
|
+
keep_prob = jax.nn.sigmoid(config.dropout_shape * (log_means - config.dropout_mid))
|
|
358
|
+
return cell_means * keep_prob
|
|
359
|
+
|
|
360
|
+
def apply(
|
|
361
|
+
self,
|
|
362
|
+
data: PyTree,
|
|
363
|
+
state: PyTree,
|
|
364
|
+
metadata: dict[str, Any] | None,
|
|
365
|
+
random_params: Any = None,
|
|
366
|
+
stats: dict[str, Any] | None = None,
|
|
367
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
368
|
+
"""Simulate a single-cell count matrix.
|
|
369
|
+
|
|
370
|
+
Follows the Splatter generative model with all steps differentiable:
|
|
371
|
+
gene means, library sizes, group DE, batch effects, dropout, and
|
|
372
|
+
Poisson count generation (continuous relaxation).
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
data: Input dictionary (may be empty; existing keys are preserved).
|
|
376
|
+
state: Element state (passed through unchanged).
|
|
377
|
+
metadata: Element metadata (passed through unchanged).
|
|
378
|
+
random_params: Dictionary of JAX random keys from generate_random_params.
|
|
379
|
+
stats: Not used.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Tuple of (output_data, state, metadata) where output_data contains:
|
|
383
|
+
- All original data keys preserved.
|
|
384
|
+
- "counts": Simulated count matrix (n_cells, n_genes).
|
|
385
|
+
- "group_labels": Hard group assignments (n_cells,).
|
|
386
|
+
- "batch_labels": Batch assignments (n_cells,).
|
|
387
|
+
- "gene_means": Per-gene expression means (n_genes,).
|
|
388
|
+
- "de_mask": Binary DE indicator (n_groups, n_genes).
|
|
389
|
+
"""
|
|
390
|
+
rp = random_params or {}
|
|
391
|
+
|
|
392
|
+
# Step 1: Gene means from learnable logits + Gamma perturbation
|
|
393
|
+
gene_means = self._sample_gene_means(rp["gene_means_key"])
|
|
394
|
+
|
|
395
|
+
# Step 2: Library sizes from LogNormal
|
|
396
|
+
lib_sizes = self._sample_library_sizes(rp["lib_sizes_key"])
|
|
397
|
+
|
|
398
|
+
# Step 3: Group assignments
|
|
399
|
+
group_labels, soft_assignments = self._assign_groups(rp["group_key"])
|
|
400
|
+
|
|
401
|
+
# Step 4: DE fold-changes per group
|
|
402
|
+
fold_changes, de_mask = self._compute_de_fold_changes(rp["de_mask_key"], rp["de_fold_key"])
|
|
403
|
+
|
|
404
|
+
# Step 5: Compute cell means = lib_size * gene_mean * group_fold_change
|
|
405
|
+
# Use soft assignments for differentiability:
|
|
406
|
+
# effective_fold = sum_g(soft_assign[c,g] * fold_change[g,:])
|
|
407
|
+
effective_fold = jnp.einsum(
|
|
408
|
+
"cg,gn->cn", soft_assignments, fold_changes
|
|
409
|
+
) # (n_cells, n_genes)
|
|
410
|
+
cell_means = lib_sizes[:, None] * gene_means[None, :] * effective_fold
|
|
411
|
+
|
|
412
|
+
# Step 6: Batch effects
|
|
413
|
+
# Assign cells evenly across batches
|
|
414
|
+
config = self.config
|
|
415
|
+
cells_per_batch = config.n_cells // config.n_batches
|
|
416
|
+
batch_labels = jnp.repeat(jnp.arange(config.n_batches), cells_per_batch)
|
|
417
|
+
remainder = config.n_cells - len(batch_labels)
|
|
418
|
+
if remainder > 0:
|
|
419
|
+
extra = jnp.full(remainder, config.n_batches - 1)
|
|
420
|
+
batch_labels = jnp.concatenate([batch_labels, extra])
|
|
421
|
+
|
|
422
|
+
cell_means = self._apply_batch_effects(cell_means, batch_labels)
|
|
423
|
+
|
|
424
|
+
# Step 7: Dropout
|
|
425
|
+
cell_means = self._apply_dropout(cell_means)
|
|
426
|
+
|
|
427
|
+
# Step 8: Continuous relaxation of Poisson sampling
|
|
428
|
+
# Use the reparameterization: counts ~ Poisson(lambda) ≈ lambda + sqrt(lambda) * noise
|
|
429
|
+
noise = jax.random.normal(rp["poisson_key"], cell_means.shape)
|
|
430
|
+
counts = cell_means + jnp.sqrt(cell_means + EPSILON) * noise
|
|
431
|
+
# Ensure non-negative counts
|
|
432
|
+
counts = jax.nn.relu(counts)
|
|
433
|
+
|
|
434
|
+
# Build output
|
|
435
|
+
output_data = {
|
|
436
|
+
**data,
|
|
437
|
+
"counts": counts,
|
|
438
|
+
"group_labels": group_labels,
|
|
439
|
+
"batch_labels": batch_labels,
|
|
440
|
+
"gene_means": gene_means,
|
|
441
|
+
"de_mask": de_mask,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return output_data, state, metadata
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""SINDy-based gene regulatory network inference.
|
|
2
|
+
|
|
3
|
+
Implements Sparse Identification of Nonlinear Dynamics (SINDy) for
|
|
4
|
+
discovering gene regulatory equations from expression time-series or
|
|
5
|
+
pseudotime-ordered single-cell data.
|
|
6
|
+
|
|
7
|
+
Unlike the GATv2-based ``DifferentiableGRN`` which learns regulatory
|
|
8
|
+
strengths via attention weights, SINDy discovers explicit governing
|
|
9
|
+
equations by fitting sparse coefficient vectors to a polynomial library
|
|
10
|
+
of candidate terms.
|
|
11
|
+
|
|
12
|
+
Algorithm:
|
|
13
|
+
1. Build a polynomial feature library from expression data.
|
|
14
|
+
2. Compute numerical derivatives (finite differences along time axis).
|
|
15
|
+
3. Solve for sparse coefficients via differentiable soft thresholding
|
|
16
|
+
(proximal gradient descent).
|
|
17
|
+
4. The coefficient matrix encodes which genes regulate which.
|
|
18
|
+
|
|
19
|
+
Reference:
|
|
20
|
+
Brunton, Proctor & Kutz (2016). Discovering governing equations from
|
|
21
|
+
data by sparse identification of nonlinear dynamical systems. PNAS.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import jax
|
|
31
|
+
import jax.numpy as jnp
|
|
32
|
+
from datarax.core.config import OperatorConfig
|
|
33
|
+
from datarax.core.operator import OperatorModule
|
|
34
|
+
from flax import nnx
|
|
35
|
+
from jaxtyping import Array, Float, PyTree
|
|
36
|
+
|
|
37
|
+
from diffbio.core import soft_ops
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SINDyGRNConfig(OperatorConfig):
|
|
44
|
+
"""Configuration for SINDy GRN inference.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
n_genes: Number of genes in the expression matrix.
|
|
48
|
+
polynomial_degree: Maximum polynomial degree for the feature library.
|
|
49
|
+
sparsity_threshold: Soft thresholding level for sparse regression.
|
|
50
|
+
Higher values produce sparser coefficient matrices.
|
|
51
|
+
n_iterations: Number of iterative thresholding steps (STRidge).
|
|
52
|
+
ridge_alpha: Ridge regression regularisation parameter.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
n_genes: int = 100
|
|
56
|
+
polynomial_degree: int = 2
|
|
57
|
+
sparsity_threshold: float = 0.1
|
|
58
|
+
n_iterations: int = 10
|
|
59
|
+
ridge_alpha: float = 0.01
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_polynomial_library(
|
|
63
|
+
x: Float[Array, "n_samples n_features"],
|
|
64
|
+
degree: int = 2,
|
|
65
|
+
) -> Float[Array, "n_samples n_library"]:
|
|
66
|
+
"""Build a polynomial feature library from input data.
|
|
67
|
+
|
|
68
|
+
For degree=1, returns x unchanged (linear terms only).
|
|
69
|
+
For degree=2, appends all pairwise products (x_i * x_j for i <= j).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
x: Input data of shape (n_samples, n_features).
|
|
73
|
+
degree: Maximum polynomial degree (1 or 2).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Library matrix with columns for each polynomial term.
|
|
77
|
+
"""
|
|
78
|
+
terms = [x]
|
|
79
|
+
|
|
80
|
+
if degree >= 2:
|
|
81
|
+
n_features = x.shape[1]
|
|
82
|
+
# Upper-triangular indices for pairwise products (i <= j)
|
|
83
|
+
i_idx, j_idx = jnp.triu_indices(n_features)
|
|
84
|
+
quadratic = x[:, i_idx] * x[:, j_idx] # (n_samples, n_pairs)
|
|
85
|
+
terms.append(quadratic)
|
|
86
|
+
|
|
87
|
+
return jnp.concatenate(terms, axis=1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _soft_threshold(
|
|
91
|
+
x: Array,
|
|
92
|
+
threshold: float,
|
|
93
|
+
) -> Array:
|
|
94
|
+
"""Differentiable soft thresholding (proximal operator for L1).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
x: Input array.
|
|
98
|
+
threshold: Threshold level.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Soft-thresholded array: sign(x) * max(|x| - threshold, 0).
|
|
102
|
+
"""
|
|
103
|
+
return soft_ops.sign(x, softness=0.1) * soft_ops.relu(
|
|
104
|
+
soft_ops.abs(x, softness=0.1) - threshold, softness=0.1
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _stridge_solve(
|
|
109
|
+
theta: Float[Array, "n_samples n_library"],
|
|
110
|
+
dx: Float[Array, "n_samples n_genes"],
|
|
111
|
+
*,
|
|
112
|
+
sparsity_threshold: float,
|
|
113
|
+
n_iterations: int,
|
|
114
|
+
ridge_alpha: float,
|
|
115
|
+
) -> Float[Array, "n_library n_genes"]:
|
|
116
|
+
"""Sequentially Thresholded Ridge Regression (STRidge).
|
|
117
|
+
|
|
118
|
+
Iteratively solves ridge regression then soft-thresholds small
|
|
119
|
+
coefficients to promote sparsity.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
theta: Feature library matrix (n_samples, n_library).
|
|
123
|
+
dx: Numerical derivatives (n_samples, n_genes).
|
|
124
|
+
sparsity_threshold: Soft thresholding level.
|
|
125
|
+
n_iterations: Number of threshold iterations.
|
|
126
|
+
ridge_alpha: Ridge regularisation parameter.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Sparse coefficient matrix (n_library, n_genes).
|
|
130
|
+
"""
|
|
131
|
+
n_lib = theta.shape[1]
|
|
132
|
+
regulariser = ridge_alpha * jnp.eye(n_lib)
|
|
133
|
+
|
|
134
|
+
# Initial ridge regression: xi = (Theta^T Theta + alpha*I)^{-1} Theta^T dx
|
|
135
|
+
gram = theta.T @ theta + regulariser
|
|
136
|
+
xi = jnp.linalg.solve(gram, theta.T @ dx)
|
|
137
|
+
|
|
138
|
+
def _threshold_step(
|
|
139
|
+
xi: Float[Array, "n_library n_genes"],
|
|
140
|
+
_: None,
|
|
141
|
+
) -> tuple[Float[Array, "n_library n_genes"], None]:
|
|
142
|
+
return _soft_threshold(xi, sparsity_threshold), None
|
|
143
|
+
|
|
144
|
+
xi, _ = jax.lax.scan(_threshold_step, xi, None, length=n_iterations)
|
|
145
|
+
return xi
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SINDyGRNOperator(OperatorModule):
|
|
149
|
+
"""SINDy-based differentiable gene regulatory network inference.
|
|
150
|
+
|
|
151
|
+
Discovers sparse governing equations for gene expression dynamics
|
|
152
|
+
by fitting a polynomial library to numerical derivatives of expression
|
|
153
|
+
data. The resulting coefficient matrix encodes which genes (and their
|
|
154
|
+
interactions) regulate which target genes.
|
|
155
|
+
|
|
156
|
+
Complements ``DifferentiableGRN`` (GATv2-based) by providing an
|
|
157
|
+
equation-discovery approach rather than an attention-based one.
|
|
158
|
+
|
|
159
|
+
Input data:
|
|
160
|
+
- ``"counts"``: Expression matrix ``(n_timepoints, n_genes)``
|
|
161
|
+
ordered by time or pseudotime.
|
|
162
|
+
|
|
163
|
+
Output adds:
|
|
164
|
+
- ``"grn_coefficients"``: Sparse coefficient matrix
|
|
165
|
+
``(n_library, n_genes)`` encoding regulatory relationships.
|
|
166
|
+
- ``"grn_equations"``: Same as coefficients (alias for clarity).
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
config: SINDyGRNConfig with model parameters.
|
|
170
|
+
rngs: Flax NNX random number generators.
|
|
171
|
+
name: Optional operator name.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
>>> config = SINDyGRNConfig(n_genes=5, polynomial_degree=1)
|
|
175
|
+
>>> op = SINDyGRNOperator(config, rngs=nnx.Rngs(0))
|
|
176
|
+
>>> data = {"counts": time_ordered_expression}
|
|
177
|
+
>>> result, _, _ = op.apply(data, {}, None)
|
|
178
|
+
>>> result["grn_coefficients"].shape
|
|
179
|
+
(5, 5)
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
config: SINDyGRNConfig,
|
|
185
|
+
*,
|
|
186
|
+
rngs: nnx.Rngs | None = None,
|
|
187
|
+
name: str | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Initialize SINDy GRN operator.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
config: SINDy configuration.
|
|
193
|
+
rngs: Random number generators.
|
|
194
|
+
name: Optional operator name.
|
|
195
|
+
"""
|
|
196
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
197
|
+
self.config: SINDyGRNConfig = config
|
|
198
|
+
|
|
199
|
+
def apply(
|
|
200
|
+
self,
|
|
201
|
+
data: PyTree,
|
|
202
|
+
state: PyTree,
|
|
203
|
+
metadata: dict[str, Any] | None,
|
|
204
|
+
random_params: Any = None, # noqa: ARG002
|
|
205
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
206
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
207
|
+
"""Apply SINDy GRN inference.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
data: Dictionary containing:
|
|
211
|
+
- ``"counts"``: Expression matrix ``(n_timepoints, n_genes)``
|
|
212
|
+
state: Element state (passed through).
|
|
213
|
+
metadata: Element metadata (passed through).
|
|
214
|
+
random_params: Unused.
|
|
215
|
+
stats: Unused.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Tuple of (output_data, state, metadata).
|
|
219
|
+
"""
|
|
220
|
+
counts = data["counts"] # (n_timepoints, n_genes)
|
|
221
|
+
cfg = self.config
|
|
222
|
+
|
|
223
|
+
# Compute numerical derivatives (forward differences)
|
|
224
|
+
dx = counts[1:] - counts[:-1] # (n_timepoints - 1, n_genes)
|
|
225
|
+
x_mid = counts[:-1] # Use start-of-interval values
|
|
226
|
+
|
|
227
|
+
# Build polynomial feature library
|
|
228
|
+
theta = build_polynomial_library(x_mid, degree=cfg.polynomial_degree)
|
|
229
|
+
|
|
230
|
+
# Solve sparse regression
|
|
231
|
+
coefficients = _stridge_solve(
|
|
232
|
+
theta,
|
|
233
|
+
dx,
|
|
234
|
+
sparsity_threshold=cfg.sparsity_threshold,
|
|
235
|
+
n_iterations=cfg.n_iterations,
|
|
236
|
+
ridge_alpha=cfg.ridge_alpha,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
{
|
|
241
|
+
**data,
|
|
242
|
+
"grn_coefficients": coefficients,
|
|
243
|
+
"grn_equations": coefficients,
|
|
244
|
+
},
|
|
245
|
+
state,
|
|
246
|
+
metadata,
|
|
247
|
+
)
|