pycmplot 0.2.5__tar.gz → 0.2.7__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 (36) hide show
  1. pycmplot-0.2.7/LICENSE +21 -0
  2. {pycmplot-0.2.5 → pycmplot-0.2.7}/PKG-INFO +60 -7
  3. {pycmplot-0.2.5 → pycmplot-0.2.7}/README.md +50 -5
  4. pycmplot-0.2.7/benchmark/bench_python.py +266 -0
  5. pycmplot-0.2.7/benchmark/collect_results.py +318 -0
  6. pycmplot-0.2.7/benchmark/generate_sumstats.py +133 -0
  7. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/__init__.py +1 -1
  8. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/_core.py +25 -22
  9. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/annotation.py +34 -0
  10. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/cli.py +94 -12
  11. pycmplot-0.2.7/pycmplot/data/hg18ToHg38.over.chain.gz +0 -0
  12. pycmplot-0.2.7/pycmplot/data/hg19ToHg38.over.chain.gz +0 -0
  13. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/io.py +346 -44
  14. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/liftover.py +82 -15
  15. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/plotting/circular.py +59 -44
  16. pycmplot-0.2.7/pycmplot/plotting/linear.py +1439 -0
  17. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/plotting/qq.py +29 -11
  18. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/resources.py +20 -7
  19. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/stats.py +9 -7
  20. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot.egg-info/PKG-INFO +60 -7
  21. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot.egg-info/SOURCES.txt +5 -1
  22. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot.egg-info/top_level.txt +1 -0
  23. {pycmplot-0.2.5 → pycmplot-0.2.7}/pyproject.toml +11 -3
  24. {pycmplot-0.2.5 → pycmplot-0.2.7}/setup.cfg +1 -1
  25. pycmplot-0.2.5/LICENSE +0 -441
  26. pycmplot-0.2.5/pycmplot/data/hg19ToHg38.over.chain +0 -56506
  27. pycmplot-0.2.5/pycmplot/plotting/linear.py +0 -1034
  28. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/__main__.py +0 -0
  29. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/constants.py +0 -0
  30. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/data/Homo_sapiens.GRCh37.geneinfo.tsv.gz +0 -0
  31. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/data/Homo_sapiens.GRCh38.geneinfo.tsv.gz +0 -0
  32. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot/plotting/__init__.py +0 -0
  33. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot.egg-info/dependency_links.txt +0 -0
  34. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot.egg-info/entry_points.txt +0 -0
  35. {pycmplot-0.2.5 → pycmplot-0.2.7}/pycmplot.egg-info/requires.txt +0 -0
  36. {pycmplot-0.2.5 → pycmplot-0.2.7}/setup.py +0 -0
pycmplot-0.2.7/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Esoh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,15 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycmplot
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Multi-track circular and linear Manhattan plot generation for GWAS summary statistics
5
5
  Author: Kevin Esoh
6
6
  Author-email: Kevin Esoh <kesohku1@jh.edu>
7
- License-Expression: CC-BY-NC-SA-4.0
7
+ License-Expression: MIT
8
8
  Project-URL: Homepage, https://github.com/esohkevin/pycmplot
9
9
  Project-URL: Issues, https://github.com/esohkevin/pycmplot/issues
10
10
  Project-URL: Docs, https://pycmplot.readthedocs.io/en/latest/
11
11
  Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
12
16
  Classifier: Operating System :: OS Independent
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Natural Language :: English
19
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
13
21
  Requires-Python: >=3.9
14
22
  Description-Content-Type: text/markdown
15
23
  License-File: LICENSE
@@ -60,6 +68,8 @@ option of the package should be used to indicate the column and then the package
60
68
  postions in hg19 to hg38 ensuring that hits table generation and plotting are done with one unified
61
69
  corrdinate system.
62
70
 
71
+ # Key features
72
+ ## Column auto-detection
63
73
  A key functionality of the package is its ability to auto-detect certain columns if ommited on the
64
74
  command-line or python API:
65
75
  - Chromosome column: `-chr, --chrom_column` or ommited
@@ -82,11 +92,54 @@ bld_candidates = [build, 'BUILD', 'Genome', 'Genome_Build', 'Genome-build']
82
92
 
83
93
  > NB: Upper and lower cases of the candidates are also considered, making each candidate expanded 3 times.
84
94
 
85
-
86
- Since GWAS summary stats files can be very large, to improve speed and memory efficiency, it is
87
- **highly recommended** to use `-tp, --trim_pval` with a value to exclude variants with p-value above a
88
- certain threshold, e.g. `0.01 (1e-2)` or `0.001 (1e-3)`.
89
-
95
+ ## Density-aware sub-sampling
96
+ Another key feature is density-aware sub-sampling for Manhattan-style scatter plots.
97
+ This was inspired by ``gwaslab``'s default behaviour (https://cloufield.github.io/gwaslab/).
98
+
99
+ Every variant whose "interestingness" signal is at or above ``keep_threshold`` is preserved (so peaks, suggestive hits, genome-wide-significant hits, and extreme
100
+ selection-scan values are kept verbatim). It uniformly sub-samples the dense bulk
101
+ below the threshold down to at most ``max_below`` rows in total. For a 10 M-variant
102
+ scan with the defaults below, this typically cuts the plotted point count from 10 M
103
+ to ~200 K + a few hundred peaks — visually indistinguishable above the suggestive
104
+ band, but two orders of magnitude faster to render.
105
+
106
+ ## Trim insignificant variants for faster plotting
107
+ An optional parameter `-tp, --trim_pval` is provided to increase speed even further.
108
+ Set with a value to exclude variants with p-value above a certain threshold,
109
+ e.g. `0.01 (1e-2)` or `0.001 (1e-3)`. Performed on top of the default auto-thin
110
+ feature above, it siginificant increases speed and reduces peak memory usage.
111
+ See benchmark figure (manuscript in preparation).
112
+
113
+ ## Genome build conversion (liftover)
114
+ Conversion of a both hg18 and hg19 positions to their hg38 equivalent is included through
115
+ `pyliftover.LiftOver`.
116
+
117
+ This means you can concatenate multiple summary stats into one file and include a `BUILD`
118
+ column to specify the genome build of each position ('hg18', 'hg19', or 'hg38') and all
119
+ 'hg18' and 'hg19' positions will be converted to 'hg38' so that all positions are plotted
120
+ using one coordinate system. If only 'hg18' or 'hg19' positions are present, no liftover
121
+ be necessary. Hence, liftover is only performed in cases of mixed genome builds.
122
+
123
+ ## Nearest-gene annotation for GWAS lead SNPs
124
+ The package bundles GFF3 files in hg19 and hg38 coordinates processed to reduce size
125
+ for gene annotation. Also included are UCSC chain files for coordinate conversion (liftover).
126
+ - ``chain_hg19_hg38`` -- UCSC LiftOver chain file for hg19 to hg38
127
+ conversion. Resolved from ``PYCMPLOT_CHAIN_HG19_HG38`` or the bundled
128
+ ``hg19ToHg38.over.chain.gz``.
129
+ - ``chain_hg18_hg38`` -- UCSC LiftOver chain file for hg18 to hg38
130
+ conversion. Resolved from ``PYCMPLOT_CHAIN_HG18_HG38`` or the bundled
131
+ ``hg18ToHg38.over.chain.gz``. Only required when any input summary
132
+ statistics file carries a ``hg18`` build label.
133
+ - ``geneinfo_hg38`` -- Ensembl gene-info TSV for GRCh38, used for
134
+ nearest-gene annotation. Resolved from ``PYCMPLOT_GENEINFO_HG38`` or
135
+ the bundled ``Homo_sapiens.GRCh38.geneinfo.tsv.gz``.
136
+ - ``geneinfo_hg19`` -- Ensembl gene-info TSV for GRCh37, used when
137
+ input data carry a hg19 build label. Resolved from
138
+ ``PYCMPLOT_GENEINFO_HG19`` or the bundled
139
+ ``Homo_sapiens.GRCh37.geneinfo.tsv.gz``.
140
+
141
+
142
+ # Application
90
143
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
91
144
  multiple populations, or multiple traits to observe shared genetic architecture.
92
145
 
@@ -29,6 +29,8 @@ option of the package should be used to indicate the column and then the package
29
29
  postions in hg19 to hg38 ensuring that hits table generation and plotting are done with one unified
30
30
  corrdinate system.
31
31
 
32
+ # Key features
33
+ ## Column auto-detection
32
34
  A key functionality of the package is its ability to auto-detect certain columns if ommited on the
33
35
  command-line or python API:
34
36
  - Chromosome column: `-chr, --chrom_column` or ommited
@@ -51,11 +53,54 @@ bld_candidates = [build, 'BUILD', 'Genome', 'Genome_Build', 'Genome-build']
51
53
 
52
54
  > NB: Upper and lower cases of the candidates are also considered, making each candidate expanded 3 times.
53
55
 
54
-
55
- Since GWAS summary stats files can be very large, to improve speed and memory efficiency, it is
56
- **highly recommended** to use `-tp, --trim_pval` with a value to exclude variants with p-value above a
57
- certain threshold, e.g. `0.01 (1e-2)` or `0.001 (1e-3)`.
58
-
56
+ ## Density-aware sub-sampling
57
+ Another key feature is density-aware sub-sampling for Manhattan-style scatter plots.
58
+ This was inspired by ``gwaslab``'s default behaviour (https://cloufield.github.io/gwaslab/).
59
+
60
+ Every variant whose "interestingness" signal is at or above ``keep_threshold`` is preserved (so peaks, suggestive hits, genome-wide-significant hits, and extreme
61
+ selection-scan values are kept verbatim). It uniformly sub-samples the dense bulk
62
+ below the threshold down to at most ``max_below`` rows in total. For a 10 M-variant
63
+ scan with the defaults below, this typically cuts the plotted point count from 10 M
64
+ to ~200 K + a few hundred peaks — visually indistinguishable above the suggestive
65
+ band, but two orders of magnitude faster to render.
66
+
67
+ ## Trim insignificant variants for faster plotting
68
+ An optional parameter `-tp, --trim_pval` is provided to increase speed even further.
69
+ Set with a value to exclude variants with p-value above a certain threshold,
70
+ e.g. `0.01 (1e-2)` or `0.001 (1e-3)`. Performed on top of the default auto-thin
71
+ feature above, it siginificant increases speed and reduces peak memory usage.
72
+ See benchmark figure (manuscript in preparation).
73
+
74
+ ## Genome build conversion (liftover)
75
+ Conversion of a both hg18 and hg19 positions to their hg38 equivalent is included through
76
+ `pyliftover.LiftOver`.
77
+
78
+ This means you can concatenate multiple summary stats into one file and include a `BUILD`
79
+ column to specify the genome build of each position ('hg18', 'hg19', or 'hg38') and all
80
+ 'hg18' and 'hg19' positions will be converted to 'hg38' so that all positions are plotted
81
+ using one coordinate system. If only 'hg18' or 'hg19' positions are present, no liftover
82
+ be necessary. Hence, liftover is only performed in cases of mixed genome builds.
83
+
84
+ ## Nearest-gene annotation for GWAS lead SNPs
85
+ The package bundles GFF3 files in hg19 and hg38 coordinates processed to reduce size
86
+ for gene annotation. Also included are UCSC chain files for coordinate conversion (liftover).
87
+ - ``chain_hg19_hg38`` -- UCSC LiftOver chain file for hg19 to hg38
88
+ conversion. Resolved from ``PYCMPLOT_CHAIN_HG19_HG38`` or the bundled
89
+ ``hg19ToHg38.over.chain.gz``.
90
+ - ``chain_hg18_hg38`` -- UCSC LiftOver chain file for hg18 to hg38
91
+ conversion. Resolved from ``PYCMPLOT_CHAIN_HG18_HG38`` or the bundled
92
+ ``hg18ToHg38.over.chain.gz``. Only required when any input summary
93
+ statistics file carries a ``hg18`` build label.
94
+ - ``geneinfo_hg38`` -- Ensembl gene-info TSV for GRCh38, used for
95
+ nearest-gene annotation. Resolved from ``PYCMPLOT_GENEINFO_HG38`` or
96
+ the bundled ``Homo_sapiens.GRCh38.geneinfo.tsv.gz``.
97
+ - ``geneinfo_hg19`` -- Ensembl gene-info TSV for GRCh37, used when
98
+ input data carry a hg19 build label. Resolved from
99
+ ``PYCMPLOT_GENEINFO_HG19`` or the bundled
100
+ ``Homo_sapiens.GRCh37.geneinfo.tsv.gz``.
101
+
102
+
103
+ # Application
59
104
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
60
105
  multiple populations, or multiple traits to observe shared genetic architecture.
61
106
 
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ bench_python.py
4
+ Benchmarks Python GWAS visualization tools: pycmplot, gwaslab, qmplot.
5
+
6
+ Usage:
7
+ python bench_python.py --tool pycmplot --input data/sumstats_1M.tsv \
8
+ --size 1M --replicates 5 --outdir results/
9
+
10
+ Writes one CSV row per replicate to results/bench_python.csv.
11
+ """
12
+
13
+ import argparse
14
+ import csv
15
+ import gc
16
+ import os
17
+ import sys
18
+ import time
19
+ import tracemalloc
20
+ from pathlib import Path
21
+
22
+ RESULT_COLS = [
23
+ "tool", "plot_type", "size_label", "n_variants",
24
+ "replicate", "wall_time_s", "peak_mem_mb", "out_file_kb"
25
+ ]
26
+
27
+
28
+ def _record(writer, row: dict):
29
+ writer.writerow({k: row.get(k, "") for k in RESULT_COLS})
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Individual tool wrappers
34
+ # Each wrapper must:
35
+ # 1. Load data from disk (include I/O in timing)
36
+ # 2. Produce a PNG to out_path
37
+ # 3. Return nothing
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def run_pycmplot(input_path: str, out_path: str, plot_type: str = "manhattan"):
41
+ """
42
+ pycmplot benchmark.
43
+ Adjust import path / function names to match your actual API.
44
+ """
45
+ import pandas as pd
46
+ import pycmplot
47
+
48
+ df = pd.read_csv(input_path, sep="\t")
49
+
50
+ if plot_type == "manhattan":
51
+ pycmplot.plot(
52
+ df,
53
+ chrom="CHR",
54
+ pos="BP",
55
+ pval="P",
56
+ snp="SNP",
57
+ plot_type="manhattan",
58
+ out=out_path,
59
+ dpi=150,
60
+ )
61
+ elif plot_type == "circular":
62
+ pycmplot.plot(
63
+ df,
64
+ chrom="CHR",
65
+ pos="BP",
66
+ pval="P",
67
+ snp="SNP",
68
+ plot_type="circular",
69
+ out=out_path,
70
+ dpi=150,
71
+ )
72
+ elif plot_type == "qq":
73
+ pycmplot.plot(
74
+ df,
75
+ chrom="CHR",
76
+ pos="BP",
77
+ pval="P",
78
+ plot_type="qq",
79
+ out=out_path,
80
+ dpi=150,
81
+ )
82
+
83
+
84
+ def run_gwaslab(input_path: str, out_path: str, plot_type: str = "manhattan"):
85
+ """
86
+ gwaslab benchmark.
87
+ https://cloufield.github.io/gwaslab/
88
+ """
89
+ import pandas as pd
90
+ import gwaslab as gl
91
+ import matplotlib
92
+ matplotlib.use("Agg")
93
+
94
+ df = pd.read_csv(input_path, sep="\t")
95
+
96
+ mysumstats = gl.Sumstats(
97
+ df,
98
+ snpid="SNP",
99
+ chrom="CHR",
100
+ pos="BP",
101
+ p="P",
102
+ beta="BETA",
103
+ se="SE",
104
+ ea="A1",
105
+ nea="A2",
106
+ )
107
+
108
+ if plot_type == "manhattan":
109
+ mysumstats.plot_mqq(
110
+ mode="m",
111
+ save=out_path,
112
+ save_args={"dpi": 150},
113
+ verbose=False,
114
+ )
115
+ elif plot_type == "qq":
116
+ mysumstats.plot_mqq(
117
+ mode="q",
118
+ save=out_path,
119
+ save_args={"dpi": 150},
120
+ verbose=False,
121
+ )
122
+
123
+
124
+ def run_qmplot(input_path: str, out_path: str, plot_type: str = "manhattan"):
125
+ """
126
+ qmplot benchmark.
127
+ https://github.com/ShujiaHuang/qmplot
128
+ """
129
+ import pandas as pd
130
+ import matplotlib
131
+ matplotlib.use("Agg")
132
+ import matplotlib.pyplot as plt
133
+ from qmplot import manhattanplot, qqplot
134
+
135
+ df = pd.read_csv(input_path, sep="\t")
136
+
137
+ fig, ax = plt.subplots(figsize=(12, 4))
138
+
139
+ if plot_type in ("manhattan", "circular"):
140
+ manhattanplot(
141
+ data=df,
142
+ chrom="CHR",
143
+ pos="BP",
144
+ pv="P",
145
+ snp="SNP",
146
+ ax=ax,
147
+ )
148
+ elif plot_type == "qq":
149
+ qqplot(
150
+ data=df["P"],
151
+ ax=ax,
152
+ )
153
+
154
+ fig.savefig(out_path, dpi=150, bbox_inches="tight")
155
+ plt.close(fig)
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Timing + memory harness
160
+ # ---------------------------------------------------------------------------
161
+
162
+ TOOL_RUNNERS = {
163
+ "pycmplot": run_pycmplot,
164
+ "gwaslab": run_gwaslab,
165
+ "qmplot": run_qmplot,
166
+ }
167
+
168
+
169
+ def benchmark_one(tool: str, input_path: str, out_path: str, plot_type: str):
170
+ """
171
+ Run one timed, memory-tracked benchmark call.
172
+
173
+ Returns
174
+ -------
175
+ tuple[float, float]
176
+ (wall_time_seconds, peak_memory_mb)
177
+ """
178
+ runner = TOOL_RUNNERS[tool]
179
+
180
+ # Force a full GC cycle before each run so prior allocations don't inflate
181
+ gc.collect()
182
+
183
+ tracemalloc.start()
184
+ t0 = time.perf_counter()
185
+
186
+ runner(input_path, out_path, plot_type)
187
+
188
+ t1 = time.perf_counter()
189
+ _, peak_bytes = tracemalloc.get_traced_memory()
190
+ tracemalloc.stop()
191
+
192
+ wall_time = t1 - t0
193
+ peak_mem_mb = peak_bytes / 1024 / 1024
194
+
195
+ return wall_time, peak_mem_mb
196
+
197
+
198
+ def main():
199
+ parser = argparse.ArgumentParser(description="Benchmark Python GWAS visualization tools")
200
+ parser.add_argument("--tool", required=True, choices=list(TOOL_RUNNERS.keys()))
201
+ parser.add_argument("--input", required=True, help="Path to sumstats TSV")
202
+ parser.add_argument("--size", required=True, help="Dataset size label (e.g. 1M)")
203
+ parser.add_argument("--plot-type", default="manhattan",
204
+ choices=["manhattan", "circular", "qq"],
205
+ help="Plot type to benchmark")
206
+ parser.add_argument("--replicates", type=int, default=5)
207
+ parser.add_argument("--outdir", default="results", help="Directory for CSV results")
208
+ parser.add_argument("--figdir", default="figures", help="Directory for generated figures")
209
+ args = parser.parse_args()
210
+
211
+ os.makedirs(args.outdir, exist_ok=True)
212
+ os.makedirs(args.figdir, exist_ok=True)
213
+
214
+ csv_path = os.path.join(args.outdir, "bench_python.csv")
215
+ write_header = not os.path.exists(csv_path)
216
+
217
+ # Count variants in input
218
+ import pandas as pd
219
+ n_variants = sum(1 for _ in open(args.input)) - 1 # subtract header
220
+
221
+ print(f"\n[bench] tool={args.tool} size={args.size} "
222
+ f"n={n_variants:,} plot={args.plot_type} reps={args.replicates}")
223
+
224
+ with open(csv_path, "a", newline="") as fh:
225
+ writer = csv.DictWriter(fh, fieldnames=RESULT_COLS)
226
+ if write_header:
227
+ writer.writeheader()
228
+
229
+ for rep in range(1, args.replicates + 1):
230
+ out_fig = os.path.join(
231
+ args.figdir,
232
+ f"{args.tool}_{args.plot_type}_{args.size}_rep{rep}.png"
233
+ )
234
+
235
+ try:
236
+ wall, mem = benchmark_one(args.tool, args.input, out_fig, args.plot_type)
237
+ out_kb = os.path.getsize(out_fig) / 1024 if os.path.exists(out_fig) else 0
238
+
239
+ row = dict(
240
+ tool=args.tool,
241
+ plot_type=args.plot_type,
242
+ size_label=args.size,
243
+ n_variants=n_variants,
244
+ replicate=rep,
245
+ wall_time_s=round(wall, 3),
246
+ peak_mem_mb=round(mem, 2),
247
+ out_file_kb=round(out_kb, 1),
248
+ )
249
+ writer.writerow(row)
250
+ fh.flush() # write incrementally in case of OOM on large runs
251
+ print(f" rep {rep}/{args.replicates} "
252
+ f"time={wall:.2f}s mem={mem:.0f}MB fig={out_kb:.0f}KB")
253
+
254
+ except Exception as e:
255
+ print(f" rep {rep} FAILED: {e}", file=sys.stderr)
256
+ writer.writerow(dict(
257
+ tool=args.tool, plot_type=args.plot_type,
258
+ size_label=args.size, n_variants=n_variants,
259
+ replicate=rep, wall_time_s="ERROR",
260
+ peak_mem_mb="ERROR", out_file_kb="ERROR"
261
+ ))
262
+ fh.flush()
263
+
264
+
265
+ if __name__ == "__main__":
266
+ main()