diffsvg 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.
@@ -0,0 +1,335 @@
1
+ """Grid search over preprocessing/model options, to see how stable the gene ranking is.
2
+
3
+ `check_stability` re-runs the pipeline per parameter combination (re-binning once per binning signature,
4
+ on a gene subsample), ranks on a statistic rather than the panel-dependent p-values, and re-runs the
5
+ baseline under other seeds to get a noise floor for the agreement.
6
+ """
7
+
8
+ import inspect
9
+ import itertools
10
+ from dataclasses import dataclass
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+ from tqdm.auto import tqdm
15
+
16
+ from diffsvg.pp.utils import bin_obs
17
+
18
+ from . import _exact
19
+ from .model import DiffSVGModelSetup, _resolve_priors, fit_model, setup_model
20
+
21
+ #: Parameters that need `bin_obs` re-run; the others only need a refit.
22
+ _BIN_PARAMS = ("n_bsplines", "degree", "n_bins", "window_radius", "layer")
23
+ _MODEL_PARAMS = ("likelihood", "priors", "use_binned")
24
+
25
+
26
+ def _stability_defaults():
27
+ """Default for every tunable, read off the real signatures so they cannot drift."""
28
+ bin_params = inspect.signature(bin_obs).parameters
29
+ setup_params = inspect.signature(setup_model).parameters
30
+ fit_params = inspect.signature(fit_model).parameters
31
+ return {
32
+ **{name: bin_params[name].default for name in _BIN_PARAMS},
33
+ "likelihood": setup_params["likelihood"].default,
34
+ "priors": setup_params["priors"].default,
35
+ "use_binned": fit_params["use_binned"].default,
36
+ }
37
+
38
+
39
+ def _format_value(value):
40
+ """Compact, readable rendering of a parameter value for run labels."""
41
+ if isinstance(value, dict): # a priors override
42
+ # The full spec, not just the distribution name: two overrides of the same prior with different
43
+ # parameters must get different run labels.
44
+ return "+".join(
45
+ f"{name}:{spec[0]}({','.join(str(v) for v in spec[1:])})" for name, spec in sorted(value.items())
46
+ )
47
+ return str(value)
48
+
49
+
50
+ @dataclass
51
+ class StabilityResult:
52
+ """What `check_stability` found; consumed by `diffsvg.pl.plot_stability`.
53
+
54
+ Attributes
55
+ ----------
56
+ stats, ranks
57
+ `genes x runs` tables of the ranking statistic and of the rank
58
+ derived from it (``1`` = strongest).
59
+ similarity
60
+ `runs x runs` Spearman correlation between rank columns.
61
+ top_overlap
62
+ `runs x runs` fraction of each run's top `top_k` genes that it
63
+ shares with the other run.
64
+ sensitivity
65
+ One row per configuration that differs from the baseline in exactly
66
+ one parameter, with its agreement against the baseline - i.e. the
67
+ one-factor-at-a-time view of which knob actually moves the ranking.
68
+ configs
69
+ `runs x parameters` table of what each run was.
70
+ noise_floor
71
+ Baseline-vs-reseeded-baseline agreement, as
72
+ ``{"spearman": ..., "top_overlap": ...}``. Agreement at or below
73
+ this is within MCMC noise.
74
+ baseline, rank_by, top_k, genes
75
+ The baseline run's label, and the settings the comparison used.
76
+ """
77
+
78
+ stats: pd.DataFrame
79
+ ranks: pd.DataFrame
80
+ similarity: pd.DataFrame
81
+ top_overlap: pd.DataFrame
82
+ sensitivity: pd.DataFrame
83
+ configs: pd.DataFrame
84
+ noise_floor: dict
85
+ baseline: str
86
+ rank_by: str
87
+ top_k: int
88
+ genes: list
89
+
90
+
91
+ def _resolve_genes(adata, genes, random_state):
92
+ if genes is None:
93
+ selected = list(adata.var_names)
94
+ elif isinstance(genes, int):
95
+ rng = np.random.default_rng(random_state)
96
+ size = min(genes, adata.n_vars)
97
+ selected = sorted(rng.choice(np.asarray(adata.var_names), size=size, replace=False).tolist())
98
+ else:
99
+ selected = list(genes)
100
+ missing = set(selected) - set(adata.var_names)
101
+ if missing:
102
+ raise KeyError(f"Genes not found in adata.var_names: {sorted(missing)}")
103
+ if len(selected) < 3:
104
+ raise ValueError(f"need at least 3 genes to compare rankings, got {len(selected)}.")
105
+ return selected
106
+
107
+
108
+ def check_stability(
109
+ adata,
110
+ anchor,
111
+ condition,
112
+ grid,
113
+ baseline=None,
114
+ genes=50,
115
+ rank_by=None,
116
+ higher_is_stronger=True,
117
+ top_k=10,
118
+ n_seeds=2,
119
+ n_workers=1,
120
+ progressbar=True,
121
+ random_state=0,
122
+ design_matrix="design_matrix",
123
+ sample_kwargs=None,
124
+ test="parallel",
125
+ tau=None,
126
+ sample=None,
127
+ inference="auto",
128
+ ):
129
+ """Re-rank genes under every combination in `grid` and measure how much the ranking moves.
130
+
131
+ Parameters
132
+ ----------
133
+ adata
134
+ Annotated data matrix, with `anchor`/`condition` in
135
+ ``adata.obsm[design_matrix]``. Never modified: each run bins and
136
+ fits out-of-place.
137
+ anchor, condition, design_matrix
138
+ Passed to `diffsvg.pp.bin_obs`. `condition` is required - the
139
+ ranking being checked is the differential one, so the condition-free
140
+ models are out of scope here.
141
+ grid
142
+ ``{parameter: [values]}`` over any of
143
+ ``("n_bsplines", "degree", "n_bins", "window_radius", "layer")``
144
+ (these re-bin) and ``("likelihood", "priors", "use_binned")`` (these
145
+ only refit). Priors are varied by passing a list of override dicts,
146
+ e.g. ``{"priors": [{"beta": ("normal", 0.0, 0.5)},
147
+ {"beta": ("normal", 0.0, 2.0)}]}``; see
148
+ `diffsvg.tl.model.DEFAULT_PRIORS`.
149
+ baseline
150
+ The configuration everything is compared against, as a dict of
151
+ parameter values. Defaults to the first value of each `grid` axis.
152
+ genes
153
+ Number of genes to subsample (default 50), an explicit list, or
154
+ `None` for all of them. The same genes are used for every run - a
155
+ different subsample per run would confound the comparison.
156
+ rank_by, higher_is_stronger
157
+ Column of `fit_model`'s statistics table to rank on, and whether
158
+ larger means stronger. Rank on a statistic rather than a p-value:
159
+ the Wald test's p-values are computed against the panel, so
160
+ subsampling shifts them and they do not compare across runs. Defaults
161
+ to ``"parallel_stat"`` for ``test="parallel"`` and ``"wald_stat"`` for
162
+ ``test="wald"``.
163
+ top_k
164
+ Size of the "top genes" set used for the overlap metric.
165
+ n_seeds
166
+ How many times to run the baseline, each with a different
167
+ `random_seed`, to establish the noise floor. `1` skips it. Ignored
168
+ when every run uses exact inference: the statistics are then
169
+ deterministic and the noise floor is 1.
170
+ n_workers, progressbar, sample_kwargs
171
+ As in `fit_model`; `sample_kwargs` is where to make the sweep
172
+ cheaper (e.g. ``{"draws": 300, "tune": 300}``), since ranking needs
173
+ far less precision than a final fit. The per-run progress bars are
174
+ suppressed in favour of one bar over runs.
175
+ random_state
176
+ Seed for the gene subsample.
177
+ test, tau
178
+ Which condition test every run uses, as in `fit_model`.
179
+ inference
180
+ Passed to `fit_model`.
181
+ sample
182
+ Passed to `diffsvg.pp.bin_obs`: the slide column for data with several
183
+ slides per condition, so every run uses the model with slide effects
184
+ (needs ``window_radius=None`` and ``test="parallel"``). Without it,
185
+ multi-slide data is pooled and the rankings are over-confident.
186
+
187
+ Returns
188
+ -------
189
+ StabilityResult
190
+ """
191
+ unknown = set(grid) - set(_BIN_PARAMS) - set(_MODEL_PARAMS)
192
+ if unknown:
193
+ raise ValueError(
194
+ f"Unknown grid parameter(s) {sorted(unknown)}. Available: {sorted(_BIN_PARAMS + _MODEL_PARAMS)}"
195
+ )
196
+ if not grid:
197
+ raise ValueError("`grid` is empty - pass at least one parameter to vary.")
198
+
199
+ if rank_by is None:
200
+ rank_by = "parallel_stat" if test == "parallel" else "wald_stat"
201
+ defaults = _stability_defaults()
202
+ gene_list = _resolve_genes(adata, genes, random_state)
203
+ sample_kwargs = dict(sample_kwargs or {})
204
+ base_seed = sample_kwargs.pop("random_seed", 42)
205
+
206
+ axes = list(grid)
207
+ configs = [
208
+ dict(defaults, **dict(zip(axes, combo, strict=True))) for combo in itertools.product(*(grid[a] for a in axes))
209
+ ]
210
+
211
+ baseline_config = dict(defaults, **{a: grid[a][0] for a in axes})
212
+ if baseline is not None:
213
+ unknown_baseline = set(baseline) - set(defaults)
214
+ if unknown_baseline:
215
+ raise ValueError(f"Unknown baseline parameter(s) {sorted(unknown_baseline)}.")
216
+ baseline_config = dict(defaults, **baseline)
217
+ if baseline_config not in configs:
218
+ configs.insert(0, baseline_config)
219
+
220
+ # Label runs by every parameter that differs between them, including a `baseline` value that is not on
221
+ # the grid, so that no two runs share a label (they would overwrite each other's column).
222
+ varying = [a for a in defaults if len({_format_value(c[a]) for c in configs}) > 1]
223
+
224
+ def label_for(config):
225
+ if not varying:
226
+ return "default"
227
+ return ", ".join(f"{a}={_format_value(config[a])}" for a in varying)
228
+
229
+ baseline_label = label_for(baseline_config)
230
+ runs = [(label_for(config), config, base_seed) for config in configs]
231
+ # Exact inference has no Monte Carlo error, so reseeding would only repeat the same run.
232
+ all_exact = inference in ("auto", "exact") and all(
233
+ _exact.supported(_resolve_priors(c["priors"]), c["likelihood"], sample is not None) for c in configs
234
+ )
235
+ if not all_exact:
236
+ runs += [
237
+ (f"{baseline_label} (reseed {i})", baseline_config, base_seed + i) for i in range(1, max(1, int(n_seeds)))
238
+ ]
239
+ labels = [label for label, _, _ in runs]
240
+ if len(set(labels)) != len(labels):
241
+ raise ValueError(f"Two runs got the same label, which would merge their results: {labels}")
242
+
243
+ # Group by binning signature so each distinct binning is computed once.
244
+ grouped = {}
245
+ for run in runs:
246
+ grouped.setdefault(tuple(_format_value(run[1][p]) for p in _BIN_PARAMS), []).append(run)
247
+
248
+ iterator = tqdm(total=len(runs), desc="Checking DiffSVG stability") if progressbar else None
249
+ columns = {}
250
+ for group in grouped.values():
251
+ bin_config = group[0][1]
252
+ binned = bin_obs(
253
+ adata,
254
+ anchor,
255
+ condition,
256
+ genes=gene_list,
257
+ design_matrix=design_matrix,
258
+ inplace=False,
259
+ sample=sample,
260
+ **{p: bin_config[p] for p in _BIN_PARAMS},
261
+ )
262
+ for label, config, seed in group:
263
+ model_setup = DiffSVGModelSetup(binned=binned, likelihood=config["likelihood"], priors=config["priors"])
264
+ stats_df, _, _ = fit_model(
265
+ adata,
266
+ model_setup,
267
+ n_workers=n_workers,
268
+ progressbar=False,
269
+ inplace=False,
270
+ use_binned=config["use_binned"],
271
+ sample_kwargs={**sample_kwargs, "random_seed": seed},
272
+ test=test,
273
+ tau=tau,
274
+ inference=inference,
275
+ )
276
+ columns[label] = stats_df[rank_by]
277
+ if iterator is not None:
278
+ iterator.update(1)
279
+ if iterator is not None:
280
+ iterator.close()
281
+
282
+ stats = pd.DataFrame(columns).reindex(gene_list)
283
+ ranks = stats.rank(ascending=not higher_is_stronger, method="average")
284
+
285
+ similarity = ranks.corr(method="spearman")
286
+ tops = {label: set(ranks[label].nsmallest(top_k).index) for label in ranks}
287
+ top_overlap = pd.DataFrame(
288
+ [[len(tops[a] & tops[b]) / top_k for b in ranks] for a in ranks], index=list(ranks), columns=list(ranks)
289
+ )
290
+
291
+ reseed_labels = [label for label, _, _ in runs if label.endswith(")") and " (reseed " in label]
292
+ noise_floor = {
293
+ "spearman": 1.0
294
+ if all_exact
295
+ else (float(min(similarity.loc[baseline_label, reseed_labels])) if reseed_labels else float("nan")),
296
+ "top_overlap": 1.0
297
+ if all_exact
298
+ else (float(min(top_overlap.loc[baseline_label, reseed_labels])) if reseed_labels else float("nan")),
299
+ }
300
+
301
+ sensitivity_rows = []
302
+ for label, config, _ in runs:
303
+ if label == baseline_label or label in reseed_labels:
304
+ continue
305
+ changed = [a for a in varying if config[a] != baseline_config[a]]
306
+ if len(changed) == 1:
307
+ sensitivity_rows.append(
308
+ {
309
+ "parameter": changed[0],
310
+ "value": _format_value(config[changed[0]]),
311
+ "run": label,
312
+ "spearman": float(similarity.loc[baseline_label, label]),
313
+ "top_overlap": float(top_overlap.loc[baseline_label, label]),
314
+ }
315
+ )
316
+ sensitivity = pd.DataFrame(sensitivity_rows, columns=["parameter", "value", "run", "spearman", "top_overlap"])
317
+
318
+ config_table = pd.DataFrame(
319
+ [{a: _format_value(config[a]) for a in axes} for _, config, _ in runs], index=[label for label, _, _ in runs]
320
+ )
321
+ config_table = config_table[~config_table.index.duplicated()]
322
+
323
+ return StabilityResult(
324
+ stats=stats,
325
+ ranks=ranks,
326
+ similarity=similarity,
327
+ top_overlap=top_overlap,
328
+ sensitivity=sensitivity,
329
+ configs=config_table,
330
+ noise_floor=noise_floor,
331
+ baseline=baseline_label,
332
+ rank_by=rank_by,
333
+ top_k=top_k,
334
+ genes=gene_list,
335
+ )
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.5
2
+ Name: diffsvg
3
+ Version: 0.1.0
4
+ Summary: Differential spatially variable genes
5
+ Project-URL: Documentation, https://diffsvg.readthedocs.io/
6
+ Project-URL: Homepage, https://github.com/theislab/diffsvg
7
+ Project-URL: Source, https://github.com/theislab/diffsvg
8
+ Author: Tim Treis
9
+ Maintainer-email: Tim Treis <tim.treis@helmholtz-munich.de>
10
+ License: BSD 3-Clause License
11
+
12
+ Copyright (c) 2026, Tim Treis
13
+ All rights reserved.
14
+
15
+ Redistribution and use in source and binary forms, with or without
16
+ modification, are permitted provided that the following conditions are met:
17
+
18
+ 1. Redistributions of source code must retain the above copyright notice, this
19
+ list of conditions and the following disclaimer.
20
+
21
+ 2. Redistributions in binary form must reproduce the above copyright notice,
22
+ this list of conditions and the following disclaimer in the documentation
23
+ and/or other materials provided with the distribution.
24
+
25
+ 3. Neither the name of the copyright holder nor the names of its
26
+ contributors may be used to endorse or promote products derived from
27
+ this software without specific prior written permission.
28
+
29
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
30
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
31
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
32
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
33
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
34
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
35
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
37
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
+ License-File: LICENSE
40
+ Classifier: Programming Language :: Python :: 3 :: Only
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Programming Language :: Python :: 3.14
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.12
46
+ Requires-Dist: anndata>=0.13
47
+ Requires-Dist: colorcet
48
+ Requires-Dist: joblib>=1.3
49
+ Requires-Dist: matplotlib
50
+ Requires-Dist: numpy
51
+ Requires-Dist: pandas
52
+ Requires-Dist: patsy
53
+ Requires-Dist: scikit-learn
54
+ Requires-Dist: scipy
55
+ Requires-Dist: session-info2
56
+ Requires-Dist: tqdm
57
+ Provides-Extra: pymc
58
+ Requires-Dist: arviz; extra == 'pymc'
59
+ Requires-Dist: nutpie; extra == 'pymc'
60
+ Requires-Dist: pymc; extra == 'pymc'
61
+ Description-Content-Type: text/markdown
62
+
63
+ # diffsvg
64
+
65
+ [![Tests][badge-tests]][tests]
66
+ [![Documentation][badge-docs]][documentation]
67
+
68
+ [badge-tests]: https://img.shields.io/github/actions/workflow/status/theislab/diffsvg/test.yaml?branch=main
69
+ [badge-docs]: https://app.readthedocs.org/projects/diffsvg/badge/
70
+
71
+ Differential spatially variable genes
72
+
73
+ ## Getting started
74
+
75
+ Please refer to the [documentation][],
76
+ in particular, the [API documentation][].
77
+
78
+ ## Installation
79
+
80
+ You need to have Python 3.12 or newer installed on your system.
81
+ If you don't have Python installed, we recommend installing [uv][].
82
+
83
+ We recommend managing dependencies in project-specific virtual environments to avoid dependency conflicts.
84
+ This is most convenient using package managers such as [uv][].
85
+ Choose from the options below to install diffsvg:
86
+
87
+ <!--
88
+ 1. Add the latest release of `diffsvg` from [PyPI][] to your `uv` project:
89
+
90
+ ```bash
91
+ uv add diffsvg
92
+ ```
93
+
94
+ 1. Install the latest release into a [standard virtual environment][venv]:
95
+
96
+ ```bash
97
+ (after activating your venv)
98
+ pip install diffsvg
99
+ ```
100
+
101
+ -->
102
+
103
+ 1. Install the latest development version:
104
+
105
+ ```bash
106
+ pip install git+https://github.com/theislab/diffsvg.git # (or `uv add`)
107
+ ```
108
+
109
+ The default inference is exact and needs no sampler. To use NUTS (`inference="nuts"`, only needed for non-normal priors), install the optional extra: `pip install "diffsvg[pymc] @ git+https://github.com/theislab/diffsvg.git"`.
110
+
111
+ ## Release notes
112
+
113
+ See the [changelog][].
114
+
115
+ ## Contact
116
+
117
+ For questions and help requests, you can reach out in the [scverse discourse][].
118
+ If you found a bug, please use the [issue tracker][].
119
+
120
+ ## Citation
121
+
122
+ > t.b.a
123
+
124
+ [uv]: https://github.com/astral-sh/uv
125
+ [scverse discourse]: https://discourse.scverse.org/
126
+ [issue tracker]: https://github.com/theislab/diffsvg/issues
127
+ [tests]: https://github.com/theislab/diffsvg/actions/workflows/test.yaml
128
+ [documentation]: https://diffsvg.readthedocs.io
129
+ [changelog]: https://diffsvg.readthedocs.io/page/changelog.html
130
+ [api documentation]: https://diffsvg.readthedocs.io/page/api.html
131
+ [pypi]: https://pypi.org/project/diffsvg
132
+ [venv]: https://docs.python.org/3/tutorial/venv.html
@@ -0,0 +1,17 @@
1
+ diffsvg/__init__.py,sha256=27tffAL8AnPIPAA9qmTpK63xhWzXn6iy6g9IbmiNxsk,55
2
+ diffsvg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ diffsvg/pl/__init__.py,sha256=cQ9h7e9S0TjaqwOrCnk3CFsf917P3v-C9A3Yxr7MnEA,294
4
+ diffsvg/pl/plot_clusters.py,sha256=GNigoKu4-s3fZrE5Hcg5Yu91ThIOG5D8hbKXRVqVFVk,9721
5
+ diffsvg/pl/plot_results.py,sha256=WW-To73N9DKJw9NyQaQTgIL2ZLm9qsf2e223Y0yduZ0,18592
6
+ diffsvg/pl/plot_stability.py,sha256=NrXzxMwVyR5Mf8Qb-AzjsdwXHO8h0mY3_6gEoShaTl8,7588
7
+ diffsvg/pp/__init__.py,sha256=NqY-t6lR62vuInYDZu-PFF7uO5kuq9KApSaZ0_jPuDc,257
8
+ diffsvg/pp/utils.py,sha256=7ISyCr-UFjCL_Vsa_0PwhtX2Fsbfm3aMFgbq34l8Ln0,26413
9
+ diffsvg/tl/__init__.py,sha256=AEetgToMRQf8pi1KbrY3UULyl6UTb_B-60Ez3mm0Br0,483
10
+ diffsvg/tl/_exact.py,sha256=V6ZRyx90igdlQ8IQ_uokGxfHZEoAK4C6hmcixhd0EKc,14306
11
+ diffsvg/tl/clustering.py,sha256=rlN3DENhNvfMlv4TmOiCnyAdagh9CaNiEN_1X9oe0cM,6219
12
+ diffsvg/tl/model.py,sha256=9yQlkRLW3bmx177u0TnKUQldGjc6jF7d4wcUg3vmjss,81850
13
+ diffsvg/tl/stability.py,sha256=em2N03RyvxiyZgkSr2waFXCIlTABmWMrj8LEAsH5XHE,13188
14
+ diffsvg-0.1.0.dist-info/METADATA,sha256=-XbQHAGdm-978ZSV-OmzDF6X6EaHp2HZU58nrV-YgVM,4952
15
+ diffsvg-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
16
+ diffsvg-0.1.0.dist-info/licenses/LICENSE,sha256=NJjfrbWBi828T_GS40kAzIG9L3hqVmg0xD_zmOPCsoc,1517
17
+ diffsvg-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Tim Treis
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.