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,15 @@
|
|
|
1
|
+
"""Type aliases for soft differentiable operations.
|
|
2
|
+
|
|
3
|
+
SoftBool represents a probability in [0, 1] -- a soft relaxation of a
|
|
4
|
+
boolean value. SoftIndex represents a probability distribution over
|
|
5
|
+
discrete indices -- a soft relaxation of an integer index.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from jax import Array
|
|
9
|
+
from jaxtyping import Float
|
|
10
|
+
|
|
11
|
+
SoftBool = Float[Array, "..."]
|
|
12
|
+
"""Soft boolean: probability in [0, 1]."""
|
|
13
|
+
|
|
14
|
+
SoftIndex = Float[Array, "..."]
|
|
15
|
+
"""Soft index: probabilities summing to 1 along the last axis."""
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Internal utility functions for soft_ops.
|
|
2
|
+
|
|
3
|
+
These are shared helpers used across the soft_ops submodules.
|
|
4
|
+
Not part of the public API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal, overload
|
|
8
|
+
|
|
9
|
+
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
from jax import Array
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_softness(softness: float | Array) -> None:
|
|
15
|
+
"""Raise ``ValueError`` if softness is not positive.
|
|
16
|
+
|
|
17
|
+
Validation is skipped inside JAX-traced contexts (jit, grad, vmap)
|
|
18
|
+
since concrete values are not available during tracing.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
softness: The softness (temperature) parameter. Must be > 0.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(softness, jax.core.Tracer):
|
|
24
|
+
return
|
|
25
|
+
if float(softness) <= 0:
|
|
26
|
+
msg = f"softness must be positive, got {softness}"
|
|
27
|
+
raise ValueError(msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_float(x: float | Array | jnp.ndarray) -> jnp.ndarray:
|
|
31
|
+
"""Cast to default float dtype if not already floating point.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
x: Input array or scalar.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Array with floating-point dtype.
|
|
38
|
+
"""
|
|
39
|
+
x = jnp.asarray(x)
|
|
40
|
+
if jnp.issubdtype(x.dtype, jnp.floating):
|
|
41
|
+
return x
|
|
42
|
+
return x.astype(jnp.result_type(float))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def normalize_axis_argument(
|
|
46
|
+
x: jnp.ndarray,
|
|
47
|
+
axis: int | None,
|
|
48
|
+
) -> tuple[jnp.ndarray, int]:
|
|
49
|
+
"""Flatten when ``axis`` is None, otherwise canonicalize the axis."""
|
|
50
|
+
if axis is None:
|
|
51
|
+
return jnp.ravel(x), 0
|
|
52
|
+
return x, canonicalize_axis(axis, x.ndim)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
def standardize_and_squash(
|
|
57
|
+
x: jnp.ndarray,
|
|
58
|
+
axis: int = ...,
|
|
59
|
+
eps: float = ...,
|
|
60
|
+
temperature: float = ...,
|
|
61
|
+
return_mean_std: Literal[False] = ...,
|
|
62
|
+
) -> jnp.ndarray: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@overload
|
|
66
|
+
def standardize_and_squash(
|
|
67
|
+
x: jnp.ndarray,
|
|
68
|
+
axis: int = ...,
|
|
69
|
+
eps: float = ...,
|
|
70
|
+
temperature: float = ...,
|
|
71
|
+
*,
|
|
72
|
+
return_mean_std: Literal[True],
|
|
73
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def standardize_and_squash(
|
|
77
|
+
x: jnp.ndarray,
|
|
78
|
+
axis: int = -1,
|
|
79
|
+
eps: float = 1e-6,
|
|
80
|
+
temperature: float = 1.0,
|
|
81
|
+
return_mean_std: bool = False,
|
|
82
|
+
) -> jnp.ndarray | tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
83
|
+
"""Standardize along axis then squash to (0, 1) via sigmoid.
|
|
84
|
+
|
|
85
|
+
Steps:
|
|
86
|
+
1. Standardize: ``(x - mean) / std`` along ``axis``
|
|
87
|
+
2. Scale by ``1 / temperature``
|
|
88
|
+
3. Squash: apply sigmoid to map to (0, 1)
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
x: Input array.
|
|
92
|
+
axis: Axis along which to standardize.
|
|
93
|
+
eps: Epsilon for numerical stability in std computation.
|
|
94
|
+
temperature: Controls sharpness. Lower = sharper.
|
|
95
|
+
return_mean_std: If True, also return mean and std.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Squashed array in (0, 1). If ``return_mean_std``, returns
|
|
99
|
+
``(squashed, mean, std)`` tuple.
|
|
100
|
+
"""
|
|
101
|
+
mean = jnp.mean(x, axis=axis, keepdims=True)
|
|
102
|
+
var = jnp.mean((x - mean) ** 2, axis=axis, keepdims=True)
|
|
103
|
+
std = jnp.sqrt(var + eps)
|
|
104
|
+
z = (x - mean) / std
|
|
105
|
+
z = z / temperature
|
|
106
|
+
z = jax.nn.sigmoid(z)
|
|
107
|
+
if return_mean_std:
|
|
108
|
+
return z, mean, std
|
|
109
|
+
return z
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def unsquash_and_destandardize(
|
|
113
|
+
y: jnp.ndarray,
|
|
114
|
+
mean: jnp.ndarray,
|
|
115
|
+
std: jnp.ndarray,
|
|
116
|
+
eps: float = 1e-10,
|
|
117
|
+
temperature: float = 1.0,
|
|
118
|
+
) -> jnp.ndarray:
|
|
119
|
+
"""Inverse of :func:`standardize_and_squash`.
|
|
120
|
+
|
|
121
|
+
Steps:
|
|
122
|
+
1. Unsquash: logit to map from (0, 1) back to R
|
|
123
|
+
2. Scale by ``temperature``
|
|
124
|
+
3. Destandardize: ``z * std + mean``
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
y: Squashed array in (0, 1).
|
|
128
|
+
mean: Mean from standardization.
|
|
129
|
+
std: Std from standardization.
|
|
130
|
+
eps: Clipping epsilon for logit stability.
|
|
131
|
+
temperature: Must match the value used in squash.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Recovered array in original scale.
|
|
135
|
+
"""
|
|
136
|
+
safe_eps = jnp.maximum(eps, 10 * jnp.finfo(y.dtype).eps)
|
|
137
|
+
y = jnp.clip(y, safe_eps, 1.0 - safe_eps)
|
|
138
|
+
z = jnp.log(y / (1.0 - y))
|
|
139
|
+
z = z * temperature
|
|
140
|
+
return z * std + mean
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def quantile_interpolation_params(
|
|
144
|
+
q: jnp.ndarray,
|
|
145
|
+
n: int,
|
|
146
|
+
method: Literal["linear", "lower", "higher", "nearest", "midpoint"],
|
|
147
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, bool]:
|
|
148
|
+
"""Compute interpolation parameters for quantile computation.
|
|
149
|
+
|
|
150
|
+
Maps a quantile ``q`` in [0, 1] to index ``k``, interpolation
|
|
151
|
+
weight ``a``, and whether to take the next element.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
q: Quantile value(s) in [0, 1].
|
|
155
|
+
n: Number of elements in the sorted array.
|
|
156
|
+
method: Interpolation method.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Tuple of (k, a, take_next) where k is the integer index,
|
|
160
|
+
a is the interpolation weight, and take_next indicates
|
|
161
|
+
whether to interpolate with the next element.
|
|
162
|
+
"""
|
|
163
|
+
p = q * (n - 1)
|
|
164
|
+
|
|
165
|
+
if method == "linear":
|
|
166
|
+
k = jnp.floor(p).astype(jnp.int32)
|
|
167
|
+
a = p - k
|
|
168
|
+
take_next = True
|
|
169
|
+
elif method == "lower":
|
|
170
|
+
k = jnp.floor(p).astype(jnp.int32)
|
|
171
|
+
k = jnp.clip(k, 0, n - 1)
|
|
172
|
+
a = jnp.zeros_like(p)
|
|
173
|
+
take_next = False
|
|
174
|
+
elif method == "higher":
|
|
175
|
+
k = jnp.ceil(p).astype(jnp.int32)
|
|
176
|
+
k = jnp.clip(k, 0, n - 1)
|
|
177
|
+
a = jnp.zeros_like(p)
|
|
178
|
+
take_next = False
|
|
179
|
+
elif method == "nearest":
|
|
180
|
+
flag = jnp.less_equal(p - jnp.floor(p), 0.5)
|
|
181
|
+
k = jnp.where(flag, jnp.floor(p), jnp.ceil(p)).astype(jnp.int32)
|
|
182
|
+
a = jnp.zeros_like(p)
|
|
183
|
+
take_next = False
|
|
184
|
+
elif method == "midpoint":
|
|
185
|
+
k = jnp.floor(p).astype(jnp.int32)
|
|
186
|
+
a = jnp.full_like(p, 0.5)
|
|
187
|
+
is_int = jnp.isclose(p, jnp.round(p))
|
|
188
|
+
a = jnp.where(is_int, 0.0, a)
|
|
189
|
+
take_next = True
|
|
190
|
+
else:
|
|
191
|
+
msg = f"Unknown quantile method: {method!r}"
|
|
192
|
+
raise ValueError(msg)
|
|
193
|
+
return k, a, take_next
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def map_in_chunks(
|
|
197
|
+
f,
|
|
198
|
+
xs: jnp.ndarray,
|
|
199
|
+
chunk_size: int,
|
|
200
|
+
) -> jnp.ndarray:
|
|
201
|
+
"""Map ``f`` row-wise over axis 0 using checkpointed ``lax.scan``.
|
|
202
|
+
|
|
203
|
+
``f`` receives a chunk of shape ``(chunk_size, *rest)`` and must
|
|
204
|
+
return ``(chunk_size, *out_rest)``. Uses ``jax.checkpoint`` for
|
|
205
|
+
O(n) backward memory.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
f: Function to apply to each chunk.
|
|
209
|
+
xs: Input array with shape ``(n, *rest)``.
|
|
210
|
+
chunk_size: Number of rows per chunk.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Output array with shape ``(n, *out_rest)``.
|
|
214
|
+
"""
|
|
215
|
+
n = xs.shape[0]
|
|
216
|
+
if chunk_size >= n:
|
|
217
|
+
return f(xs)
|
|
218
|
+
remainder = n % chunk_size
|
|
219
|
+
if remainder:
|
|
220
|
+
pad_size = chunk_size - remainder
|
|
221
|
+
padding = jnp.zeros((pad_size, *xs.shape[1:]), dtype=xs.dtype)
|
|
222
|
+
xs_padded = jnp.concatenate([xs, padding], axis=0)
|
|
223
|
+
else:
|
|
224
|
+
xs_padded = xs
|
|
225
|
+
n_padded = xs_padded.shape[0]
|
|
226
|
+
xs_chunked = xs_padded.reshape(
|
|
227
|
+
n_padded // chunk_size,
|
|
228
|
+
chunk_size,
|
|
229
|
+
*xs.shape[1:],
|
|
230
|
+
)
|
|
231
|
+
f_remat = jax.checkpoint(f)
|
|
232
|
+
_, ys = jax.lax.scan(
|
|
233
|
+
lambda _, chunk: (None, f_remat(chunk)),
|
|
234
|
+
None,
|
|
235
|
+
xs_chunked,
|
|
236
|
+
)
|
|
237
|
+
ys = ys.reshape(n_padded, *ys.shape[2:])
|
|
238
|
+
return ys[:n]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def reduce_in_chunks(
|
|
242
|
+
f,
|
|
243
|
+
xs: jnp.ndarray,
|
|
244
|
+
chunk_size: int,
|
|
245
|
+
) -> jnp.ndarray:
|
|
246
|
+
"""Apply ``f`` to chunks of ``xs`` along axis 0 and sum results.
|
|
247
|
+
|
|
248
|
+
``f`` receives a chunk of shape ``(chunk_size, *rest)`` and must
|
|
249
|
+
return a result whose shape does **not** include the chunk
|
|
250
|
+
dimension. Remainder rows are zero-padded; the correction
|
|
251
|
+
``f(zeros)`` is subtracted.
|
|
252
|
+
|
|
253
|
+
Uses ``jax.checkpoint`` for memory-efficient backpropagation.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
f: Reduction function applied per chunk.
|
|
257
|
+
xs: Input array with shape ``(n, *rest)``.
|
|
258
|
+
chunk_size: Number of rows per chunk.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Summed result across all chunks.
|
|
262
|
+
"""
|
|
263
|
+
n = xs.shape[0]
|
|
264
|
+
if chunk_size >= n:
|
|
265
|
+
return f(xs)
|
|
266
|
+
remainder = n % chunk_size
|
|
267
|
+
if remainder:
|
|
268
|
+
pad_size = chunk_size - remainder
|
|
269
|
+
padding = jnp.zeros((pad_size, *xs.shape[1:]), dtype=xs.dtype)
|
|
270
|
+
xs_padded = jnp.concatenate([xs, padding], axis=0)
|
|
271
|
+
else:
|
|
272
|
+
pad_size = 0
|
|
273
|
+
xs_padded = xs
|
|
274
|
+
n_padded = xs_padded.shape[0]
|
|
275
|
+
xs_chunked = xs_padded.reshape(
|
|
276
|
+
n_padded // chunk_size,
|
|
277
|
+
chunk_size,
|
|
278
|
+
*xs.shape[1:],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
out_struct = jax.eval_shape(f, xs_chunked[0])
|
|
282
|
+
init = jnp.zeros(out_struct.shape, dtype=out_struct.dtype)
|
|
283
|
+
|
|
284
|
+
f_remat = jax.checkpoint(f)
|
|
285
|
+
|
|
286
|
+
def body(acc: jnp.ndarray, chunk: jnp.ndarray):
|
|
287
|
+
"""Accumulate reduction result for one chunk."""
|
|
288
|
+
return acc + f_remat(chunk), None
|
|
289
|
+
|
|
290
|
+
result, _ = jax.lax.scan(body, init, xs_chunked)
|
|
291
|
+
|
|
292
|
+
if remainder:
|
|
293
|
+
zero_pad = jnp.zeros((pad_size, *xs.shape[1:]), dtype=xs.dtype)
|
|
294
|
+
result = result - f(zero_pad)
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def canonicalize_axis(axis: int | None, num_dims: int) -> int:
|
|
299
|
+
"""Normalize axis to a positive integer.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
axis: Axis index (positive or negative). Must not be None.
|
|
303
|
+
num_dims: Number of dimensions in the array.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Normalized positive axis index.
|
|
307
|
+
|
|
308
|
+
Raises:
|
|
309
|
+
ValueError: If axis is None or out of bounds.
|
|
310
|
+
"""
|
|
311
|
+
if axis is None:
|
|
312
|
+
msg = "axis must be specified"
|
|
313
|
+
raise ValueError(msg)
|
|
314
|
+
if not -num_dims <= axis < num_dims:
|
|
315
|
+
msg = f"axis {axis} is out of bounds for array of dimension {num_dims}"
|
|
316
|
+
raise ValueError(msg)
|
|
317
|
+
if axis < 0:
|
|
318
|
+
axis += num_dims
|
|
319
|
+
return axis
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def flatten_or_canonicalize_axis(
|
|
323
|
+
x: jnp.ndarray,
|
|
324
|
+
axis: int | None,
|
|
325
|
+
) -> tuple[jnp.ndarray, int, int | None]:
|
|
326
|
+
"""Flatten ``x`` if ``axis`` is None, otherwise canonicalize.
|
|
327
|
+
|
|
328
|
+
Returns ``(x_prepared, resolved_axis, original_ndim_if_flattened)``.
|
|
329
|
+
The third element is None when axis was not None.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
x: Input array.
|
|
333
|
+
axis: Axis index or None (flatten).
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Tuple of prepared array, resolved axis, and original ndim
|
|
337
|
+
(only set when the array was flattened).
|
|
338
|
+
"""
|
|
339
|
+
if axis is None:
|
|
340
|
+
num_dims = x.ndim
|
|
341
|
+
return jnp.ravel(x), 0, num_dims
|
|
342
|
+
return x, canonicalize_axis(axis, x.ndim), None
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Autograd-safe math operations.
|
|
2
|
+
|
|
3
|
+
Provides NaN-free alternatives to standard JAX math functions by using
|
|
4
|
+
the double-where trick: the forward pass computes the correct value
|
|
5
|
+
even at domain boundaries, and the backward pass produces finite (zero)
|
|
6
|
+
gradients instead of NaN/Inf.
|
|
7
|
+
|
|
8
|
+
The double-where trick works by:
|
|
9
|
+
1. Replacing problematic inputs with safe values (e.g., 0 -> 1 for sqrt)
|
|
10
|
+
2. Computing the function on the safe input
|
|
11
|
+
3. Using ``jnp.where`` to select the safe output or a fallback (e.g., 0)
|
|
12
|
+
|
|
13
|
+
Because JAX traces through both branches of ``jnp.where``, step 1
|
|
14
|
+
ensures the "unused" branch never produces NaN in its gradient.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import jax.numpy as jnp
|
|
18
|
+
from jax import Array
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sqrt(x: Array) -> Array:
|
|
22
|
+
"""Autograd-safe square root.
|
|
23
|
+
|
|
24
|
+
Returns ``sqrt(x)`` for ``x > 0`` and ``0`` otherwise, without
|
|
25
|
+
producing NaN gradients at ``x = 0``.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
x: Input array.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Elementwise square root, safe for autodiff.
|
|
32
|
+
"""
|
|
33
|
+
safe_x = jnp.where(x > 0, x, 1.0)
|
|
34
|
+
return jnp.where(x > 0, jnp.sqrt(safe_x), 0.0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def arcsin(x: Array) -> Array:
|
|
38
|
+
"""Autograd-safe arcsine.
|
|
39
|
+
|
|
40
|
+
Returns ``arcsin(x)`` for ``|x| < 1`` and ``+/-pi/2`` at the
|
|
41
|
+
boundary, without producing NaN gradients at ``x = +/-1``.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
x: Input array with values in [-1, 1].
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Elementwise arcsine, safe for autodiff.
|
|
48
|
+
"""
|
|
49
|
+
interior = jnp.abs(x) < 1
|
|
50
|
+
safe_x = jnp.where(interior, x, 0.0)
|
|
51
|
+
return jnp.where(interior, jnp.arcsin(safe_x), jnp.sign(x) * (jnp.pi / 2))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def arccos(x: Array) -> Array:
|
|
55
|
+
"""Autograd-safe arccosine.
|
|
56
|
+
|
|
57
|
+
Returns ``arccos(x)`` for ``|x| < 1``, ``0`` at ``x = 1``, and
|
|
58
|
+
``pi`` at ``x = -1``, without producing NaN gradients at the
|
|
59
|
+
boundary.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
x: Input array with values in [-1, 1].
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Elementwise arccosine, safe for autodiff.
|
|
66
|
+
"""
|
|
67
|
+
interior = jnp.abs(x) < 1
|
|
68
|
+
safe_x = jnp.where(interior, x, 0.0)
|
|
69
|
+
return jnp.where(interior, jnp.arccos(safe_x), jnp.where(x >= 1, 0.0, jnp.pi))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def div(x: Array, y: Array) -> Array:
|
|
73
|
+
"""Autograd-safe division.
|
|
74
|
+
|
|
75
|
+
Returns ``x / y`` when ``y != 0`` and ``0`` otherwise, without
|
|
76
|
+
producing NaN gradients at ``y = 0``.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
x: Numerator array.
|
|
80
|
+
y: Denominator array.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Elementwise safe division.
|
|
84
|
+
"""
|
|
85
|
+
nonzero = y != 0
|
|
86
|
+
safe_y = jnp.where(nonzero, y, 1.0)
|
|
87
|
+
return jnp.where(nonzero, x / safe_y, 0.0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def log(x: Array) -> Array:
|
|
91
|
+
"""Autograd-safe natural logarithm.
|
|
92
|
+
|
|
93
|
+
Returns ``log(x)`` for ``x > 0`` and ``0`` otherwise, without
|
|
94
|
+
producing NaN gradients at ``x = 0``.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
x: Input array.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Elementwise natural logarithm, safe for autodiff.
|
|
101
|
+
"""
|
|
102
|
+
safe_x = jnp.where(x > 0, x, 1.0)
|
|
103
|
+
return jnp.where(x > 0, jnp.log(safe_x), 0.0)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def norm(x: Array, axis: int | None = None, keepdims: bool = False) -> Array:
|
|
107
|
+
"""Autograd-safe L2 norm.
|
|
108
|
+
|
|
109
|
+
Computes ``sqrt(sum(x**2))`` using :func:`sqrt`, avoiding NaN
|
|
110
|
+
gradients when the norm is zero.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
x: Input array.
|
|
114
|
+
axis: Axis or axes along which to compute the norm.
|
|
115
|
+
keepdims: If True, retains reduced axes with size 1.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
L2 norm along the given axis, safe for autodiff.
|
|
119
|
+
"""
|
|
120
|
+
return sqrt(jnp.sum(x * x, axis=axis, keepdims=keepdims))
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Soft comparison operators.
|
|
2
|
+
|
|
3
|
+
Provides differentiable relaxations of elementwise comparison operations
|
|
4
|
+
(greater, less, equal, etc.) returning SoftBool values in [0, 1].
|
|
5
|
+
|
|
6
|
+
Each function uses :func:`~diffbio.core.soft_ops.elementwise.sigmoidal`
|
|
7
|
+
as the underlying smooth step function, inheriting the multi-mode
|
|
8
|
+
smoothness options.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import jax.numpy as jnp
|
|
12
|
+
from jax import Array
|
|
13
|
+
|
|
14
|
+
from diffbio.core.soft_ops._types import SoftBool
|
|
15
|
+
from diffbio.core.soft_ops._utils import ensure_float
|
|
16
|
+
from diffbio.core.soft_ops.elementwise import Mode, abs, sigmoidal
|
|
17
|
+
from diffbio.core.soft_ops.logical import logical_not
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def greater(
|
|
21
|
+
x: Array,
|
|
22
|
+
y: float | Array,
|
|
23
|
+
softness: float | Array = 0.1,
|
|
24
|
+
mode: Mode = "smooth",
|
|
25
|
+
epsilon: float = 1e-10,
|
|
26
|
+
) -> SoftBool:
|
|
27
|
+
"""Soft ``x > y``.
|
|
28
|
+
|
|
29
|
+
Uses sigmoidal on ``x - y - epsilon`` so the output approaches 0
|
|
30
|
+
at equality as softness -> 0.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
x: First input array.
|
|
34
|
+
y: Second input array (broadcastable with x).
|
|
35
|
+
softness: Width of transition (> 0).
|
|
36
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
37
|
+
epsilon: Small offset for strict inequality at the limit.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
SoftBool in [0, 1].
|
|
41
|
+
"""
|
|
42
|
+
x = ensure_float(x)
|
|
43
|
+
if mode == "hard":
|
|
44
|
+
return jnp.greater(x, y).astype(x.dtype)
|
|
45
|
+
return sigmoidal(x - y - epsilon, softness=softness, mode=mode)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def greater_equal(
|
|
49
|
+
x: Array,
|
|
50
|
+
y: float | Array,
|
|
51
|
+
softness: float | Array = 0.1,
|
|
52
|
+
mode: Mode = "smooth",
|
|
53
|
+
epsilon: float = 1e-10,
|
|
54
|
+
) -> SoftBool:
|
|
55
|
+
"""Soft ``x >= y``.
|
|
56
|
+
|
|
57
|
+
Uses sigmoidal on ``x - y + epsilon`` so the output approaches 1
|
|
58
|
+
at equality as softness -> 0.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
x: First input array.
|
|
62
|
+
y: Second input array.
|
|
63
|
+
softness: Width of transition (> 0).
|
|
64
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
65
|
+
epsilon: Small offset for non-strict inequality at the limit.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
SoftBool in [0, 1].
|
|
69
|
+
"""
|
|
70
|
+
x = ensure_float(x)
|
|
71
|
+
if mode == "hard":
|
|
72
|
+
return jnp.greater_equal(x, y).astype(x.dtype)
|
|
73
|
+
return sigmoidal(x - y + epsilon, softness=softness, mode=mode)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def less(
|
|
77
|
+
x: Array,
|
|
78
|
+
y: float | Array,
|
|
79
|
+
softness: float | Array = 0.1,
|
|
80
|
+
mode: Mode = "smooth",
|
|
81
|
+
epsilon: float = 1e-10,
|
|
82
|
+
) -> SoftBool:
|
|
83
|
+
"""Soft ``x < y``. Complement of :func:`greater_equal`.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
x: First input array.
|
|
87
|
+
y: Second input array.
|
|
88
|
+
softness: Width of transition (> 0).
|
|
89
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
90
|
+
epsilon: Small offset.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
SoftBool in [0, 1].
|
|
94
|
+
"""
|
|
95
|
+
x = ensure_float(x)
|
|
96
|
+
if mode == "hard":
|
|
97
|
+
return jnp.less(x, y).astype(x.dtype)
|
|
98
|
+
return logical_not(
|
|
99
|
+
greater_equal(x, y, softness=softness, mode=mode, epsilon=epsilon),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def less_equal(
|
|
104
|
+
x: Array,
|
|
105
|
+
y: float | Array,
|
|
106
|
+
softness: float | Array = 0.1,
|
|
107
|
+
mode: Mode = "smooth",
|
|
108
|
+
epsilon: float = 1e-10,
|
|
109
|
+
) -> SoftBool:
|
|
110
|
+
"""Soft ``x <= y``. Complement of :func:`greater`.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
x: First input array.
|
|
114
|
+
y: Second input array.
|
|
115
|
+
softness: Width of transition (> 0).
|
|
116
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
117
|
+
epsilon: Small offset.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
SoftBool in [0, 1].
|
|
121
|
+
"""
|
|
122
|
+
x = ensure_float(x)
|
|
123
|
+
if mode == "hard":
|
|
124
|
+
return jnp.less_equal(x, y).astype(x.dtype)
|
|
125
|
+
return logical_not(
|
|
126
|
+
greater(x, y, softness=softness, mode=mode, epsilon=epsilon),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def equal(
|
|
131
|
+
x: Array,
|
|
132
|
+
y: Array,
|
|
133
|
+
softness: float | Array = 0.1,
|
|
134
|
+
mode: Mode = "smooth",
|
|
135
|
+
epsilon: float = 1e-10,
|
|
136
|
+
) -> SoftBool:
|
|
137
|
+
"""Soft ``x == y``.
|
|
138
|
+
|
|
139
|
+
Implemented as soft ``abs(x - y) <= 0``, scaled to [0, 1].
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
x: First input array.
|
|
143
|
+
y: Second input array.
|
|
144
|
+
softness: Width of transition (> 0).
|
|
145
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
146
|
+
epsilon: Small offset.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
SoftBool in [0, 1].
|
|
150
|
+
"""
|
|
151
|
+
x = ensure_float(x)
|
|
152
|
+
if mode == "hard":
|
|
153
|
+
return jnp.equal(x, y).astype(x.dtype)
|
|
154
|
+
diff = abs(x - y, softness=softness, mode=mode)
|
|
155
|
+
return 2.0 * less_equal(
|
|
156
|
+
diff,
|
|
157
|
+
jnp.zeros_like(diff),
|
|
158
|
+
mode=mode,
|
|
159
|
+
softness=softness,
|
|
160
|
+
epsilon=epsilon,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def not_equal(
|
|
165
|
+
x: Array,
|
|
166
|
+
y: Array,
|
|
167
|
+
softness: float | Array = 0.1,
|
|
168
|
+
mode: Mode = "smooth",
|
|
169
|
+
epsilon: float = 1e-10,
|
|
170
|
+
) -> SoftBool:
|
|
171
|
+
"""Soft ``x != y``.
|
|
172
|
+
|
|
173
|
+
Implemented as soft ``abs(x - y) > 0``, scaled to [0, 1].
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
x: First input array.
|
|
177
|
+
y: Second input array.
|
|
178
|
+
softness: Width of transition (> 0).
|
|
179
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
180
|
+
epsilon: Small offset.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
SoftBool in [0, 1].
|
|
184
|
+
"""
|
|
185
|
+
x = ensure_float(x)
|
|
186
|
+
if mode == "hard":
|
|
187
|
+
return jnp.not_equal(x, y).astype(x.dtype)
|
|
188
|
+
diff = abs(x - y, softness=softness, mode=mode)
|
|
189
|
+
tmp = greater(
|
|
190
|
+
diff,
|
|
191
|
+
jnp.zeros_like(diff),
|
|
192
|
+
mode=mode,
|
|
193
|
+
softness=softness,
|
|
194
|
+
epsilon=epsilon,
|
|
195
|
+
)
|
|
196
|
+
return 2.0 * tmp - 1.0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def isclose(
|
|
200
|
+
x: Array,
|
|
201
|
+
y: Array,
|
|
202
|
+
softness: float | Array = 0.1,
|
|
203
|
+
rtol: float = 1e-05,
|
|
204
|
+
atol: float = 1e-08,
|
|
205
|
+
mode: Mode = "smooth",
|
|
206
|
+
epsilon: float = 1e-10,
|
|
207
|
+
) -> SoftBool:
|
|
208
|
+
"""Soft approximate equality.
|
|
209
|
+
|
|
210
|
+
Implements soft ``abs(x - y) <= atol + rtol * abs(y)``.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
x: First input array.
|
|
214
|
+
y: Second input array.
|
|
215
|
+
softness: Width of transition (> 0).
|
|
216
|
+
rtol: Relative tolerance.
|
|
217
|
+
atol: Absolute tolerance.
|
|
218
|
+
mode: ``"hard"`` or sigmoidal mode.
|
|
219
|
+
epsilon: Small offset.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
SoftBool in [0, 1].
|
|
223
|
+
"""
|
|
224
|
+
x = ensure_float(x)
|
|
225
|
+
if mode == "hard":
|
|
226
|
+
return jnp.isclose(x, y, atol=atol, rtol=rtol).astype(x.dtype)
|
|
227
|
+
diff = abs(x - y, softness=softness, mode=mode)
|
|
228
|
+
y_abs = abs(y, softness=softness, mode=mode)
|
|
229
|
+
return 2.0 * less_equal(
|
|
230
|
+
diff,
|
|
231
|
+
atol + rtol * y_abs,
|
|
232
|
+
mode=mode,
|
|
233
|
+
softness=softness,
|
|
234
|
+
epsilon=epsilon,
|
|
235
|
+
)
|