|
| 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() |
0 commit comments