ultrams 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. ultrams-0.2.0/MANIFEST.in +2 -0
  2. ultrams-0.2.0/PKG-INFO +102 -0
  3. ultrams-0.2.0/README.md +84 -0
  4. ultrams-0.2.0/docs/data-format.md +53 -0
  5. ultrams-0.2.0/docs/model-selection.md +27 -0
  6. ultrams-0.2.0/examples/README.md +15 -0
  7. ultrams-0.2.0/examples/batch_embeddings.py +36 -0
  8. ultrams-0.2.0/examples/data/NOTICE.md +29 -0
  9. ultrams-0.2.0/examples/data/example_5_spectra.mgf +350 -0
  10. ultrams-0.2.0/examples/pytorch_finetune.py +63 -0
  11. ultrams-0.2.0/examples/spectrum_search.py +48 -0
  12. {ultrams-0.1.2 → ultrams-0.2.0}/pyproject.toml +7 -1
  13. ultrams-0.2.0/src/ultrams/__init__.py +14 -0
  14. ultrams-0.2.0/src/ultrams/cli.py +72 -0
  15. {ultrams-0.1.2 → ultrams-0.2.0}/src/ultrams/inference.py +34 -2
  16. ultrams-0.2.0/src/ultrams/spectra.py +134 -0
  17. ultrams-0.2.0/src/ultrams.egg-info/PKG-INFO +102 -0
  18. ultrams-0.2.0/src/ultrams.egg-info/SOURCES.txt +26 -0
  19. ultrams-0.2.0/src/ultrams.egg-info/entry_points.txt +2 -0
  20. {ultrams-0.1.2 → ultrams-0.2.0}/src/ultrams.egg-info/requires.txt +3 -0
  21. ultrams-0.2.0/tests/test_model_api.py +74 -0
  22. ultrams-0.2.0/tests/test_spectra.py +131 -0
  23. ultrams-0.1.2/PKG-INFO +0 -94
  24. ultrams-0.1.2/README.md +0 -78
  25. ultrams-0.1.2/src/ultrams/__init__.py +0 -6
  26. ultrams-0.1.2/src/ultrams.egg-info/PKG-INFO +0 -94
  27. ultrams-0.1.2/src/ultrams.egg-info/SOURCES.txt +0 -12
  28. {ultrams-0.1.2 → ultrams-0.2.0}/LICENSE +0 -0
  29. {ultrams-0.1.2 → ultrams-0.2.0}/setup.cfg +0 -0
  30. {ultrams-0.1.2 → ultrams-0.2.0}/src/ultrams/_architecture.py +0 -0
  31. {ultrams-0.1.2 → ultrams-0.2.0}/src/ultrams/finetune.py +0 -0
  32. {ultrams-0.1.2 → ultrams-0.2.0}/src/ultrams.egg-info/dependency_links.txt +0 -0
  33. {ultrams-0.1.2 → ultrams-0.2.0}/src/ultrams.egg-info/top_level.txt +0 -0
@@ -0,0 +1,2 @@
1
+ recursive-include docs *.md
2
+ recursive-include examples *.py *.md *.mgf
ultrams-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: ultrams
3
+ Version: 0.2.0
4
+ Summary: Pretrained UltraMS models for mass spectrometry
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/Dsadd4/UltraMS
7
+ Project-URL: Source, https://github.com/Dsadd4/UltraMS
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy<3,>=1.24
12
+ Requires-Dist: torch>=2.1
13
+ Requires-Dist: transformers<5,>=4.33
14
+ Requires-Dist: huggingface-hub<1,>=0.23
15
+ Provides-Extra: io
16
+ Requires-Dist: pyteomics>=4.6; extra == "io"
17
+ Dynamic: license-file
18
+
19
+ # UltraMS
20
+
21
+ **Pretrained models for tandem mass spectra.** Turn MS/MS spectra into embeddings, compare spectra, or fine-tune the encoder for a measured property.
22
+
23
+ ```bash
24
+ python -m pip install ultrams
25
+ ```
26
+
27
+ | I want to… | Start here |
28
+ | --- | --- |
29
+ | Embed one spectrum | [Python example](#embed-a-spectrum) |
30
+ | Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
31
+ | Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
32
+ | Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
33
+ | Reproduce pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
34
+
35
+ ## Embed a spectrum
36
+
37
+ ```python
38
+ from ultrams import UltraMS
39
+
40
+ model = UltraMS.from_pretrained("unsupervised")
41
+ embedding = model.encode(
42
+ mz=[100.1, 121.1, 150.0],
43
+ intensity=[20, 100, 35],
44
+ precursor_mz=301.2,
45
+ ).embedding
46
+ print(embedding.shape) # (1024,)
47
+ ```
48
+
49
+ The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
50
+
51
+ ## Embed a spectrum file
52
+
53
+ ```bash
54
+ ultrams-embed spectra.mgf embeddings.npz --model unsupervised
55
+ ```
56
+
57
+ The output has aligned `ids` and `embeddings` arrays. To use mzML, first run `python -m pip install 'ultrams[io]'`; the base installation reads MGF and `.mgf.gz` without an extra file-format package. The [checked-in example](https://github.com/Dsadd4/UltraMS/tree/main/examples) includes five real MS/MS spectra and a complete Python batch workflow.
58
+
59
+ ## Fine-tune with PyTorch
60
+
61
+ `UltraMS` is a `torch.nn.Module`. Each dataset item contains peak `mz`, peak `intensity`, `precursor_mz`, and a measured numeric `target`. The two rows below are runnable example data; substitute your measured spectra and targets for research.
62
+
63
+ ```python
64
+ import torch
65
+ from torch.utils.data import DataLoader
66
+ from ultrams import UltraMS
67
+
68
+ dataset = [
69
+ {"mz": [100.1, 121.1, 150.0], "intensity": [20, 100, 35], "precursor_mz": 301.2, "target": 0.5},
70
+ {"mz": [102.1, 135.2, 167.3], "intensity": [40, 100, 25], "precursor_mz": 315.3, "target": 0.7},
71
+ ]
72
+ device = "cuda" if torch.cuda.is_available() else "cpu"
73
+ model = UltraMS.from_pretrained("unsupervised", device=device).train()
74
+ head = torch.nn.Linear(model.embedding_dim, 1).to(device)
75
+ loader = DataLoader(dataset, batch_size=2, collate_fn=model.batch_converter())
76
+ optimizer = torch.optim.AdamW([*model.parameters(), *head.parameters()], lr=1e-5)
77
+
78
+ for batch in loader:
79
+ batch = {name: value.to(device) for name, value in batch.items()}
80
+ prediction = head(model(batch["peaks"], batch["attention_mask"], batch["precursor_mz"]))
81
+ loss = torch.nn.functional.mse_loss(prediction.squeeze(-1), batch["target"])
82
+ optimizer.zero_grad()
83
+ loss.backward()
84
+ optimizer.step()
85
+ print(f"loss: {loss.item():.4f}")
86
+ ```
87
+
88
+ The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
89
+
90
+ ## Choose a model
91
+
92
+ | Model name | Representation | Dimension | Spectral peak limit | Weights |
93
+ | --- | --- | ---: | ---: | --- |
94
+ | `unsupervised` | Encoder spectrum embedding | 1024 | 150 | [Unsupervised](https://huggingface.co/dsadd4/UltraMS-Unsupervised) |
95
+ | `mona` | MoNA contrastive projection | 1024 | 100 | [MoNA contrastive](https://huggingface.co/dsadd4/UltraMS-MoNA-Contrastive) |
96
+ | `search` | UltraAtlas search projection | 512 | 150 | [Search](https://huggingface.co/dsadd4/UltraMS-Search) |
97
+
98
+ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `model.pt` with `UltraMS.from_checkpoint(path)`. [Model selection](https://github.com/Dsadd4/UltraMS/blob/main/docs/model-selection.md) explains the training purpose and output of each checkpoint. The Search checkpoint is the **UltraAtlas application `best.pt`**.
99
+
100
+ ## Pretraining
101
+
102
+ The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
@@ -0,0 +1,84 @@
1
+ # UltraMS
2
+
3
+ **Pretrained models for tandem mass spectra.** Turn MS/MS spectra into embeddings, compare spectra, or fine-tune the encoder for a measured property.
4
+
5
+ ```bash
6
+ python -m pip install ultrams
7
+ ```
8
+
9
+ | I want to… | Start here |
10
+ | --- | --- |
11
+ | Embed one spectrum | [Python example](#embed-a-spectrum) |
12
+ | Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
13
+ | Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
14
+ | Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
15
+ | Reproduce pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
16
+
17
+ ## Embed a spectrum
18
+
19
+ ```python
20
+ from ultrams import UltraMS
21
+
22
+ model = UltraMS.from_pretrained("unsupervised")
23
+ embedding = model.encode(
24
+ mz=[100.1, 121.1, 150.0],
25
+ intensity=[20, 100, 35],
26
+ precursor_mz=301.2,
27
+ ).embedding
28
+ print(embedding.shape) # (1024,)
29
+ ```
30
+
31
+ The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
32
+
33
+ ## Embed a spectrum file
34
+
35
+ ```bash
36
+ ultrams-embed spectra.mgf embeddings.npz --model unsupervised
37
+ ```
38
+
39
+ The output has aligned `ids` and `embeddings` arrays. To use mzML, first run `python -m pip install 'ultrams[io]'`; the base installation reads MGF and `.mgf.gz` without an extra file-format package. The [checked-in example](https://github.com/Dsadd4/UltraMS/tree/main/examples) includes five real MS/MS spectra and a complete Python batch workflow.
40
+
41
+ ## Fine-tune with PyTorch
42
+
43
+ `UltraMS` is a `torch.nn.Module`. Each dataset item contains peak `mz`, peak `intensity`, `precursor_mz`, and a measured numeric `target`. The two rows below are runnable example data; substitute your measured spectra and targets for research.
44
+
45
+ ```python
46
+ import torch
47
+ from torch.utils.data import DataLoader
48
+ from ultrams import UltraMS
49
+
50
+ dataset = [
51
+ {"mz": [100.1, 121.1, 150.0], "intensity": [20, 100, 35], "precursor_mz": 301.2, "target": 0.5},
52
+ {"mz": [102.1, 135.2, 167.3], "intensity": [40, 100, 25], "precursor_mz": 315.3, "target": 0.7},
53
+ ]
54
+ device = "cuda" if torch.cuda.is_available() else "cpu"
55
+ model = UltraMS.from_pretrained("unsupervised", device=device).train()
56
+ head = torch.nn.Linear(model.embedding_dim, 1).to(device)
57
+ loader = DataLoader(dataset, batch_size=2, collate_fn=model.batch_converter())
58
+ optimizer = torch.optim.AdamW([*model.parameters(), *head.parameters()], lr=1e-5)
59
+
60
+ for batch in loader:
61
+ batch = {name: value.to(device) for name, value in batch.items()}
62
+ prediction = head(model(batch["peaks"], batch["attention_mask"], batch["precursor_mz"]))
63
+ loss = torch.nn.functional.mse_loss(prediction.squeeze(-1), batch["target"])
64
+ optimizer.zero_grad()
65
+ loss.backward()
66
+ optimizer.step()
67
+ print(f"loss: {loss.item():.4f}")
68
+ ```
69
+
70
+ The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
71
+
72
+ ## Choose a model
73
+
74
+ | Model name | Representation | Dimension | Spectral peak limit | Weights |
75
+ | --- | --- | ---: | ---: | --- |
76
+ | `unsupervised` | Encoder spectrum embedding | 1024 | 150 | [Unsupervised](https://huggingface.co/dsadd4/UltraMS-Unsupervised) |
77
+ | `mona` | MoNA contrastive projection | 1024 | 100 | [MoNA contrastive](https://huggingface.co/dsadd4/UltraMS-MoNA-Contrastive) |
78
+ | `search` | UltraAtlas search projection | 512 | 150 | [Search](https://huggingface.co/dsadd4/UltraMS-Search) |
79
+
80
+ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `model.pt` with `UltraMS.from_checkpoint(path)`. [Model selection](https://github.com/Dsadd4/UltraMS/blob/main/docs/model-selection.md) explains the training purpose and output of each checkpoint. The Search checkpoint is the **UltraAtlas application `best.pt`**.
81
+
82
+ ## Pretraining
83
+
84
+ The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
@@ -0,0 +1,53 @@
1
+ # Spectrum input
2
+
3
+ UltraMS accepts an MS/MS spectrum as two equal-length, one-dimensional arrays and the precursor ion's measured `m/z`:
4
+
5
+ ```python
6
+ from ultrams import UltraMS
7
+
8
+ model = UltraMS.from_pretrained("unsupervised")
9
+ result = model.encode(
10
+ mz=[100.1, 121.1, 150.0],
11
+ intensity=[20, 100, 35],
12
+ precursor_mz=301.2,
13
+ )
14
+ print(result.embedding.shape) # (1024,)
15
+ ```
16
+
17
+ `mz` and `intensity` describe the same spectral peaks in the same order. `precursor_mz` is a positive, finite number. Every spectral peak `m/z` and intensity must be finite. The input must contain at least three spectral peaks with positive `m/z` after preprocessing.
18
+
19
+ ## Preprocessing
20
+
21
+ `encode()` and `batch_converter()` apply the same preparation:
22
+
23
+ 1. Discard spectral peaks with `m/z <= 0`.
24
+ 2. Divide intensities by the largest retained intensity when it is positive, then clip intensities to `[0, 1]`.
25
+ 3. If the spectrum exceeds the model's limit, keep the most intense spectral peaks.
26
+ 4. Sort the retained spectral peaks by `m/z`.
27
+
28
+ The three released models accept up to **150** spectral peaks for `"unsupervised"`, **100** for `"mona"`, and **150** for `"search"`. You can inspect the loaded model's limit with `model.input_max_peaks`. A batch made with `model.batch_converter()` pads shorter spectra and supplies an `attention_mask`; padding does not count as a measured spectral peak.
29
+
30
+ For a PyTorch `Dataset`, return one mapping per spectrum:
31
+
32
+ ```python
33
+ {
34
+ "mz": [100.1, 121.1, 150.0],
35
+ "intensity": [20, 100, 35],
36
+ "precursor_mz": 301.2,
37
+ "target": 0.5,
38
+ }
39
+ ```
40
+
41
+ The `target` field is a numeric training value used by `model.batch_converter()` when present in every item of a batch. For `model.finetune(...)`, use `"label"` instead; see [PyTorch fine-tuning](../examples/pytorch_finetune.py) and the [README](../README.md).
42
+
43
+ `model.encode(...).embedding` is a NumPy vector for inference. `model(peaks, attention_mask, precursor_mz)` returns a `[batch, embedding_dim]` PyTorch tensor with gradients for training. See [model selection](model-selection.md) for the representation returned by each checkpoint.
44
+
45
+ ## Spectrum files
46
+
47
+ For MGF or `.mgf.gz`, UltraMS reads each `BEGIN IONS` block, uses `PEPMASS` for precursor-ion `m/z`, and takes the spectrum ID from `TITLE`, `NAME`, or `SCANS` when available. Extra columns after a spectral peak's `m/z` and intensity are ignored. The optional `ultrams[io]` installation adds mzML input; only MS2 spectra are read.
48
+
49
+ ```bash
50
+ ultrams-embed examples/data/example_5_spectra.mgf embeddings.npz --model search
51
+ ```
52
+
53
+ The `.npz` output contains aligned `ids` and `embeddings` arrays. See the [batch embedding example](../examples/batch_embeddings.py) to use the file reader and model directly in Python.
@@ -0,0 +1,27 @@
1
+ # Choose a model
2
+
3
+ All three checkpoints use the UltraMS encoder. `UltraMS.from_pretrained(name)` downloads the selected weights from Hugging Face; `model.encode(...).embedding` returns the representation associated with that checkpoint.
4
+
5
+ | Model | Load with | Training | `embedding` | Dimension | Maximum spectral peaks |
6
+ | --- | --- | --- | --- | ---: | ---: |
7
+ | [Unsupervised](https://huggingface.co/dsadd4/UltraMS-Unsupervised) | `"unsupervised"` | Self-supervised pretraining on UltraMSdata, followed by retention-time training | Encoder spectrum-level CLS embedding | 1024 | 150 |
8
+ | [MoNA contrastive](https://huggingface.co/dsadd4/UltraMS-MoNA-Contrastive) | `"mona"` | Contrastive training on MoNA spectra | Projection of the encoder's CLS embedding | 1024 | 100 |
9
+ | [Search](https://huggingface.co/dsadd4/UltraMS-Search) | `"search"` | Spectrum-to-spectrum contrastive training for the UltraAtlas search model | Projection of the encoder's CLS embedding | 512 | 150 |
10
+
11
+ Use **Unsupervised** as the general starting point for embeddings or a new downstream task. Use **MoNA contrastive** for a representation trained on molecular-identity relationships in MoNA. Use **Search** when working in the spectrum similarity space used by the UltraAtlas application. A model alone does not include the UltraAtlas reference collection or its search index; the [spectrum search example](../examples/spectrum_search.py) shows how to rank a supplied spectral library.
12
+
13
+ The Unsupervised release comes from the completed retention-time-only epoch 11 checkpoint (`rtonly11`). The Search release is the `best.pt` selected for the UltraAtlas application. These source names identify the released weights; the public API uses the names in the table.
14
+
15
+ ```python
16
+ from ultrams import UltraMS
17
+
18
+ model = UltraMS.from_pretrained("search")
19
+ vector = model.encode(
20
+ mz=[100.1, 121.1, 150.0],
21
+ intensity=[20, 100, 35],
22
+ precursor_mz=301.2,
23
+ ).embedding
24
+ print(vector.shape) # (512,)
25
+ ```
26
+
27
+ For every model, `result.cls` exposes the encoder's normalized CLS embedding. MoNA and Search also expose `result.projection`. The Unsupervised and Search default embeddings are unit-normalized; the MoNA default embedding is the learned projection before normalization. Use cosine similarity when comparing spectra. Input requirements and spectral peak preparation are in [Spectrum input](data-format.md).
@@ -0,0 +1,15 @@
1
+ # Run UltraMS on MS/MS spectra
2
+
3
+ These examples use five spectra from the [DreaMS project](data/NOTICE.md). Install UltraMS from the repository root first:
4
+
5
+ ```bash
6
+ python -m pip install -e .
7
+ ```
8
+
9
+ | Task | Run |
10
+ | --- | --- |
11
+ | Read an MGF file and save its embeddings | `python examples/batch_embeddings.py` |
12
+ | Fine-tune with a native PyTorch loop | `python examples/pytorch_finetune.py` |
13
+ | Rank spectra by cosine similarity | `python examples/spectrum_search.py` |
14
+
15
+ Each command downloads its pretrained checkpoint from Hugging Face on first use. `batch_embeddings.py` saves an `.npz` containing spectrum IDs and one embedding per input spectrum. `spectrum_search.py` saves a CSV of ranked matches; its scores illustrate spectrum similarity and are not a benchmark. `pytorch_finetune.py` uses **placeholder regression targets based on file order** solely to show how to train the encoder and a task head. Replace these targets with measured labels for research. By default, output files go to a newly created temporary directory printed by each command; use `--output` or `--output-dir` to choose a persistent destination.
@@ -0,0 +1,36 @@
1
+ """Read an MGF file and save one UltraMS embedding per spectrum."""
2
+
3
+ import argparse
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+ from ultrams import UltraMS, read_spectra
11
+
12
+
13
+ EXAMPLE_MGF = Path(__file__).parent / "data" / "example_5_spectra.mgf"
14
+
15
+
16
+ def main() -> None:
17
+ parser = argparse.ArgumentParser(description=__doc__)
18
+ parser.add_argument("--input", type=Path, default=EXAMPLE_MGF, help="MGF input file")
19
+ parser.add_argument("--model", choices=("unsupervised", "mona", "search"), default="unsupervised")
20
+ parser.add_argument("--batch-size", type=int, default=32)
21
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
22
+ parser.add_argument("--output", type=Path, help="Output .npz path")
23
+ args = parser.parse_args()
24
+
25
+ spectra = list(read_spectra(args.input))
26
+ model = UltraMS.from_pretrained(args.model, device=args.device)
27
+ embeddings = model.encode_batch(spectra, batch_size=args.batch_size)
28
+ output = args.output or Path(tempfile.mkdtemp(prefix="ultrams-embeddings-")) / "embeddings.npz"
29
+ output.parent.mkdir(parents=True, exist_ok=True)
30
+ np.savez_compressed(output, ids=np.asarray([spectrum["id"] for spectrum in spectra]), embeddings=embeddings)
31
+ print(f"Encoded {len(spectra)} spectra into {embeddings.shape} embeddings")
32
+ print(output)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,29 @@
1
+ # Example spectra
2
+
3
+ `example_5_spectra.mgf` is copied without modification from the [DreaMS repository](https://github.com/pluskal-lab/DreaMS/blob/main/data/examples/example_5_spectra.mgf). It contains five example MS/MS spectra. SHA-256 of this copy: `a68132b1528dd818a4297a85463c21384bc80b8cad78231272e43528eae44d30`.
4
+
5
+ The upstream repository publishes this file under the following MIT license:
6
+
7
+ ```text
8
+ MIT License
9
+
10
+ Copyright (c) 2024 Roman Bushuiev
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ ```