osiris-utils 1.1.10a0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- benchmarks/benchmark_hdf5_io.py +46 -0
- benchmarks/benchmark_load_all.py +54 -0
- docs/source/api/decks.rst +48 -0
- docs/source/api/postprocess.rst +66 -2
- docs/source/api/sim_diag.rst +1 -1
- docs/source/api/utilities.rst +1 -1
- docs/source/conf.py +2 -1
- docs/source/examples/example_Derivatives.md +78 -0
- docs/source/examples/example_FFT.md +152 -0
- docs/source/examples/example_InputDeck.md +148 -0
- docs/source/examples/example_Simulation_Diagnostic.md +213 -0
- docs/source/examples/quick_start.md +51 -0
- docs/source/examples.rst +14 -0
- docs/source/index.rst +8 -0
- examples/edited-deck.1d +1 -1
- examples/example_Derivatives.ipynb +24 -36
- examples/example_FFT.ipynb +44 -23
- examples/example_InputDeck.ipynb +24 -277
- examples/example_Simulation_Diagnostic.ipynb +27 -17
- examples/quick_start.ipynb +17 -1
- osiris_utils/__init__.py +10 -6
- osiris_utils/cli/__init__.py +6 -0
- osiris_utils/cli/__main__.py +85 -0
- osiris_utils/cli/export.py +199 -0
- osiris_utils/cli/info.py +156 -0
- osiris_utils/cli/plot.py +189 -0
- osiris_utils/cli/validate.py +247 -0
- osiris_utils/data/__init__.py +15 -0
- osiris_utils/data/data.py +41 -171
- osiris_utils/data/diagnostic.py +285 -274
- osiris_utils/data/simulation.py +20 -13
- osiris_utils/decks/__init__.py +4 -0
- osiris_utils/decks/decks.py +83 -8
- osiris_utils/decks/species.py +12 -9
- osiris_utils/postprocessing/__init__.py +28 -0
- osiris_utils/postprocessing/derivative.py +317 -106
- osiris_utils/postprocessing/fft.py +135 -24
- osiris_utils/postprocessing/field_centering.py +28 -14
- osiris_utils/postprocessing/heatflux_correction.py +39 -18
- osiris_utils/postprocessing/mft.py +10 -2
- osiris_utils/postprocessing/postprocess.py +8 -5
- osiris_utils/postprocessing/pressure_correction.py +29 -17
- osiris_utils/utils.py +26 -17
- osiris_utils/vis/__init__.py +3 -0
- osiris_utils/vis/plot3d.py +148 -0
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/METADATA +55 -7
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/RECORD +51 -34
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/WHEEL +1 -1
- osiris_utils-1.2.0.dist-info/entry_points.txt +2 -0
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/top_level.txt +1 -0
- osiris_utils/postprocessing/mft_for_gridfile.py +0 -55
- {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Command-line interface for osiris_utils."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import osiris_utils
|
|
7
|
+
|
|
8
|
+
from . import export, info, plot, validate
|
|
9
|
+
|
|
10
|
+
__version__ = osiris_utils.__version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> int:
|
|
14
|
+
"""Main entry point for the osiris CLI.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
argv : list of str, optional
|
|
19
|
+
Command line arguments. If None, uses sys.argv[1:].
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
int
|
|
24
|
+
Exit code (0 for success, non-zero for errors).
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="osiris",
|
|
29
|
+
description="Command-line tools for OSIRIS plasma simulation data analysis",
|
|
30
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
31
|
+
epilog="""
|
|
32
|
+
Examples:
|
|
33
|
+
osiris info path/to/simulation # Show simulation metadata
|
|
34
|
+
osiris export file.h5 --format csv # Export data to CSV
|
|
35
|
+
osiris plot file.h5 --save plot.png # Create quick plot
|
|
36
|
+
osiris validate path/to/simulation # Check file integrity
|
|
37
|
+
|
|
38
|
+
For help on a specific command:
|
|
39
|
+
osiris <command> --help
|
|
40
|
+
""",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--version",
|
|
45
|
+
action="version",
|
|
46
|
+
version=f"osiris_utils {__version__}",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-v",
|
|
51
|
+
"--verbose",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Enable verbose output",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Create subparsers for different commands
|
|
57
|
+
subparsers = parser.add_subparsers(
|
|
58
|
+
title="commands",
|
|
59
|
+
dest="command",
|
|
60
|
+
help="Available commands",
|
|
61
|
+
required=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Import command modules
|
|
65
|
+
|
|
66
|
+
# Register each command's parser
|
|
67
|
+
info.register_parser(subparsers)
|
|
68
|
+
export.register_parser(subparsers)
|
|
69
|
+
plot.register_parser(subparsers)
|
|
70
|
+
validate.register_parser(subparsers)
|
|
71
|
+
|
|
72
|
+
# Parse arguments
|
|
73
|
+
args = parser.parse_args(argv)
|
|
74
|
+
|
|
75
|
+
# Execute the appropriate command
|
|
76
|
+
try:
|
|
77
|
+
return args.func(args)
|
|
78
|
+
except Exception:
|
|
79
|
+
if args.verbose:
|
|
80
|
+
raise
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
sys.exit(main())
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Export command - convert OSIRIS data to different formats."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
import osiris_utils as ou
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_parser(subparsers) -> None:
|
|
15
|
+
"""Register the 'export' subcommand parser."""
|
|
16
|
+
parser = subparsers.add_parser(
|
|
17
|
+
"export",
|
|
18
|
+
help="Export OSIRIS data to CSV, JSON, or NumPy formats",
|
|
19
|
+
description="Convert OSIRIS HDF5 data to common formats for analysis",
|
|
20
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
21
|
+
epilog="""
|
|
22
|
+
Examples:
|
|
23
|
+
osiris export file.h5 --format csv --output data.csv
|
|
24
|
+
osiris export sim/MS/FLD/e3 --format npy --output e3_data.npy
|
|
25
|
+
osiris export file.h5 --format json --output data.json
|
|
26
|
+
""",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"path",
|
|
31
|
+
type=str,
|
|
32
|
+
help="Path to OSIRIS file or diagnostic directory",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-f",
|
|
37
|
+
"--format",
|
|
38
|
+
type=str,
|
|
39
|
+
choices=["csv", "json", "npy"],
|
|
40
|
+
default="csv",
|
|
41
|
+
help="Output format (default: csv)",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"-o",
|
|
46
|
+
"--output",
|
|
47
|
+
type=str,
|
|
48
|
+
required=True,
|
|
49
|
+
help="Output file path",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"-t",
|
|
54
|
+
"--timestep",
|
|
55
|
+
type=int,
|
|
56
|
+
default=None,
|
|
57
|
+
help="Specific timestep index to export (default: export all)",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--no-coords",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Exclude coordinate information (data only)",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.set_defaults(func=run)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run(args: argparse.Namespace) -> int:
|
|
70
|
+
"""Execute the export command."""
|
|
71
|
+
path = Path(args.path)
|
|
72
|
+
|
|
73
|
+
if not path.exists():
|
|
74
|
+
print(f"Error: Path '{path}' does not exist", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Load data
|
|
79
|
+
if path.is_file():
|
|
80
|
+
data_obj = ou.OsirisGridFile(str(path))
|
|
81
|
+
export_single_file(data_obj, args)
|
|
82
|
+
elif path.is_dir():
|
|
83
|
+
# Assume it's a diagnostic directory
|
|
84
|
+
export_diagnostic_dir(path, args)
|
|
85
|
+
else:
|
|
86
|
+
print(f"Error: '{path}' is not a file or directory", file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
print(f"Exported to: {args.output}")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Error exporting data: {e}", file=sys.stderr)
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def export_single_file(data_obj, args: argparse.Namespace) -> None:
|
|
98
|
+
"""Export a single OSIRIS file."""
|
|
99
|
+
data = data_obj.data
|
|
100
|
+
output_path = Path(args.output)
|
|
101
|
+
|
|
102
|
+
if args.format == "csv":
|
|
103
|
+
export_to_csv(data, data_obj, output_path, args.no_coords)
|
|
104
|
+
elif args.format == "json":
|
|
105
|
+
export_to_json(data, data_obj, output_path, args.no_coords)
|
|
106
|
+
elif args.format == "npy":
|
|
107
|
+
np.save(output_path, data)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def export_diagnostic_dir(diag_path: Path, args: argparse.Namespace) -> None:
|
|
111
|
+
"""Export all timesteps from a diagnostic directory."""
|
|
112
|
+
# Find all h5 files
|
|
113
|
+
h5_files = sorted(list(diag_path.glob("*.h5")))
|
|
114
|
+
|
|
115
|
+
if not h5_files:
|
|
116
|
+
raise ValueError(f"No HDF5 files found in {diag_path}")
|
|
117
|
+
|
|
118
|
+
if args.timestep is not None:
|
|
119
|
+
# Export specific timestep
|
|
120
|
+
if args.timestep >= len(h5_files):
|
|
121
|
+
raise ValueError(f"Timestep {args.timestep} out of range (0-{len(h5_files) - 1})")
|
|
122
|
+
data_obj = ou.OsirisGridFile(str(h5_files[args.timestep]))
|
|
123
|
+
export_single_file(data_obj, args)
|
|
124
|
+
# Export all timesteps
|
|
125
|
+
elif args.format == "npy":
|
|
126
|
+
# Stack all data into single array
|
|
127
|
+
all_data = []
|
|
128
|
+
for f in h5_files:
|
|
129
|
+
data_obj = ou.OsirisGridFile(str(f))
|
|
130
|
+
all_data.append(data_obj.data)
|
|
131
|
+
stacked = np.array(all_data)
|
|
132
|
+
np.save(args.output, stacked)
|
|
133
|
+
else:
|
|
134
|
+
# For CSV/JSON, export each timestep separately or create multi-index
|
|
135
|
+
export_multi_timestep(h5_files, args)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def export_to_csv(data: np.ndarray, data_obj, output_path: Path, no_coords: bool) -> None:
|
|
139
|
+
"""Export data to CSV format."""
|
|
140
|
+
if data.ndim == 1:
|
|
141
|
+
# 1D data
|
|
142
|
+
if no_coords:
|
|
143
|
+
df = pd.DataFrame({data_obj.name: data})
|
|
144
|
+
else:
|
|
145
|
+
df = pd.DataFrame({"x": data_obj.x[0], data_obj.name: data})
|
|
146
|
+
elif data.ndim == 2:
|
|
147
|
+
# 2D data - flatten with coordinates
|
|
148
|
+
if no_coords:
|
|
149
|
+
df = pd.DataFrame(data)
|
|
150
|
+
else:
|
|
151
|
+
x = data_obj.x[0]
|
|
152
|
+
y = data_obj.x[1]
|
|
153
|
+
xx, yy = np.meshgrid(x, y, indexing="ij")
|
|
154
|
+
df = pd.DataFrame({"x": xx.flatten(), "y": yy.flatten(), data_obj.name: data.flatten()})
|
|
155
|
+
else:
|
|
156
|
+
# 3D+ data - just flatten
|
|
157
|
+
df = pd.DataFrame(data.flatten(), columns=[data_obj.name])
|
|
158
|
+
|
|
159
|
+
df.to_csv(output_path, index=False)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def export_to_json(data: np.ndarray, data_obj, output_path: Path, no_coords: bool) -> None:
|
|
163
|
+
"""Export data to JSON format."""
|
|
164
|
+
output = {
|
|
165
|
+
"name": data_obj.name,
|
|
166
|
+
"type": data_obj.type,
|
|
167
|
+
"units": data_obj.units,
|
|
168
|
+
"time": data_obj.time,
|
|
169
|
+
"iteration": data_obj.iter,
|
|
170
|
+
"data": data.tolist(),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if not no_coords:
|
|
174
|
+
output["grid"] = {f"x{i + 1}": coord.tolist() for i, coord in enumerate(data_obj.x)}
|
|
175
|
+
output["axis_info"] = data_obj.axis
|
|
176
|
+
|
|
177
|
+
with open(output_path, "w") as f:
|
|
178
|
+
json.dump(output, f, indent=2)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def export_multi_timestep(h5_files: list, args: argparse.Namespace) -> None:
|
|
182
|
+
"""Export multiple timesteps to CSV/JSON."""
|
|
183
|
+
if args.format == "csv":
|
|
184
|
+
# Create multi-index CSV
|
|
185
|
+
all_dfs = []
|
|
186
|
+
for i, f in enumerate(h5_files):
|
|
187
|
+
data_obj = ou.OsirisGridFile(str(f))
|
|
188
|
+
df = pd.DataFrame({"timestep": i, "time": data_obj.time, "data": data_obj.data.flatten()})
|
|
189
|
+
all_dfs.append(df)
|
|
190
|
+
combined = pd.concat(all_dfs, ignore_index=True)
|
|
191
|
+
combined.to_csv(args.output, index=False)
|
|
192
|
+
elif args.format == "json":
|
|
193
|
+
# Create JSON array
|
|
194
|
+
all_data = []
|
|
195
|
+
for f in h5_files:
|
|
196
|
+
data_obj = ou.OsirisGridFile(str(f))
|
|
197
|
+
all_data.append({"time": data_obj.time, "iteration": data_obj.iter, "data": data_obj.data.tolist()})
|
|
198
|
+
with open(args.output, "w") as outf:
|
|
199
|
+
json.dump(all_data, outf, indent=2)
|
osiris_utils/cli/info.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Info command - display metadata about OSIRIS files and simulations."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import osiris_utils as ou
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_parser(subparsers) -> None:
|
|
11
|
+
"""Register the 'info' subcommand parser."""
|
|
12
|
+
parser = subparsers.add_parser(
|
|
13
|
+
"info",
|
|
14
|
+
help="Display metadata about OSIRIS files or simulations",
|
|
15
|
+
description="Show detailed information about OSIRIS simulation data",
|
|
16
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
17
|
+
epilog="""
|
|
18
|
+
Examples:
|
|
19
|
+
osiris info path/to/simulation # Show all simulation info
|
|
20
|
+
osiris info path/to/file.h5 # Show single file info
|
|
21
|
+
osiris info path/to/simulation --brief # Show summary only
|
|
22
|
+
""",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"path",
|
|
27
|
+
type=str,
|
|
28
|
+
help="Path to OSIRIS simulation directory or HDF5 file",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--brief",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Show brief summary only",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parser.set_defaults(func=run)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(args: argparse.Namespace) -> int:
|
|
41
|
+
"""Execute the info command."""
|
|
42
|
+
path = Path(args.path)
|
|
43
|
+
|
|
44
|
+
if not path.exists():
|
|
45
|
+
print(f"Error: Path '{path}' does not exist", file=sys.stderr)
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
# Determine if path is a file or directory
|
|
49
|
+
# Determine if path is a file or directory
|
|
50
|
+
if path.is_file():
|
|
51
|
+
return show_file_info(path, args.brief)
|
|
52
|
+
elif path.is_dir():
|
|
53
|
+
return show_simulation_info(path, args.brief, args.verbose)
|
|
54
|
+
else:
|
|
55
|
+
print(f"Error: '{path}' is not a file or directory", file=sys.stderr)
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def show_file_info(filepath: Path, brief: bool = False) -> int:
|
|
60
|
+
"""Display information about a single OSIRIS file."""
|
|
61
|
+
try:
|
|
62
|
+
# Try to open as grid file
|
|
63
|
+
data = ou.OsirisGridFile(str(filepath))
|
|
64
|
+
|
|
65
|
+
print(f"File: {filepath.name}")
|
|
66
|
+
print(f"Type: {data.type}")
|
|
67
|
+
print(f"Name: {data.name}")
|
|
68
|
+
print(f"Dimensions: {data.dim}D")
|
|
69
|
+
|
|
70
|
+
if not brief:
|
|
71
|
+
print("\nGrid Information:")
|
|
72
|
+
print(f" nx: {data.nx}")
|
|
73
|
+
print(f" dx: {data.dx}")
|
|
74
|
+
print(f" Grid range: {[tuple(ax['axis']) for ax in data.axis]}")
|
|
75
|
+
|
|
76
|
+
print("\nTime Information:")
|
|
77
|
+
print(f" Time: {data.time}")
|
|
78
|
+
print(f" dt: {data.dt}")
|
|
79
|
+
print(f" Iteration: {data.iter}")
|
|
80
|
+
|
|
81
|
+
print("\nData Information:")
|
|
82
|
+
print(f" Shape: {data.data.shape}")
|
|
83
|
+
print(f" Units: {data.units}")
|
|
84
|
+
print(f" Label: {data.label}")
|
|
85
|
+
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def show_simulation_info(simpath: Path, brief: bool = False, verbose: bool = False) -> int:
|
|
94
|
+
"""Display information about an OSIRIS simulation."""
|
|
95
|
+
try:
|
|
96
|
+
# Look for input deck
|
|
97
|
+
input_deck = None
|
|
98
|
+
for candidate in ["os-stdin", "input.deck", "deck.in"]:
|
|
99
|
+
deck_path = simpath / candidate
|
|
100
|
+
if deck_path.exists():
|
|
101
|
+
input_deck = deck_path
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if input_deck is None:
|
|
105
|
+
print(f"Error: No input deck found in {simpath}", file=sys.stderr)
|
|
106
|
+
print(" (Looking for: os-stdin, input.deck, deck.in)", file=sys.stderr)
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
# Load simulation
|
|
110
|
+
sim = ou.Simulation(str(input_deck))
|
|
111
|
+
|
|
112
|
+
print(f"Simulation: {simpath.name}")
|
|
113
|
+
print(f"Input Deck: {input_deck.name}")
|
|
114
|
+
|
|
115
|
+
if not brief:
|
|
116
|
+
print("\nSpecies:")
|
|
117
|
+
for species in sim.species:
|
|
118
|
+
print(f" - {species}")
|
|
119
|
+
|
|
120
|
+
# Scan for available diagnostics
|
|
121
|
+
print("\nAvailable Diagnostics:")
|
|
122
|
+
ms_path = simpath / "MS"
|
|
123
|
+
if ms_path.exists():
|
|
124
|
+
# Check for fields
|
|
125
|
+
fld_path = ms_path / "FLD"
|
|
126
|
+
if fld_path.exists():
|
|
127
|
+
print(" Fields:")
|
|
128
|
+
for item in sorted(fld_path.iterdir()):
|
|
129
|
+
if item.is_dir():
|
|
130
|
+
# Count files in diagnostic
|
|
131
|
+
n_files = len(list(item.glob("*.h5")))
|
|
132
|
+
print(f" - {item.name} ({n_files} timesteps)")
|
|
133
|
+
|
|
134
|
+
# Check for density/current
|
|
135
|
+
for diag_type in ["DENSITY", "CURRENT"]:
|
|
136
|
+
diag_path = ms_path / diag_type
|
|
137
|
+
if diag_path.exists():
|
|
138
|
+
print(f" {diag_type.title()}:")
|
|
139
|
+
for species_dir in sorted(diag_path.iterdir()):
|
|
140
|
+
if species_dir.is_dir():
|
|
141
|
+
for qty_dir in sorted(species_dir.iterdir()):
|
|
142
|
+
if qty_dir.is_dir():
|
|
143
|
+
n_files = len(list(qty_dir.glob("*.h5")))
|
|
144
|
+
print(f" - {species_dir.name}/{qty_dir.name} ({n_files} timesteps)")
|
|
145
|
+
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"Error reading simulation: {e}", file=sys.stderr)
|
|
150
|
+
if verbose:
|
|
151
|
+
raise
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Make args available in scope for error handling
|
|
156
|
+
args = None
|
osiris_utils/cli/plot.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Plot command - quick visualization of OSIRIS data."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import matplotlib
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
import osiris_utils as ou
|
|
12
|
+
|
|
13
|
+
# Use non-interactive backend by default for CLI
|
|
14
|
+
matplotlib.use('Agg')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_parser(subparsers) -> None:
|
|
18
|
+
"""Register the 'plot' subcommand parser."""
|
|
19
|
+
parser = subparsers.add_parser(
|
|
20
|
+
"plot",
|
|
21
|
+
help="Create quick plots from OSIRIS data",
|
|
22
|
+
description="Generate publication-quality plots from OSIRIS simulation data",
|
|
23
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
24
|
+
epilog="""
|
|
25
|
+
Examples:
|
|
26
|
+
osiris plot file.h5 --save plot.png
|
|
27
|
+
osiris plot file.h5 --save plot.png --title "Ez Field"
|
|
28
|
+
osiris plot file.h5 --save plot.png --cmap viridis
|
|
29
|
+
osiris plot file.h5 --display # Show interactive plot
|
|
30
|
+
""",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"path",
|
|
35
|
+
type=str,
|
|
36
|
+
help="Path to OSIRIS HDF5 file",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-s",
|
|
41
|
+
"--save",
|
|
42
|
+
type=str,
|
|
43
|
+
default=None,
|
|
44
|
+
help="Save plot to file (e.g., plot.png)",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--display",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Display plot interactively (requires X server)",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--title",
|
|
55
|
+
type=str,
|
|
56
|
+
default=None,
|
|
57
|
+
help="Plot title (default: auto-generated)",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--cmap",
|
|
62
|
+
type=str,
|
|
63
|
+
default="RdBu_r",
|
|
64
|
+
help="Colormap for 2D plots (default: RdBu_r)",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--dpi",
|
|
69
|
+
type=int,
|
|
70
|
+
default=150,
|
|
71
|
+
help="DPI for saved plots (default: 150)",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--log-scale",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Use logarithmic scale for color/y-axis",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
parser.set_defaults(func=run)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run(args: argparse.Namespace) -> int:
|
|
84
|
+
"""Execute the plot command."""
|
|
85
|
+
path = Path(args.path)
|
|
86
|
+
|
|
87
|
+
if not path.exists():
|
|
88
|
+
print(f"Error: File '{path}' does not exist", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
if not path.is_file():
|
|
92
|
+
print(f"Error: '{path}' is not a file", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
if not args.save and not args.display:
|
|
96
|
+
print("Error: Must specify --save or --display", file=sys.stderr)
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Switch to interactive backend if displaying
|
|
101
|
+
if args.display:
|
|
102
|
+
matplotlib.use('TkAgg')
|
|
103
|
+
|
|
104
|
+
# Load data
|
|
105
|
+
data_obj = ou.OsirisGridFile(str(path))
|
|
106
|
+
|
|
107
|
+
# Create plot based on dimensionality
|
|
108
|
+
if data_obj.dim == 1:
|
|
109
|
+
create_1d_plot(data_obj, args)
|
|
110
|
+
elif data_obj.dim == 2:
|
|
111
|
+
create_2d_plot(data_obj, args)
|
|
112
|
+
else:
|
|
113
|
+
print(f"Error: {data_obj.dim}D plotting not supported yet", file=sys.stderr)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
# Save or display
|
|
117
|
+
if args.save:
|
|
118
|
+
plt.savefig(args.save, dpi=args.dpi, bbox_inches='tight')
|
|
119
|
+
print(f"Plot saved to: {args.save}")
|
|
120
|
+
|
|
121
|
+
if args.display:
|
|
122
|
+
plt.show()
|
|
123
|
+
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"Error creating plot: {e}", file=sys.stderr)
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_1d_plot(data_obj, args: argparse.Namespace) -> None:
|
|
132
|
+
"""Create a 1D line plot."""
|
|
133
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
134
|
+
|
|
135
|
+
# For 1D data, x is the coordinate array directly
|
|
136
|
+
x = data_obj.x
|
|
137
|
+
data = data_obj.data
|
|
138
|
+
|
|
139
|
+
ax.plot(x, data, linewidth=2)
|
|
140
|
+
|
|
141
|
+
# Labels
|
|
142
|
+
if args.title:
|
|
143
|
+
ax.set_title(args.title)
|
|
144
|
+
else:
|
|
145
|
+
ax.set_title(f"${data_obj.label}$ at t = {data_obj.time}")
|
|
146
|
+
|
|
147
|
+
ax.set_xlabel(data_obj.axis[0].get('plot_label', f'x [{data_obj.axis[0]["units"]}]'))
|
|
148
|
+
ax.set_ylabel(f"${data_obj.label}$ [{data_obj.units}]")
|
|
149
|
+
|
|
150
|
+
if args.log_scale:
|
|
151
|
+
ax.set_yscale('log')
|
|
152
|
+
|
|
153
|
+
ax.grid(True, alpha=0.3)
|
|
154
|
+
plt.tight_layout()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def create_2d_plot(data_obj, args: argparse.Namespace) -> None:
|
|
158
|
+
"""Create a 2D heatmap."""
|
|
159
|
+
fig, ax = plt.subplots(figsize=(10, 8))
|
|
160
|
+
|
|
161
|
+
x = data_obj.x[0]
|
|
162
|
+
y = data_obj.x[1]
|
|
163
|
+
data = data_obj.data
|
|
164
|
+
|
|
165
|
+
# Create meshgrid for pcolormesh
|
|
166
|
+
extent = [x[0], x[-1], y[0], y[-1]]
|
|
167
|
+
|
|
168
|
+
if args.log_scale:
|
|
169
|
+
# Use symmetric log scale for data that may have negative values
|
|
170
|
+
vmax = np.abs(data).max()
|
|
171
|
+
norm = matplotlib.colors.SymLogNorm(linthresh=vmax / 100, vmin=-vmax, vmax=vmax)
|
|
172
|
+
im = ax.imshow(data.T, origin='lower', aspect='auto', extent=extent, cmap=args.cmap, norm=norm)
|
|
173
|
+
else:
|
|
174
|
+
im = ax.imshow(data.T, origin='lower', aspect='auto', extent=extent, cmap=args.cmap)
|
|
175
|
+
|
|
176
|
+
# Colorbar
|
|
177
|
+
cbar = plt.colorbar(im, ax=ax)
|
|
178
|
+
cbar.set_label(f"${data_obj.label}$ [{data_obj.units}]")
|
|
179
|
+
|
|
180
|
+
# Labels
|
|
181
|
+
if args.title:
|
|
182
|
+
ax.set_title(args.title)
|
|
183
|
+
else:
|
|
184
|
+
ax.set_title(f"${data_obj.label}$ at t = {data_obj.time}")
|
|
185
|
+
|
|
186
|
+
ax.set_xlabel(data_obj.axis[0].get('plot_label', f'x [{data_obj.axis[0]["units"]}]'))
|
|
187
|
+
ax.set_ylabel(data_obj.axis[1].get('plot_label', f'y [{data_obj.axis[1]["units"]}]'))
|
|
188
|
+
|
|
189
|
+
plt.tight_layout()
|