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.
Files changed (52) hide show
  1. benchmarks/benchmark_hdf5_io.py +46 -0
  2. benchmarks/benchmark_load_all.py +54 -0
  3. docs/source/api/decks.rst +48 -0
  4. docs/source/api/postprocess.rst +66 -2
  5. docs/source/api/sim_diag.rst +1 -1
  6. docs/source/api/utilities.rst +1 -1
  7. docs/source/conf.py +2 -1
  8. docs/source/examples/example_Derivatives.md +78 -0
  9. docs/source/examples/example_FFT.md +152 -0
  10. docs/source/examples/example_InputDeck.md +148 -0
  11. docs/source/examples/example_Simulation_Diagnostic.md +213 -0
  12. docs/source/examples/quick_start.md +51 -0
  13. docs/source/examples.rst +14 -0
  14. docs/source/index.rst +8 -0
  15. examples/edited-deck.1d +1 -1
  16. examples/example_Derivatives.ipynb +24 -36
  17. examples/example_FFT.ipynb +44 -23
  18. examples/example_InputDeck.ipynb +24 -277
  19. examples/example_Simulation_Diagnostic.ipynb +27 -17
  20. examples/quick_start.ipynb +17 -1
  21. osiris_utils/__init__.py +10 -6
  22. osiris_utils/cli/__init__.py +6 -0
  23. osiris_utils/cli/__main__.py +85 -0
  24. osiris_utils/cli/export.py +199 -0
  25. osiris_utils/cli/info.py +156 -0
  26. osiris_utils/cli/plot.py +189 -0
  27. osiris_utils/cli/validate.py +247 -0
  28. osiris_utils/data/__init__.py +15 -0
  29. osiris_utils/data/data.py +41 -171
  30. osiris_utils/data/diagnostic.py +285 -274
  31. osiris_utils/data/simulation.py +20 -13
  32. osiris_utils/decks/__init__.py +4 -0
  33. osiris_utils/decks/decks.py +83 -8
  34. osiris_utils/decks/species.py +12 -9
  35. osiris_utils/postprocessing/__init__.py +28 -0
  36. osiris_utils/postprocessing/derivative.py +317 -106
  37. osiris_utils/postprocessing/fft.py +135 -24
  38. osiris_utils/postprocessing/field_centering.py +28 -14
  39. osiris_utils/postprocessing/heatflux_correction.py +39 -18
  40. osiris_utils/postprocessing/mft.py +10 -2
  41. osiris_utils/postprocessing/postprocess.py +8 -5
  42. osiris_utils/postprocessing/pressure_correction.py +29 -17
  43. osiris_utils/utils.py +26 -17
  44. osiris_utils/vis/__init__.py +3 -0
  45. osiris_utils/vis/plot3d.py +148 -0
  46. {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/METADATA +55 -7
  47. {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/RECORD +51 -34
  48. {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/WHEEL +1 -1
  49. osiris_utils-1.2.0.dist-info/entry_points.txt +2 -0
  50. {osiris_utils-1.1.10a0.dist-info → osiris_utils-1.2.0.dist-info}/top_level.txt +1 -0
  51. osiris_utils/postprocessing/mft_for_gridfile.py +0 -55
  52. {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)
@@ -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
@@ -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()