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,166 @@
|
|
|
1
|
+
"""Differentiable read-depth downsampling for count data.
|
|
2
|
+
|
|
3
|
+
Provides a differentiable approximation to binomial read downsampling,
|
|
4
|
+
enabling gradient flow through the downsampling operation via a
|
|
5
|
+
straight-through estimator.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- cell-load/src/cell_load/dataset/_perturbation.py (downsampling logic)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import jax
|
|
18
|
+
import jax.numpy as jnp
|
|
19
|
+
from flax import nnx
|
|
20
|
+
|
|
21
|
+
from datarax.core.config import OperatorConfig
|
|
22
|
+
from datarax.core.operator import OperatorModule
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class DownsamplingConfig(OperatorConfig):
|
|
29
|
+
"""Configuration for ReadDownsampler.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
mode: Downsampling mode. ``"fraction"`` scales by a fixed fraction.
|
|
33
|
+
``"target_depth"`` computes per-cell fraction from target read depth.
|
|
34
|
+
fraction: Fraction of reads to keep when ``mode="fraction"``.
|
|
35
|
+
target_depth: Target total reads per cell when ``mode="target_depth"``.
|
|
36
|
+
apply_log1p: Whether to apply log1p to the downsampled counts.
|
|
37
|
+
is_log1p_input: Whether the input counts are already log1p-transformed.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
mode: str = "fraction"
|
|
41
|
+
fraction: float = 1.0
|
|
42
|
+
target_depth: int | None = None
|
|
43
|
+
apply_log1p: bool = True
|
|
44
|
+
is_log1p_input: bool = True
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
"""Validate configuration."""
|
|
48
|
+
# Downsampling is stochastic
|
|
49
|
+
object.__setattr__(self, "stochastic", True)
|
|
50
|
+
if self.stream_name is None:
|
|
51
|
+
object.__setattr__(self, "stream_name", "downsample")
|
|
52
|
+
super().__post_init__()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ReadDownsampler(OperatorModule):
|
|
56
|
+
"""Differentiable read-depth downsampler.
|
|
57
|
+
|
|
58
|
+
Approximates binomial downsampling using a continuous relaxation that
|
|
59
|
+
allows gradient flow via the straight-through estimator:
|
|
60
|
+
|
|
61
|
+
- **Forward:** ``downsampled = floor(counts * fraction)`` plus a
|
|
62
|
+
stochastic rounding of the remainder.
|
|
63
|
+
- **Backward:** Gradients flow through ``counts * fraction`` (the
|
|
64
|
+
expected value).
|
|
65
|
+
|
|
66
|
+
Handles log1p domain: if input is log1p-transformed, applies expm1
|
|
67
|
+
before downsampling and log1p after.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Downsampling configuration.
|
|
71
|
+
rngs: RNG state for stochastic sampling.
|
|
72
|
+
name: Optional module name.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
config: DownsamplingConfig,
|
|
78
|
+
*,
|
|
79
|
+
rngs: nnx.Rngs | None = None,
|
|
80
|
+
name: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
83
|
+
|
|
84
|
+
def apply(
|
|
85
|
+
self,
|
|
86
|
+
data: dict[str, Any],
|
|
87
|
+
state: dict[str, Any],
|
|
88
|
+
metadata: Any,
|
|
89
|
+
random_params: Any = None, # noqa: ARG002
|
|
90
|
+
stats: Any = None, # noqa: ARG002
|
|
91
|
+
) -> tuple[dict[str, Any], dict[str, Any], Any]:
|
|
92
|
+
"""Apply differentiable downsampling to count data.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
data: Dict with ``"counts"`` key containing expression matrix.
|
|
96
|
+
state: Pipeline state (passed through).
|
|
97
|
+
metadata: Pipeline metadata (passed through).
|
|
98
|
+
random_params: Unused (RNG handled internally).
|
|
99
|
+
stats: Unused.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of (updated_data, state, metadata).
|
|
103
|
+
"""
|
|
104
|
+
counts = data["counts"]
|
|
105
|
+
config: DownsamplingConfig = self.config # type: ignore[assignment]
|
|
106
|
+
|
|
107
|
+
# Invert log1p if needed
|
|
108
|
+
if config.is_log1p_input:
|
|
109
|
+
counts = jnp.expm1(counts)
|
|
110
|
+
|
|
111
|
+
# Compute per-cell fraction
|
|
112
|
+
fraction: float | jnp.ndarray
|
|
113
|
+
if config.mode == "target_depth" and config.target_depth is not None:
|
|
114
|
+
cell_totals = jnp.sum(counts, axis=-1, keepdims=True)
|
|
115
|
+
cell_totals = jnp.maximum(cell_totals, 1.0) # avoid div by zero
|
|
116
|
+
fraction = jnp.minimum(config.target_depth / cell_totals, 1.0)
|
|
117
|
+
else:
|
|
118
|
+
fraction = config.fraction
|
|
119
|
+
|
|
120
|
+
# Straight-through differentiable downsampling
|
|
121
|
+
downsampled = _straight_through_downsample(counts, fraction, self.rngs)
|
|
122
|
+
|
|
123
|
+
# Ensure non-negative
|
|
124
|
+
downsampled = jnp.maximum(downsampled, 0.0)
|
|
125
|
+
|
|
126
|
+
# Re-apply log1p if configured
|
|
127
|
+
if config.apply_log1p:
|
|
128
|
+
downsampled = jnp.log1p(downsampled)
|
|
129
|
+
|
|
130
|
+
return {**data, "counts": downsampled}, state, metadata
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _straight_through_downsample(
|
|
134
|
+
counts: jnp.ndarray,
|
|
135
|
+
fraction: float | jnp.ndarray,
|
|
136
|
+
rngs: nnx.Rngs | None,
|
|
137
|
+
) -> jnp.ndarray:
|
|
138
|
+
"""Downsample counts with straight-through gradient estimator.
|
|
139
|
+
|
|
140
|
+
Forward: ``floor(counts * f) + Bernoulli(counts * f - floor(counts * f))``
|
|
141
|
+
Backward: gradient flows through ``counts * f`` (expected value).
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
counts: Count matrix.
|
|
145
|
+
fraction: Downsampling fraction (scalar or per-cell array).
|
|
146
|
+
rngs: RNG state for Bernoulli sampling.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Downsampled count matrix.
|
|
150
|
+
"""
|
|
151
|
+
expected = counts * fraction
|
|
152
|
+
floored = jnp.floor(expected)
|
|
153
|
+
remainder = expected - floored
|
|
154
|
+
|
|
155
|
+
# Stochastic rounding of remainder
|
|
156
|
+
if rngs is not None and "downsample" in rngs:
|
|
157
|
+
key = rngs.downsample()
|
|
158
|
+
else:
|
|
159
|
+
key = jax.random.key(0)
|
|
160
|
+
|
|
161
|
+
uniform = jax.random.uniform(key, shape=remainder.shape)
|
|
162
|
+
rounded = jnp.where(uniform < remainder, 1.0, 0.0)
|
|
163
|
+
|
|
164
|
+
# Forward: discrete value. Backward: gradient through expected value.
|
|
165
|
+
discrete = floored + rounded
|
|
166
|
+
return expected + jax.lax.stop_gradient(discrete - expected)
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""Enhanced differentiable batch correction operators using MMD and WGAN.
|
|
2
|
+
|
|
3
|
+
This module provides two neural-network-based batch correction strategies:
|
|
4
|
+
|
|
5
|
+
- **DifferentiableMMDBatchCorrection**: Autoencoder with Maximum Mean Discrepancy
|
|
6
|
+
(MMD) regularisation that penalises distributional differences between batches
|
|
7
|
+
in latent space.
|
|
8
|
+
- **DifferentiableWGANBatchCorrection**: Adversarial autoencoder with a Wasserstein
|
|
9
|
+
GAN discriminator that learns batch-invariant latent representations through
|
|
10
|
+
gradient reversal.
|
|
11
|
+
|
|
12
|
+
Design references:
|
|
13
|
+
- scGPT gradient reversal for adversarial batch correction.
|
|
14
|
+
- scVI batch encoding as one-hot or embedding.
|
|
15
|
+
- scGPT domain-specific batch normalisation.
|
|
16
|
+
|
|
17
|
+
Both operators inherit from ``OperatorModule`` and follow the standard
|
|
18
|
+
``apply(data, state, metadata)`` interface for pipeline composability.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
from artifex.generative_models.core.base import MLP
|
|
28
|
+
from artifex.generative_models.core.losses.adversarial import (
|
|
29
|
+
wasserstein_discriminator_loss,
|
|
30
|
+
wasserstein_generator_loss,
|
|
31
|
+
)
|
|
32
|
+
from artifex.generative_models.core.losses.base import reduce_loss
|
|
33
|
+
from artifex.generative_models.core.losses.divergence import maximum_mean_discrepancy
|
|
34
|
+
from datarax.core.config import OperatorConfig
|
|
35
|
+
from datarax.core.operator import OperatorModule
|
|
36
|
+
from flax import nnx
|
|
37
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
38
|
+
|
|
39
|
+
from diffbio.operators._loss_balancing import LossBalancingMixin
|
|
40
|
+
from diffbio.utils.nn_utils import ensure_rngs
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"DifferentiableMMDBatchCorrection",
|
|
46
|
+
"DifferentiableWGANBatchCorrection",
|
|
47
|
+
"MMDBatchCorrectionConfig",
|
|
48
|
+
"WGANBatchCorrectionConfig",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Gradient reversal primitive (JAX custom_vjp)
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@jax.custom_vjp
|
|
58
|
+
def _gradient_reversal(x: jax.Array, scale: float) -> jax.Array:
|
|
59
|
+
"""Pass-through forward, negate gradients backward."""
|
|
60
|
+
return x
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _gradient_reversal_fwd(x: jax.Array, scale: float) -> tuple[jax.Array, float]:
|
|
64
|
+
"""Forward pass for gradient reversal."""
|
|
65
|
+
return x, scale
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _gradient_reversal_bwd(scale: float, grad: jax.Array) -> tuple[jax.Array, None]:
|
|
69
|
+
"""Backward pass: negate and scale the gradient."""
|
|
70
|
+
return (-scale * grad, None)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_gradient_reversal.defvjp(_gradient_reversal_fwd, _gradient_reversal_bwd)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Configs
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class MMDBatchCorrectionConfig(OperatorConfig):
|
|
83
|
+
"""Configuration for MMD-based batch correction.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
n_genes: Number of input genes (features).
|
|
87
|
+
hidden_dim: Width of hidden layers in the autoencoder.
|
|
88
|
+
latent_dim: Dimensionality of the latent space.
|
|
89
|
+
kernel_bandwidth: Bandwidth for the RBF kernel in the MMD loss.
|
|
90
|
+
use_gradnorm: Whether to use GradNormBalancer for multi-task loss
|
|
91
|
+
balancing between reconstruction and MMD losses.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
n_genes: int = 2000
|
|
95
|
+
hidden_dim: int = 128
|
|
96
|
+
latent_dim: int = 64
|
|
97
|
+
kernel_bandwidth: float = 1.0
|
|
98
|
+
use_gradnorm: bool = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class WGANBatchCorrectionConfig(OperatorConfig):
|
|
103
|
+
"""Configuration for WGAN-based batch correction.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
n_genes: Number of input genes (features).
|
|
107
|
+
hidden_dim: Width of hidden layers in the generator autoencoder.
|
|
108
|
+
latent_dim: Dimensionality of the latent space.
|
|
109
|
+
discriminator_hidden_dim: Width of hidden layers in the discriminator.
|
|
110
|
+
use_gradnorm: Whether to use GradNormBalancer for multi-task loss
|
|
111
|
+
balancing between generator and discriminator losses.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
n_genes: int = 2000
|
|
115
|
+
hidden_dim: int = 128
|
|
116
|
+
latent_dim: int = 64
|
|
117
|
+
discriminator_hidden_dim: int = 64
|
|
118
|
+
use_gradnorm: bool = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# MMD batch correction
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class DifferentiableMMDBatchCorrection(LossBalancingMixin, OperatorModule):
|
|
127
|
+
"""Autoencoder batch correction with MMD regularisation.
|
|
128
|
+
|
|
129
|
+
Architecture:
|
|
130
|
+
Encoder MLP maps gene expression to a latent representation, and a
|
|
131
|
+
decoder MLP reconstructs the expression from that latent. The MMD
|
|
132
|
+
loss penalises distributional mismatch between batches in latent
|
|
133
|
+
space so the learned representation becomes batch-invariant.
|
|
134
|
+
|
|
135
|
+
Loss:
|
|
136
|
+
``reconstruction_mse + mmd(latent_batch_0, latent_batch_1, ...)``
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config: MMDBatchCorrectionConfig with model hyper-parameters.
|
|
140
|
+
rngs: Flax NNX random number generators.
|
|
141
|
+
name: Optional operator name.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> config = MMDBatchCorrectionConfig(n_genes=2000)
|
|
145
|
+
>>> op = DifferentiableMMDBatchCorrection(config, rngs=nnx.Rngs(0))
|
|
146
|
+
>>> result, _, _ = op.apply(data, {}, None)
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
config: MMDBatchCorrectionConfig,
|
|
152
|
+
*,
|
|
153
|
+
rngs: nnx.Rngs | None = None,
|
|
154
|
+
name: str | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Initialise encoder and decoder MLPs.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
config: Operator configuration.
|
|
160
|
+
rngs: Random number generators for weight initialisation.
|
|
161
|
+
name: Optional operator name.
|
|
162
|
+
"""
|
|
163
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
164
|
+
|
|
165
|
+
rngs = ensure_rngs(rngs)
|
|
166
|
+
|
|
167
|
+
self.encoder = MLP(
|
|
168
|
+
hidden_dims=[config.hidden_dim, config.hidden_dim, config.latent_dim],
|
|
169
|
+
in_features=config.n_genes,
|
|
170
|
+
activation="relu",
|
|
171
|
+
use_batch_norm=False,
|
|
172
|
+
rngs=rngs,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self.decoder = MLP(
|
|
176
|
+
hidden_dims=[config.hidden_dim, config.hidden_dim, config.n_genes],
|
|
177
|
+
in_features=config.latent_dim,
|
|
178
|
+
activation="relu",
|
|
179
|
+
use_batch_norm=False,
|
|
180
|
+
rngs=rngs,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# -- helpers --------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _encode(
|
|
186
|
+
self, expression: Float[Array, "n_cells n_genes"]
|
|
187
|
+
) -> Float[Array, "n_cells latent_dim"]:
|
|
188
|
+
"""Map expression to latent space.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
expression: Input gene expression matrix.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Latent representation.
|
|
195
|
+
"""
|
|
196
|
+
latent: jax.Array = self.encoder(expression)
|
|
197
|
+
return latent
|
|
198
|
+
|
|
199
|
+
def _decode(
|
|
200
|
+
self, latent: Float[Array, "n_cells latent_dim"]
|
|
201
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
202
|
+
"""Reconstruct expression from latent space.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
latent: Latent representation.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Reconstructed gene expression.
|
|
209
|
+
"""
|
|
210
|
+
reconstruction: jax.Array = self.decoder(latent)
|
|
211
|
+
return reconstruction
|
|
212
|
+
|
|
213
|
+
def _compute_pairwise_mmd(
|
|
214
|
+
self,
|
|
215
|
+
latent: Float[Array, "n_cells latent_dim"],
|
|
216
|
+
batch_labels: Int[Array, "n_cells"],
|
|
217
|
+
) -> Float[Array, ""]:
|
|
218
|
+
"""Compute MMD between batch-0 and non-batch-0 cells in latent space.
|
|
219
|
+
|
|
220
|
+
Uses masked mean embeddings to remain fully JIT-compatible (no boolean
|
|
221
|
+
indexing). For multi-batch data the comparison is batch-0 vs the rest,
|
|
222
|
+
which encourages all batches to align in latent space. When only one
|
|
223
|
+
batch is present the two groups are identical and MMD is near zero.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
latent: Latent representations for all cells.
|
|
227
|
+
batch_labels: Integer batch assignments per cell.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Scalar MMD loss.
|
|
231
|
+
"""
|
|
232
|
+
n_cells = latent.shape[0]
|
|
233
|
+
|
|
234
|
+
# Soft masks (JIT-safe: no boolean indexing)
|
|
235
|
+
is_batch0 = (batch_labels == 0).astype(jnp.float32) # (n_cells,)
|
|
236
|
+
is_other = 1.0 - is_batch0
|
|
237
|
+
|
|
238
|
+
n_batch0 = jnp.maximum(is_batch0.sum(), 1.0)
|
|
239
|
+
n_other = jnp.maximum(is_other.sum(), 1.0)
|
|
240
|
+
|
|
241
|
+
# Weighted latent: zero out cells not in the group, then stack as
|
|
242
|
+
# (1, n_cells, latent_dim) for the MMD function which expects
|
|
243
|
+
# [batch, samples, features].
|
|
244
|
+
latent_batch0 = latent * is_batch0[:, None] # (n_cells, d)
|
|
245
|
+
latent_other = latent * is_other[:, None]
|
|
246
|
+
|
|
247
|
+
# Normalize so the masked-out zeros don't bias the kernel.
|
|
248
|
+
# Scale the active entries up by n_cells / n_active so that the
|
|
249
|
+
# effective sample still fills the (n_cells, d) array.
|
|
250
|
+
latent_batch0 = latent_batch0 * (n_cells / n_batch0)
|
|
251
|
+
latent_other = latent_other * (n_cells / n_other)
|
|
252
|
+
|
|
253
|
+
return maximum_mean_discrepancy(
|
|
254
|
+
latent_batch0[None, ...], # (1, n_cells, d)
|
|
255
|
+
latent_other[None, ...], # (1, n_cells, d)
|
|
256
|
+
kernel_type="rbf",
|
|
257
|
+
kernel_bandwidth=self.config.kernel_bandwidth,
|
|
258
|
+
reduction="mean",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# -- apply ----------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def apply(
|
|
264
|
+
self,
|
|
265
|
+
data: PyTree,
|
|
266
|
+
state: PyTree,
|
|
267
|
+
metadata: dict[str, Any] | None,
|
|
268
|
+
random_params: Any = None,
|
|
269
|
+
stats: dict[str, Any] | None = None,
|
|
270
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
271
|
+
"""Encode, decode, and compute MMD + reconstruction losses.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
data: Dictionary containing:
|
|
275
|
+
- ``"expression"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
276
|
+
- ``"batch_labels"``: Integer batch assignments ``(n_cells,)``
|
|
277
|
+
state: Pipeline state (passed through unchanged).
|
|
278
|
+
metadata: Pipeline metadata (passed through unchanged).
|
|
279
|
+
random_params: Unused.
|
|
280
|
+
stats: Unused.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Tuple of ``(result, state, metadata)`` where *result* contains:
|
|
284
|
+
- ``"expression"``: Original expression
|
|
285
|
+
- ``"batch_labels"``: Original batch labels
|
|
286
|
+
- ``"corrected_expression"``: Decoded (corrected) expression
|
|
287
|
+
- ``"latent"``: Latent representation
|
|
288
|
+
- ``"mmd_loss"``: Scalar MMD loss between batches
|
|
289
|
+
- ``"reconstruction_loss"``: Scalar MSE reconstruction loss
|
|
290
|
+
"""
|
|
291
|
+
expression = data["expression"]
|
|
292
|
+
batch_labels = data["batch_labels"]
|
|
293
|
+
|
|
294
|
+
# Forward pass
|
|
295
|
+
latent = self._encode(expression)
|
|
296
|
+
reconstructed = self._decode(latent)
|
|
297
|
+
|
|
298
|
+
# Losses
|
|
299
|
+
reconstruction_loss = reduce_loss(
|
|
300
|
+
(reconstructed - expression) ** 2,
|
|
301
|
+
reduction="mean",
|
|
302
|
+
)
|
|
303
|
+
mmd_loss = self._compute_pairwise_mmd(latent, batch_labels)
|
|
304
|
+
|
|
305
|
+
result = {
|
|
306
|
+
**data,
|
|
307
|
+
"corrected_expression": reconstructed,
|
|
308
|
+
"latent": latent,
|
|
309
|
+
"mmd_loss": mmd_loss,
|
|
310
|
+
"reconstruction_loss": reconstruction_loss,
|
|
311
|
+
}
|
|
312
|
+
return result, state, metadata
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# WGAN batch correction
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class DifferentiableWGANBatchCorrection(LossBalancingMixin, OperatorModule):
|
|
321
|
+
"""Adversarial autoencoder batch correction with Wasserstein GAN loss.
|
|
322
|
+
|
|
323
|
+
Architecture:
|
|
324
|
+
An encoder (generator) maps gene expression to a batch-invariant
|
|
325
|
+
latent space and a decoder reconstructs the expression. A separate
|
|
326
|
+
discriminator tries to predict the batch label from the latent
|
|
327
|
+
representation. Gradient reversal (a la scGPT) ensures the encoder
|
|
328
|
+
learns to *fool* the discriminator, yielding batch-invariant latents.
|
|
329
|
+
|
|
330
|
+
Losses:
|
|
331
|
+
- ``generator_loss``: Wasserstein generator loss (encoder wants to
|
|
332
|
+
fool the discriminator) plus reconstruction MSE.
|
|
333
|
+
- ``discriminator_loss``: Wasserstein discriminator/critic loss.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
config: WGANBatchCorrectionConfig with model hyper-parameters.
|
|
337
|
+
rngs: Flax NNX random number generators.
|
|
338
|
+
name: Optional operator name.
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> config = WGANBatchCorrectionConfig(n_genes=2000)
|
|
342
|
+
>>> op = DifferentiableWGANBatchCorrection(config, rngs=nnx.Rngs(0))
|
|
343
|
+
>>> result, _, _ = op.apply(data, {}, None)
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
def __init__(
|
|
347
|
+
self,
|
|
348
|
+
config: WGANBatchCorrectionConfig,
|
|
349
|
+
*,
|
|
350
|
+
rngs: nnx.Rngs | None = None,
|
|
351
|
+
name: str | None = None,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Initialise encoder, decoder, and discriminator MLPs.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
config: Operator configuration.
|
|
357
|
+
rngs: Random number generators for weight initialisation.
|
|
358
|
+
name: Optional operator name.
|
|
359
|
+
"""
|
|
360
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
361
|
+
|
|
362
|
+
rngs = ensure_rngs(rngs)
|
|
363
|
+
|
|
364
|
+
self.encoder = MLP(
|
|
365
|
+
hidden_dims=[config.hidden_dim, config.hidden_dim, config.latent_dim],
|
|
366
|
+
in_features=config.n_genes,
|
|
367
|
+
activation="relu",
|
|
368
|
+
use_batch_norm=False,
|
|
369
|
+
rngs=rngs,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
self.decoder = MLP(
|
|
373
|
+
hidden_dims=[config.hidden_dim, config.hidden_dim, config.n_genes],
|
|
374
|
+
in_features=config.latent_dim,
|
|
375
|
+
activation="relu",
|
|
376
|
+
use_batch_norm=False,
|
|
377
|
+
rngs=rngs,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
self.discriminator = MLP(
|
|
381
|
+
hidden_dims=[
|
|
382
|
+
config.discriminator_hidden_dim,
|
|
383
|
+
config.discriminator_hidden_dim,
|
|
384
|
+
1,
|
|
385
|
+
],
|
|
386
|
+
in_features=config.latent_dim,
|
|
387
|
+
activation="relu",
|
|
388
|
+
use_batch_norm=False,
|
|
389
|
+
rngs=rngs,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# -- helpers --------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def _encode(
|
|
395
|
+
self, expression: Float[Array, "n_cells n_genes"]
|
|
396
|
+
) -> Float[Array, "n_cells latent_dim"]:
|
|
397
|
+
"""Map expression to latent space.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
expression: Input gene expression matrix.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Latent representation.
|
|
404
|
+
"""
|
|
405
|
+
latent: jax.Array = self.encoder(expression)
|
|
406
|
+
return latent
|
|
407
|
+
|
|
408
|
+
def _decode(
|
|
409
|
+
self, latent: Float[Array, "n_cells latent_dim"]
|
|
410
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
411
|
+
"""Reconstruct expression from latent space.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
latent: Latent representation.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Reconstructed gene expression.
|
|
418
|
+
"""
|
|
419
|
+
reconstruction: jax.Array = self.decoder(latent)
|
|
420
|
+
return reconstruction
|
|
421
|
+
|
|
422
|
+
def _discriminate(self, latent: Float[Array, "n_cells latent_dim"]) -> Float[Array, "n_cells"]:
|
|
423
|
+
"""Compute discriminator (critic) scores from latent representations.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
latent: Latent representation.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Per-cell scalar critic scores.
|
|
430
|
+
"""
|
|
431
|
+
scores: jax.Array = self.discriminator(latent)
|
|
432
|
+
return scores.squeeze(-1) # (n_cells,)
|
|
433
|
+
|
|
434
|
+
# -- apply ----------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def apply(
|
|
437
|
+
self,
|
|
438
|
+
data: PyTree,
|
|
439
|
+
state: PyTree,
|
|
440
|
+
metadata: dict[str, Any] | None,
|
|
441
|
+
random_params: Any = None,
|
|
442
|
+
stats: dict[str, Any] | None = None,
|
|
443
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
444
|
+
"""Encode, decode, and compute adversarial + reconstruction losses.
|
|
445
|
+
|
|
446
|
+
The discriminator receives latent codes through a gradient reversal
|
|
447
|
+
layer so that encoder gradients push toward batch invariance while
|
|
448
|
+
discriminator gradients push toward better batch classification.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
data: Dictionary containing:
|
|
452
|
+
- ``"expression"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
453
|
+
- ``"batch_labels"``: Integer batch assignments ``(n_cells,)``
|
|
454
|
+
state: Pipeline state (passed through unchanged).
|
|
455
|
+
metadata: Pipeline metadata (passed through unchanged).
|
|
456
|
+
random_params: Unused.
|
|
457
|
+
stats: Unused.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Tuple of ``(result, state, metadata)`` where *result* contains:
|
|
461
|
+
- ``"expression"``: Original expression
|
|
462
|
+
- ``"batch_labels"``: Original batch labels
|
|
463
|
+
- ``"corrected_expression"``: Decoded (corrected) expression
|
|
464
|
+
- ``"latent"``: Latent representation
|
|
465
|
+
- ``"discriminator_scores"``: Per-cell critic scores
|
|
466
|
+
- ``"generator_loss"``: Scalar Wasserstein generator loss
|
|
467
|
+
- ``"discriminator_loss"``: Scalar Wasserstein discriminator loss
|
|
468
|
+
"""
|
|
469
|
+
expression = data["expression"]
|
|
470
|
+
batch_labels = data["batch_labels"]
|
|
471
|
+
|
|
472
|
+
# Encode -> decode
|
|
473
|
+
latent = self._encode(expression)
|
|
474
|
+
reconstructed = self._decode(latent)
|
|
475
|
+
|
|
476
|
+
# Discriminator path: gradient reversal so encoder learns to fool it
|
|
477
|
+
latent_reversed = _gradient_reversal(latent, 1.0)
|
|
478
|
+
disc_scores = self._discriminate(latent_reversed)
|
|
479
|
+
|
|
480
|
+
# Reconstruction loss
|
|
481
|
+
reconstruction_loss = reduce_loss(
|
|
482
|
+
(reconstructed - expression) ** 2,
|
|
483
|
+
reduction="mean",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Identify "real" (batch 0) and "fake" (batch != 0) for WGAN framing.
|
|
487
|
+
# The discriminator tries to distinguish batch 0 from the rest.
|
|
488
|
+
# We compute masked means manually to stay JIT-compatible (no boolean
|
|
489
|
+
# indexing), then delegate to the library Wasserstein loss functions.
|
|
490
|
+
is_batch0 = (batch_labels == 0).astype(jnp.float32)
|
|
491
|
+
is_other = 1.0 - is_batch0
|
|
492
|
+
n_real = jnp.maximum(is_batch0.sum(), 1.0)
|
|
493
|
+
n_fake = jnp.maximum(is_other.sum(), 1.0)
|
|
494
|
+
|
|
495
|
+
# Masked scores: one scalar per group (batch-mean critic output)
|
|
496
|
+
real_mean_score = (disc_scores * is_batch0).sum() / n_real
|
|
497
|
+
fake_mean_score = (disc_scores * is_other).sum() / n_fake
|
|
498
|
+
|
|
499
|
+
# Use library Wasserstein losses (each expects 1-D scores)
|
|
500
|
+
generator_loss = wasserstein_generator_loss(fake_mean_score[None]) + reconstruction_loss
|
|
501
|
+
discriminator_loss = wasserstein_discriminator_loss(
|
|
502
|
+
real_mean_score[None],
|
|
503
|
+
fake_mean_score[None],
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
result = {
|
|
507
|
+
**data,
|
|
508
|
+
"corrected_expression": reconstructed,
|
|
509
|
+
"latent": latent,
|
|
510
|
+
"discriminator_scores": disc_scores,
|
|
511
|
+
"generator_loss": generator_loss,
|
|
512
|
+
"discriminator_loss": discriminator_loss,
|
|
513
|
+
}
|
|
514
|
+
return result, state, metadata
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
# ---------------------------------------------------------------------------
|
|
518
|
+
# Internal MLP container (nnx.Module so it appears in the grad tree)
|
|
519
|
+
# ---------------------------------------------------------------------------
|