Skip to content

Commit bacbe65

Browse files
committed
add CLI
1 parent a6b2ece commit bacbe65

File tree

4 files changed

+241
-0
lines changed

4 files changed

+241
-0
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ homepage = "https://github.com/iterorganization/Waveform-Editor"
5555
where = ["."]
5656
include = ["waveform_editor*"]
5757

58+
[project.scripts]
59+
waveform-editor = "waveform_editor.cli:cli"
60+
5861
[tool.setuptools_scm]
5962
# version_file = "waveform_editor/_version.py"
6063

tests/test_waveform.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,9 @@ def test_overlap_derivatives():
273273
expected = [2, 2, -1.5, -1.5, -1.5, -1.5, -1.5]
274274
values = waveform.get_derivative(np.linspace(0, 3, 7))
275275
assert np.allclose(values, expected)
276+
277+
278+
def test_get_start_end(waveform):
279+
"""Test if waveform returns correct start and end value."""
280+
assert waveform.get_start() == 0
281+
assert waveform.get_end() == 14

waveform_editor/cli.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import csv
2+
import logging
3+
import sys
4+
5+
import click
6+
import numpy as np
7+
from rich import box, console, traceback
8+
from rich.table import Table
9+
10+
import waveform_editor
11+
from waveform_editor.waveform_exporter import WaveformExporter
12+
from waveform_editor.yaml_parser import YamlParser
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
def _excepthook(type_, value, tb):
18+
logger.debug("Suppressed traceback:", exc_info=(type_, value, tb))
19+
# Only display the last traceback frame:
20+
if tb is not None:
21+
while tb.tb_next:
22+
tb = tb.tb_next
23+
rich_tb = traceback.Traceback.from_exception(type_, value, tb, extra_lines=0)
24+
console.Console(stderr=True).print(rich_tb)
25+
26+
27+
@click.group("waveform-editor", invoke_without_command=True, no_args_is_help=True)
28+
@click.option("-v", "--version", is_flag=True, help="Show version information")
29+
def cli(version):
30+
"""The Waveform Editor command line interface.
31+
32+
Please use one of the available commands listed below. You can get help for each
33+
command by executing:
34+
35+
waveform-editor <command> --help
36+
"""
37+
# Limit the traceback to 1 item: avoid scaring CLI users with long traceback prints
38+
# and let them focus on the actual error message
39+
sys.excepthook = _excepthook
40+
41+
if version:
42+
print_version()
43+
44+
45+
def print_version():
46+
"""Print version information of the waveform editor."""
47+
cons = console.Console()
48+
grid = Table(
49+
title="waveform editor version info", show_header=False, title_style="bold"
50+
)
51+
grid.box = box.HORIZONTALS
52+
if cons.size.width > 120:
53+
grid.width = 120
54+
grid.add_row("waveform editor version:", waveform_editor.__version__)
55+
grid.add_section()
56+
console.Console().print(grid)
57+
58+
59+
@cli.command("export-csv")
60+
@click.argument("yaml", type=click.Path(exists=True))
61+
@click.argument("output", type=str)
62+
@click.option("--times", type=click.Path(exists=True))
63+
@click.option("--num_interp", type=int)
64+
def export_csv(yaml, output, times, num_interp):
65+
"""Export waveform data to a CSV file.
66+
67+
\b
68+
Arguments:
69+
yaml: Path to the waveform YAML file.
70+
output: Path where the CSV file will be saved.
71+
72+
\b
73+
Options:
74+
--times: CSV file containing a custom time array (column-based)
75+
--num_interp: Number of points for linear interpolation (only used if --times
76+
is not provided).
77+
"""
78+
exporter = setup_exporter(yaml, times, num_interp)
79+
exporter.to_csv(output)
80+
81+
82+
@cli.command("export-png")
83+
@click.argument("yaml", type=click.Path(exists=True))
84+
@click.argument("output", type=str)
85+
@click.option("--times", type=click.Path(exists=True))
86+
@click.option("--num_interp", type=int)
87+
def export_png(yaml, output, times, num_interp):
88+
"""Export waveform data to a PNG file.
89+
90+
\b
91+
Arguments:
92+
yaml: Path to the waveform YAML file.
93+
output: Path where the PNG file will be saved.
94+
95+
\b
96+
Options:
97+
--times: CSV file containing a custom time array (column-based).
98+
--num_interp: Number of points for linear interpolation (only used if --times
99+
is not provided).
100+
"""
101+
exporter = setup_exporter(yaml, times, num_interp)
102+
exporter.to_png(output)
103+
104+
105+
@cli.command("export-ids")
106+
@click.argument("yaml", type=str)
107+
@click.argument("uri", type=str)
108+
@click.option("--dd-version", type=str)
109+
@click.option("--times", type=click.Path(exists=True))
110+
@click.option("--num_interp", type=int)
111+
def export_ids(yaml, uri, dd_version, times, num_interp):
112+
"""Export waveform data to an IDS.
113+
114+
\b
115+
Arguments:
116+
yaml: Path to the waveform YAML file.
117+
uri: URI containing the IDS, and path to export to. (See below for examples)
118+
119+
\b
120+
Options:
121+
--dd-version: Data Dictionary version to use for the IDS export, if not provided
122+
IMASPy's default DD-version will be used.
123+
--times: CSV file containing a custom time array (column-based).
124+
--num_interp: Number of points for linear interpolation (only used if --times
125+
is not provided).
126+
127+
\b
128+
Example URIs:
129+
- imas:hdf5?path=./testdb#ec_launchers/beam(1)/power_launched
130+
- imas:hdf5?path=./testdb#ec_launchers:1/beam(1)/power_launched
131+
- imas:hdf5?path=./testdb#equilibrium/time_slice()/boundary/elongation
132+
"""
133+
exporter = setup_exporter(yaml, times, num_interp)
134+
exporter.to_ids(uri, dd_version=dd_version)
135+
136+
137+
def setup_exporter(yaml, times, num_interp):
138+
"""Initialize and return a WaveformExporter.
139+
140+
Args:
141+
yaml: Path to the waveform YAML file.
142+
times: Path to a CSV file containing a custom time array.
143+
num_interp: Number of points for linear interpolation (only used if `times`
144+
is None).
145+
Returns:
146+
An instance of the WaveformExporter configured with the waveform.
147+
"""
148+
149+
waveform = load_waveform_from_yaml(yaml)
150+
time_array = load_time_array(times, waveform, num_interp)
151+
exporter = WaveformExporter(waveform, times=time_array)
152+
return exporter
153+
154+
155+
def load_time_array(times, waveform, num_interp):
156+
"""Load time array from CSV file or use default linear interpolation.
157+
158+
Arguments:
159+
times: Path to a CSV file containing a custom time array, or None to use linear
160+
interpolation.
161+
waveform: Waveform to load.
162+
num_interp: Number of points for linear interpolation (only used if `times`
163+
is None).
164+
165+
Returns:
166+
A numpy array containing the time values.
167+
"""
168+
if times and num_interp:
169+
click.secho(
170+
"Both `--num_interp` and `--times` were set. The provided times will "
171+
"be used, and `num_interp` will be ignored.",
172+
fg="yellow",
173+
)
174+
if times:
175+
try:
176+
# assuming single column format
177+
with open(times, newline="") as csvfile:
178+
reader = csv.reader(csvfile)
179+
time_array = [float(row[0]) for row in reader if row]
180+
181+
return np.array(time_array)
182+
except Exception as e:
183+
click.secho(
184+
f"Invalid time array file:\n {e}",
185+
fg="red",
186+
)
187+
elif num_interp:
188+
start = waveform.get_start()
189+
end = waveform.get_end()
190+
return np.linspace(start, end, num_interp)
191+
else:
192+
click.secho(
193+
"Neither `--times` nor `--num_interp` was provided. The time points will "
194+
"automatically be determined, based on the tendencies in the waveform.",
195+
fg="yellow",
196+
)
197+
return None
198+
199+
200+
def load_waveform_from_yaml(yaml_file):
201+
"""Load a waveform object from a YAML file.
202+
203+
Arguments:
204+
yaml_file: Path to the YAML file.
205+
206+
Returns:
207+
The waveform parsed from the YAML file.
208+
"""
209+
with open(yaml_file) as file:
210+
yaml_str = file.read()
211+
yaml_parser = YamlParser()
212+
yaml_parser.parse_waveforms(yaml_str)
213+
annotations = yaml_parser.waveform.annotations
214+
if annotations:
215+
click.secho(
216+
"The following errors and warnings were detected in the YAML file:\n"
217+
f"{annotations}",
218+
fg="red",
219+
)
220+
return yaml_parser.waveform
221+
222+
223+
if __name__ == "__main__":
224+
cli()

waveform_editor/waveform.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ def calc_length(self):
129129
"""Returns the length of the waveform."""
130130
return self.tendencies[-1].end - self.tendencies[0].start
131131

132+
def get_start(self):
133+
"""Returns the start time of the first tendency in the waveform."""
134+
return self.tendencies[0].start
135+
136+
def get_end(self):
137+
"""Returns the end time of the last tendency in the waveform."""
138+
return self.tendencies[-1].end
139+
132140
def _process_waveform(self, waveform):
133141
"""Processes the waveform YAML and populates the tendencies list.
134142

0 commit comments

Comments
 (0)