hs2p 1.0.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.
hs2p-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: hs2p
3
+ Version: 1.0.0
4
+ Summary: Embedding of whole slide images with Foundation Models
5
+ Home-page: https://github.com/clemsgrs/hs2p
6
+ Author: Clément Grisi
7
+ Author-email: clement.grisi@radboudumc.nl
8
+ Project-URL: Bug Tracker, https://github.com/clemsgrs/hs2p/issues
9
+ Platform: unix
10
+ Platform: linux
11
+ Platform: osx
12
+ Platform: cygwin
13
+ Platform: win32
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: omegaconf
23
+ Requires-Dist: numpy<2
24
+ Requires-Dist: pandas
25
+ Requires-Dist: h5py
26
+ Requires-Dist: tqdm
27
+ Requires-Dist: wandb
28
+ Requires-Dist: pillow
29
+ Requires-Dist: matplotlib
30
+ Requires-Dist: opencv-python
31
+ Requires-Dist: wholeslidedata
32
+ Provides-Extra: testing
33
+ Requires-Dist: pytest>=6.0; extra == "testing"
34
+ Requires-Dist: pytest-cov>=2.0; extra == "testing"
35
+ Requires-Dist: mypy>=0.910; extra == "testing"
36
+ Requires-Dist: flake8>=3.9; extra == "testing"
37
+ Requires-Dist: tox>=3.24; extra == "testing"
38
+ Dynamic: author-email
39
+ Dynamic: description
40
+ Dynamic: description-content-type
41
+ Dynamic: home-page
42
+ Dynamic: project-url
43
+
44
+ <h1 align="center">Histopathology Slide Pre-processing Pipeline</h1>
45
+
46
+ [![PyPI version](https://img.shields.io/pypi/v/hs2p?label=pypi&logo=pypi&color=3776AB)](https://pypi.org/project/hs2p/)
47
+ [![Docker Version](https://img.shields.io/docker/v/waticlems/hs2p?sort=semver&label=docker&logo=docker&color=2496ED)](https://hub.docker.com/r/waticlems/hs2p)
48
+
49
+
50
+ HS2P is an open-source project largely based on [CLAM](https://github.com/mahmoodlab/CLAM) tissue segmentation and patching code.
51
+
52
+ <p>
53
+ <a href="https://github.com/psf/black"><img alt="empty" src=https://img.shields.io/badge/code%20style-black-000000.svg></a>
54
+ <a href="https://github.com/PyCQA/pylint"><img alt="empty" src=https://img.shields.io/github/stars/clemsgrs/hs2p?style=social></a>
55
+ </p>
56
+
57
+ ## 🛠️ Installation
58
+
59
+ System requirements: Linux-based OS (e.g., Ubuntu 22.04) with Python 3.11+ and Docker installed.
60
+
61
+ We recommend running the script inside a container using the latest `hs2p` image from Docker Hub:
62
+
63
+ ```shell
64
+ docker pull waticlems/hs2p:latest
65
+ docker run --rm -it \
66
+ -v /path/to/your/data:/data \
67
+ waticlems/hs2p:latest
68
+ ```
69
+
70
+ Replace `/path/to/your/data` with your local data directory.
71
+
72
+ Alternatively, you can install `hs2p` via pip:
73
+
74
+ ```shell
75
+ pip install hs2p
76
+ ```
77
+
78
+ ## Patch Extraction: Step-by-step guide
79
+
80
+ <img src="illustrations/extraction_illu.png" width="1000px" align="center" />
81
+
82
+ 1. Create a `.csv` file containing paths to the desired slides. Optionally, you can provide paths to pre-computed tissue masks under the 'mask_path' column
83
+
84
+ ```csv
85
+ wsi_path,mask_path
86
+ /path/to/slide1.tif,/path/to/mask1.tif
87
+ /path/to/slide2.tif,/path/to/mask2.tif
88
+ ...
89
+ ```
90
+
91
+ 2. Create a configuration file
92
+
93
+ A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
94
+
95
+ 3. Kick off slide tiling
96
+
97
+ ```shell
98
+ python3 -m hs2p.tiling --config-file </path/to/config.yaml>
99
+ ```
100
+
101
+ ## Patch Sampling: Step-by-step guide
102
+
103
+ <img src="illustrations/sampling_illu.png" width="1000px" align="center" />
104
+
105
+ 1. Create a `.csv` file containing paths to the desired slides & associated annotation masks:
106
+
107
+ ```csv
108
+ wsi_path,mask_path
109
+ /path/to/slide1.tif,/path/to/mask1.tif
110
+ /path/to/slide2.tif,/path/to/mask2.tif
111
+ ...
112
+ ```
113
+
114
+ 2. Create a configuration file
115
+
116
+ A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
117
+
118
+ 3. Kick off tile sampling
119
+
120
+ ```shell
121
+ python3 -m hs2p.sampling --config-file </path/to/config.yaml>
122
+ ```
hs2p-1.0.0/README.md ADDED
@@ -0,0 +1,79 @@
1
+ <h1 align="center">Histopathology Slide Pre-processing Pipeline</h1>
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/hs2p?label=pypi&logo=pypi&color=3776AB)](https://pypi.org/project/hs2p/)
4
+ [![Docker Version](https://img.shields.io/docker/v/waticlems/hs2p?sort=semver&label=docker&logo=docker&color=2496ED)](https://hub.docker.com/r/waticlems/hs2p)
5
+
6
+
7
+ HS2P is an open-source project largely based on [CLAM](https://github.com/mahmoodlab/CLAM) tissue segmentation and patching code.
8
+
9
+ <p>
10
+ <a href="https://github.com/psf/black"><img alt="empty" src=https://img.shields.io/badge/code%20style-black-000000.svg></a>
11
+ <a href="https://github.com/PyCQA/pylint"><img alt="empty" src=https://img.shields.io/github/stars/clemsgrs/hs2p?style=social></a>
12
+ </p>
13
+
14
+ ## 🛠️ Installation
15
+
16
+ System requirements: Linux-based OS (e.g., Ubuntu 22.04) with Python 3.11+ and Docker installed.
17
+
18
+ We recommend running the script inside a container using the latest `hs2p` image from Docker Hub:
19
+
20
+ ```shell
21
+ docker pull waticlems/hs2p:latest
22
+ docker run --rm -it \
23
+ -v /path/to/your/data:/data \
24
+ waticlems/hs2p:latest
25
+ ```
26
+
27
+ Replace `/path/to/your/data` with your local data directory.
28
+
29
+ Alternatively, you can install `hs2p` via pip:
30
+
31
+ ```shell
32
+ pip install hs2p
33
+ ```
34
+
35
+ ## Patch Extraction: Step-by-step guide
36
+
37
+ <img src="illustrations/extraction_illu.png" width="1000px" align="center" />
38
+
39
+ 1. Create a `.csv` file containing paths to the desired slides. Optionally, you can provide paths to pre-computed tissue masks under the 'mask_path' column
40
+
41
+ ```csv
42
+ wsi_path,mask_path
43
+ /path/to/slide1.tif,/path/to/mask1.tif
44
+ /path/to/slide2.tif,/path/to/mask2.tif
45
+ ...
46
+ ```
47
+
48
+ 2. Create a configuration file
49
+
50
+ A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
51
+
52
+ 3. Kick off slide tiling
53
+
54
+ ```shell
55
+ python3 -m hs2p.tiling --config-file </path/to/config.yaml>
56
+ ```
57
+
58
+ ## Patch Sampling: Step-by-step guide
59
+
60
+ <img src="illustrations/sampling_illu.png" width="1000px" align="center" />
61
+
62
+ 1. Create a `.csv` file containing paths to the desired slides & associated annotation masks:
63
+
64
+ ```csv
65
+ wsi_path,mask_path
66
+ /path/to/slide1.tif,/path/to/mask1.tif
67
+ /path/to/slide2.tif,/path/to/mask2.tif
68
+ ...
69
+ ```
70
+
71
+ 2. Create a configuration file
72
+
73
+ A good starting point is to look at the default configuration file under `hs2p/configs/default.yaml` where parameters are documented.
74
+
75
+ 3. Kick off tile sampling
76
+
77
+ ```shell
78
+ python3 -m hs2p.sampling --config-file </path/to/config.yaml>
79
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,17 @@
1
+ import pathlib
2
+
3
+ from omegaconf import OmegaConf
4
+
5
+
6
+ def load_config(config_name: str):
7
+ config_filename = config_name + ".yaml"
8
+ return OmegaConf.load(pathlib.Path(__file__).parent.resolve() / config_filename)
9
+
10
+
11
+ default_config = load_config("default")
12
+
13
+
14
+ def load_and_merge_config(config_name: str):
15
+ default_config = OmegaConf.create(default_config)
16
+ loaded_config = load_config(config_name)
17
+ return OmegaConf.merge(default_config, loaded_config)
@@ -0,0 +1,349 @@
1
+ import os
2
+ import tqdm
3
+ import argparse
4
+ import traceback
5
+ import numpy as np
6
+ import pandas as pd
7
+ import seaborn as sns
8
+ import multiprocessing as mp
9
+ from pathlib import Path
10
+
11
+ from hs2p.utils import setup, load_csv, fix_random_seeds
12
+ from hs2p.wsi import extract_coordinates, filter_coordinates, sample_coordinates, save_coordinates, visualize_coordinates, overlay_mask_on_slide, SamplingParameters
13
+
14
+
15
+ def get_args_parser(add_help: bool = True):
16
+ parser = argparse.ArgumentParser("hs2p", add_help=add_help)
17
+ parser.add_argument(
18
+ "--config-file", default="", metavar="FILE", help="path to config file"
19
+ )
20
+ parser.add_argument(
21
+ "--skip-datetime", action="store_true", help="skip run id datetime prefix"
22
+ )
23
+ parser.add_argument(
24
+ "--output-dir",
25
+ type=str,
26
+ help="output directory to save logs and checkpoints",
27
+ )
28
+ parser.add_argument(
29
+ "opts",
30
+ help="Modify config options at the end of the command using \"path.key=value\".",
31
+ default=None,
32
+ nargs=argparse.REMAINDER,
33
+ )
34
+ return parser
35
+
36
+
37
+ def process_slide_wrapper(kwargs):
38
+ return process_slide(**kwargs)
39
+
40
+
41
+ def process_slide(
42
+ *,
43
+ wsi_path: Path,
44
+ mask_path: Path,
45
+ cfg,
46
+ mask_visualize_dir,
47
+ sampling_visualize_dir,
48
+ sampling_params: SamplingParameters,
49
+ disable_tqdm: bool = False,
50
+ num_workers: int = 4,
51
+ ):
52
+ """
53
+ Process a single slide: sample tile coordinates and visualize if needed.
54
+ """
55
+ wsi_name = wsi_path.stem.replace(" ", "_")
56
+ try:
57
+
58
+ if cfg.visualize and sampling_visualize_dir is not None:
59
+ preview_palette = np.zeros(shape=768, dtype=int)
60
+ if sampling_params.color_mapping is None:
61
+ ncat = len(sampling_params.pixel_mapping)
62
+ if ncat <= 10:
63
+ color_palette = sns.color_palette("tab10")[:ncat]
64
+ elif ncat <= 20:
65
+ color_palette = sns.color_palette("tab20")[:ncat]
66
+ else:
67
+ raise ValueError(
68
+ f"Implementation supports up to 20 categories (provided pixel_mapping has {ncat})"
69
+ )
70
+ color_mapping = {
71
+ k: tuple(255 * x for x in color_palette[i])
72
+ for i, k in enumerate(sampling_params.pixel_mapping.keys())
73
+ }
74
+ else:
75
+ color_mapping = sampling_params.color_mapping
76
+ p = [0] * 3 * len(color_mapping)
77
+ for k, v in sampling_params.pixel_mapping.items():
78
+ if color_mapping[k] is not None:
79
+ p[v * 3 : v * 3 + 3] = color_mapping[k]
80
+ n = len(p)
81
+ preview_palette[0:n] = np.array(p).astype(int)
82
+ else:
83
+ color_mapping = None
84
+ preview_palette = None
85
+
86
+ if not cfg.tiling.sampling_params.independant_sampling:
87
+ tissue_mask_visu_path = None
88
+ if cfg.visualize and mask_visualize_dir is not None:
89
+ tissue_mask_visu_path = Path(mask_visualize_dir, f"{wsi_name}-tissue.png")
90
+ coordinates, tile_level, resize_factor, tile_size_lv0 = extract_coordinates(
91
+ wsi_path=wsi_path,
92
+ mask_path=mask_path,
93
+ backend=cfg.tiling.backend,
94
+ tiling_params=cfg.tiling.params,
95
+ segment_params=cfg.tiling.seg_params,
96
+ filter_params=cfg.tiling.filter_params,
97
+ sampling_params=sampling_params,
98
+ mask_visu_path=tissue_mask_visu_path,
99
+ disable_tqdm=disable_tqdm,
100
+ num_workers=num_workers,
101
+ )
102
+ filtered_coordinates = filter_coordinates(
103
+ wsi_path=wsi_path,
104
+ mask_path=mask_path,
105
+ backend=cfg.tiling.backend,
106
+ coordinates=coordinates,
107
+ tile_level=tile_level,
108
+ segment_params=cfg.tiling.seg_params,
109
+ tiling_params=cfg.tiling.params,
110
+ sampling_params=sampling_params,
111
+ disable_tqdm=disable_tqdm,
112
+ ) # a dict mapping annotation -> coordinates
113
+ for annotation, coordinates in filtered_coordinates.items():
114
+ if len(coordinates) == 0:
115
+ continue
116
+ coordinates_dir = Path(cfg.output_dir, "coordinates", annotation)
117
+ coordinates_dir.mkdir(exist_ok=True, parents=True)
118
+ coordinates_path = Path(coordinates_dir, f"{wsi_name}.npy")
119
+ save_coordinates(
120
+ coordinates=coordinates,
121
+ target_spacing=cfg.tiling.params.spacing,
122
+ tile_level=tile_level,
123
+ tile_size=cfg.tiling.params.tile_size,
124
+ resize_factor=resize_factor,
125
+ tile_size_lv0=tile_size_lv0,
126
+ save_path=coordinates_path,
127
+ )
128
+ if cfg.visualize and sampling_visualize_dir is not None:
129
+ visualize_coordinates(
130
+ wsi_path=wsi_path,
131
+ coordinates=coordinates,
132
+ tile_size_lv0=tile_size_lv0,
133
+ save_dir=sampling_visualize_dir,
134
+ downsample=cfg.tiling.visu_params.downsample,
135
+ backend=cfg.tiling.backend,
136
+ mask_path=mask_path,
137
+ annotation=annotation,
138
+ palette=preview_palette,
139
+ )
140
+ else:
141
+ for annotation in sampling_params.pixel_mapping.keys():
142
+ if sampling_params.tissue_percentage[annotation] is None:
143
+ continue
144
+ annotation_mask_dir = mask_visualize_dir / annotation
145
+ annotation_mask_dir.mkdir(exist_ok=True, parents=True)
146
+ tissue_mask_visu_path = None
147
+ if cfg.visualize and mask_visualize_dir is not None:
148
+ tissue_mask_visu_path = Path(annotation_mask_dir, f"{wsi_name}.jpg")
149
+ coordinates, tile_level, resize_factor, tile_size_lv0 = sample_coordinates(
150
+ wsi_path=wsi_path,
151
+ mask_path=mask_path,
152
+ backend=cfg.tiling.backend,
153
+ tiling_params=cfg.tiling.params,
154
+ segment_params=cfg.tiling.seg_params,
155
+ filter_params=cfg.tiling.filter_params,
156
+ sampling_params=sampling_params,
157
+ annotation=annotation,
158
+ mask_visu_path=tissue_mask_visu_path,
159
+ disable_tqdm=disable_tqdm,
160
+ num_workers=num_workers,
161
+ )
162
+ if len(coordinates) == 0:
163
+ continue
164
+ coordinates_dir = Path(cfg.output_dir, "coordinates", annotation)
165
+ coordinates_dir.mkdir(exist_ok=True, parents=True)
166
+ coordinates_path = Path(coordinates_dir, f"{wsi_name}.npy")
167
+ save_coordinates(
168
+ coordinates=coordinates,
169
+ target_spacing=cfg.tiling.params.spacing,
170
+ tile_level=tile_level,
171
+ tile_size=cfg.tiling.params.tile_size,
172
+ resize_factor=resize_factor,
173
+ tile_size_lv0=tile_size_lv0,
174
+ save_path=coordinates_path,
175
+ )
176
+ if cfg.visualize and sampling_visualize_dir is not None:
177
+ visualize_coordinates(
178
+ wsi_path=wsi_path,
179
+ coordinates=coordinates,
180
+ tile_size_lv0=tile_size_lv0,
181
+ save_dir=sampling_visualize_dir,
182
+ downsample=cfg.tiling.visu_params.downsample,
183
+ backend=cfg.tiling.backend,
184
+ mask_path=mask_path,
185
+ annotation=annotation,
186
+ palette=preview_palette,
187
+ )
188
+ if cfg.visualize and mask_visualize_dir is not None:
189
+ mask_visu_path = Path(mask_visualize_dir, f"{wsi_name}.png")
190
+ overlay_mask = overlay_mask_on_slide(
191
+ wsi_path=wsi_path,
192
+ annotation_mask_path=mask_path,
193
+ downsample=cfg.tiling.visu_params.downsample,
194
+ palette=preview_palette,
195
+ pixel_mapping=sampling_params.pixel_mapping,
196
+ color_mapping=color_mapping,
197
+ )
198
+ overlay_mask.save(mask_visu_path)
199
+ return str(wsi_path), {"status": "success"}
200
+
201
+ except Exception as e:
202
+ return str(wsi_path), {
203
+ "status": "failed",
204
+ "error": str(e),
205
+ "traceback": str(traceback.format_exc()),
206
+ }
207
+
208
+
209
+ def main(args):
210
+
211
+ cfg = setup(args)
212
+ output_dir = Path(cfg.output_dir)
213
+
214
+ fix_random_seeds(cfg.seed)
215
+
216
+ wsi_paths, mask_paths = load_csv(cfg)
217
+
218
+ parallel_workers = min(mp.cpu_count(), cfg.speed.num_workers)
219
+ if "SLURM_JOB_CPUS_PER_NODE" in os.environ:
220
+ parallel_workers = min(
221
+ parallel_workers, int(os.environ["SLURM_JOB_CPUS_PER_NODE"])
222
+ )
223
+
224
+ process_list = output_dir / "process_list.csv"
225
+ if process_list.is_file() and cfg.resume:
226
+ process_df = pd.read_csv(process_list)
227
+ else:
228
+ data = {
229
+ "wsi_name": [p.stem for p in wsi_paths],
230
+ "wsi_path": [str(p) for p in wsi_paths],
231
+ "mask_path": [str(p) if p is not None else p for p in mask_paths],
232
+ "sampling_status": ["tbp"] * len(wsi_paths),
233
+ "error": [str(np.nan)] * len(wsi_paths),
234
+ "traceback": [str(np.nan)] * len(wsi_paths),
235
+ }
236
+ process_df = pd.DataFrame(data)
237
+
238
+ skip_sampling = process_df["sampling_status"].str.contains("success").all()
239
+
240
+ pixel_mapping = {k: v for e in cfg.tiling.sampling_params.pixel_mapping for k, v in e.items()}
241
+ tissue_percentage = {k: v for e in cfg.tiling.sampling_params.tissue_percentage for k, v in e.items()}
242
+ if "tissue" not in tissue_percentage:
243
+ tissue_percentage["tissue"] = cfg.tiling.params.min_tissue_percentage
244
+ if cfg.tiling.sampling_params.color_mapping is not None:
245
+ color_mapping = {k: v for e in cfg.tiling.sampling_params.color_mapping for k, v in e.items()}
246
+ else:
247
+ color_mapping = None
248
+
249
+ sampling_params = SamplingParameters(
250
+ pixel_mapping=pixel_mapping,
251
+ color_mapping=color_mapping,
252
+ tissue_percentage=tissue_percentage,
253
+ )
254
+
255
+ if not skip_sampling:
256
+
257
+ mask = process_df["sampling_status"] != "success"
258
+ process_stack = process_df[mask]
259
+ total = len(process_stack)
260
+
261
+ wsi_paths_to_process = [
262
+ Path(x) for x in process_stack.wsi_path.values.tolist()
263
+ ]
264
+ mask_paths_to_process = [
265
+ Path(x) if x is not None else x
266
+ for x in process_stack.mask_path.values.tolist()
267
+ ]
268
+
269
+ # setup directories for coordinates and visualization
270
+ coordinates_dir = output_dir / "coordinates"
271
+ coordinates_dir.mkdir(exist_ok=True, parents=True)
272
+ mask_visualize_dir = None
273
+ sampling_visualize_dir = None
274
+ if cfg.visualize:
275
+ visualize_dir = output_dir / "visualization"
276
+ mask_visualize_dir = Path(visualize_dir, "mask")
277
+ sampling_visualize_dir = Path(visualize_dir, "sampling")
278
+ mask_visualize_dir.mkdir(exist_ok=True, parents=True)
279
+ sampling_visualize_dir.mkdir(exist_ok=True, parents=True)
280
+
281
+ sampling_updates = {}
282
+ with mp.Pool(processes=parallel_workers) as pool:
283
+ args_list = [
284
+ {
285
+ "wsi_path": wsi_fp,
286
+ "mask_path": mask_fp,
287
+ "cfg": cfg,
288
+ "mask_visualize_dir": mask_visualize_dir,
289
+ "sampling_visualize_dir": sampling_visualize_dir,
290
+ "sampling_params": sampling_params,
291
+ "disable_tqdm": True,
292
+ "num_workers": parallel_workers,
293
+ }
294
+ for wsi_fp, mask_fp in zip(
295
+ wsi_paths_to_process, mask_paths_to_process
296
+ )
297
+ ]
298
+ results = list(
299
+ tqdm.tqdm(
300
+ pool.imap(process_slide_wrapper, args_list),
301
+ total=total,
302
+ desc="Slide sampling",
303
+ unit="slide",
304
+ leave=True,
305
+ )
306
+ )
307
+ for wsi_path, status_info in results:
308
+ sampling_updates[wsi_path] = status_info
309
+
310
+ for wsi_path, status_info in sampling_updates.items():
311
+ process_df.loc[
312
+ process_df["wsi_path"] == wsi_path, "sampling_status"
313
+ ] = status_info["status"]
314
+ if "error" in status_info:
315
+ process_df.loc[
316
+ process_df["wsi_path"] == wsi_path, "error"
317
+ ] = status_info["error"]
318
+ process_df.loc[
319
+ process_df["wsi_path"] == wsi_path, "traceback"
320
+ ] = status_info["traceback"]
321
+ process_df.to_csv(process_list, index=False)
322
+
323
+ # summary logging
324
+ total_slides = len(process_df)
325
+ failed_sampling = process_df[process_df["sampling_status"] == "failed"]
326
+ print("=+=" * 10)
327
+ print(f"Total number of slides: {total_slides}")
328
+ print(f"Failed sampling: {len(failed_sampling)}")
329
+ for annotation, pct in tissue_percentage.items():
330
+ if pct is None:
331
+ continue
332
+ slides_with_tiles = [
333
+ str(p)
334
+ for p in wsi_paths
335
+ if Path(coordinates_dir, annotation, f"{p.stem}.npy").is_file()
336
+ ]
337
+ no_tiles = process_df[~process_df["wsi_path"].isin(slides_with_tiles)]
338
+ print(f"No {annotation} tiles after sampling step: {len(no_tiles)}")
339
+ print("=+=" * 10)
340
+
341
+ else:
342
+ print("=+=" * 10)
343
+ print("All slides have been sampled. Skipping sampling step.")
344
+ print("=+=" * 10)
345
+
346
+
347
+ if __name__ == "__main__":
348
+ args = get_args_parser(add_help=True).parse_args()
349
+ main(args)