fullplot 2.0.0__tar.gz → 2.0.1__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.
- {fullplot-2.0.0 → fullplot-2.0.1}/CHANGELOG.md +5 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/PKG-INFO +87 -1
- {fullplot-2.0.0 → fullplot-2.0.1}/README.md +86 -0
- fullplot-2.0.1/examples/hdf5_plotting/15field_results.py +93 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/PLOTTING.md +46 -3
- {fullplot-2.0.0 → fullplot-2.0.1}/pyproject.toml +1 -1
- {fullplot-2.0.0 → fullplot-2.0.1}/pyproject.toml.orig +1 -1
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/__init__.py +1 -1
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/fullplot.py +129 -37
- fullplot-2.0.0/tests/test_imports.py +0 -9
- {fullplot-2.0.0 → fullplot-2.0.1}/LICENSE +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/THIRD_PARTY_LICENSES.md +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/0generate_plotting_data.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/10multidimensional_slices.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/11show_multiple_figures.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/12light_theme.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/13module_level_api.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/14save_formats.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/1inspect_hdf5.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/2single_trace.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/3multiple_traces.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/4dual_axis.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/5heatmap_2d_dataset.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/6heatmap_stacked_1d.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/7log_axes.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/8log_color_map.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/9multidimensional_line_traces.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/0generate_linearization_data.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/1inspect_linearization.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/2read_transient_points.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/LINEARIZATION.md +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/1_generate_map.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/2_axis_types.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/3_metadata_and_outputs.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/4_hdf5_layout.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/MAPS.md +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/0_generate_trace_demo_file.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/10_windowed_trace_keeps_full_time.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/11_shared_time_axis_window.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/1_quick_hdf5_trace_plot.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/2_extract_trace_and_filter.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/3_create_redlines_bluelines.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/4_command_and_sequence_traces.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/5_trace_math_resample_error.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/6_window_scale_offset_derivative.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/7_save_processed_traces.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/8_missing_nan_values_create_gaps.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/9_shared_time_axis_alignment.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/TRACES.md +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/_trace_demo_data.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/cli.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/hdf5.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/linearization.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/maps.py +0 -0
- {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/themes.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
* Added metadata/coordinate-aware numeric field orientation and H5File.surface(), with time-first line slicing and legacy FullFlow array-history reading.
|
|
6
|
+
* Added standalone field plotting examples and tests for generic HDF5, coordinate validation, ambiguity and shared map/surface axes.
|
|
7
|
+
|
|
3
8
|
## 2.0.0
|
|
4
9
|
|
|
5
10
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fullplot
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: HDF5 plotting, trace processing, linearization reading, and map generation for engineering data.
|
|
5
5
|
Keywords: hdf5,plotting,engineering,simulation,test-data,linearization,state-space,map-generation,scientific-computing
|
|
6
6
|
Author: Saaketh Ramoju
|
|
@@ -1079,6 +1079,91 @@ run.map(
|
|
|
1079
1079
|
|
|
1080
1080
|
---
|
|
1081
1081
|
|
|
1082
|
+
## Time-dependent numerical fields and 3D surfaces
|
|
1083
|
+
|
|
1084
|
+
A distributed simulation may record `mass_distribution`, `temperature`,
|
|
1085
|
+
`heat_flux`, or `aero_forces` as multidimensional HDF5 datasets. FullPlot can
|
|
1086
|
+
read these directly; it does not require FullFlow at plotting time.
|
|
1087
|
+
|
|
1088
|
+
For FullFlow 4.0.3 output, numeric field histories have time on axis 0:
|
|
1089
|
+
|
|
1090
|
+
| Field | Dataset shape | Interpretation |
|
|
1091
|
+
| --- | --- | --- |
|
|
1092
|
+
| Mass or temperature | `(time, station)` | One scalar per station per timestep |
|
|
1093
|
+
| Aerodynamic forces | `(time, station, 3)` | Three force components per station |
|
|
1094
|
+
| Fixed final field | `(station,)` or `(station, 3)` | No time axis |
|
|
1095
|
+
|
|
1096
|
+
FullFlow's `State(np.ndarray)` and `StateArray` are saved identically as
|
|
1097
|
+
numerical fields. The HDF5 field has a `time_axis` attribute and a reference to
|
|
1098
|
+
the time dataset. FullPlot uses this information where available, so you do not
|
|
1099
|
+
need to transpose time-first arrays just to make a heat map.
|
|
1100
|
+
|
|
1101
|
+
### Plot a FullFlow field
|
|
1102
|
+
|
|
1103
|
+
```python
|
|
1104
|
+
import fullplot as fplt
|
|
1105
|
+
|
|
1106
|
+
run = fplt.open("2thermal_cells.h5").at("Thermal_Cells/transient")
|
|
1107
|
+
field = "components/Nose_Thermal/temperature"
|
|
1108
|
+
position = "components/Nose_Thermal/stations"
|
|
1109
|
+
|
|
1110
|
+
run.map(
|
|
1111
|
+
z=field,
|
|
1112
|
+
x="time",
|
|
1113
|
+
y=position,
|
|
1114
|
+
xlabel="Time [s]",
|
|
1115
|
+
ylabel="Axial Position [m]",
|
|
1116
|
+
zlabel="Temperature [K]",
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
run.plot(
|
|
1120
|
+
y=field,
|
|
1121
|
+
x="time",
|
|
1122
|
+
slice={1: 3}, # temperature at station index 3
|
|
1123
|
+
xlabel="Time [s]",
|
|
1124
|
+
ylabel="Temperature [K]",
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
run.surface(
|
|
1128
|
+
z=field,
|
|
1129
|
+
x="time",
|
|
1130
|
+
y=position,
|
|
1131
|
+
xlabel="Time [s]",
|
|
1132
|
+
ylabel="Axial Position [m]",
|
|
1133
|
+
zlabel="Temperature [K]",
|
|
1134
|
+
)
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
`map()` renders the third variable as color; `surface()` draws a true 3D
|
|
1138
|
+
Matplotlib surface. Both accept `slice=`, `save=`, `show=`, and the usual title
|
|
1139
|
+
and label arguments. `surface()` returns the figure and 3D axes, not a
|
|
1140
|
+
browser-based interactive Plotly object. For `(time, station, component)`, use
|
|
1141
|
+
`slice={2: 0}` to select a force component before plotting.
|
|
1142
|
+
|
|
1143
|
+
### Axis selection and compatibility
|
|
1144
|
+
|
|
1145
|
+
For a 2D field, `x` and `y` must match its two coordinate dimensions (center or
|
|
1146
|
+
edge arrays are supported where appropriate). FullPlot uses coordinate
|
|
1147
|
+
lengths and available `time_axis`/`time_dataset` metadata to determine the
|
|
1148
|
+
orientation. If both orientations remain possible, provide
|
|
1149
|
+
`orientation="xy"` for data indexed `[x, y]` or `orientation="yx"` for `[y, x]`.
|
|
1150
|
+
A fixed station field recorded on every step may be reduced automatically to
|
|
1151
|
+
its unchanged one-dimensional coordinate array. For generic HDF5 data without
|
|
1152
|
+
axis hints, the established row/column behavior remains available.
|
|
1153
|
+
|
|
1154
|
+
`plot()` selects the time dimension automatically if present in the metadata;
|
|
1155
|
+
for other multidimensional data, supply `axis=` or `slice=` as needed. Numeric
|
|
1156
|
+
JSON-string array histories from older FullFlow HDF5 exports can also be read
|
|
1157
|
+
with `read()` and displayed without a conversion script. Unrelated text fields
|
|
1158
|
+
remain text.
|
|
1159
|
+
|
|
1160
|
+
The standalone example
|
|
1161
|
+
[`15field_results.py`](examples/hdf5_plotting/15field_results.py) creates a
|
|
1162
|
+
numeric HDF5 field and makes a heat map, a trace, and a 3D surface. You can
|
|
1163
|
+
also pass a FullFlow thermal example's HDF5 file as its first argument.
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1082
1167
|
## Map generation
|
|
1083
1168
|
|
|
1084
1169
|
FullPlot can generate simple rectangular-grid HDF5 maps.
|
|
@@ -1228,6 +1313,7 @@ examples/
|
|
|
1228
1313
|
* log axes and log color scales,
|
|
1229
1314
|
* multidimensional line traces,
|
|
1230
1315
|
* multidimensional slices,
|
|
1316
|
+
* native time-first field histories and 3D surface plots,
|
|
1231
1317
|
* light theme plots,
|
|
1232
1318
|
* module-level API calls,
|
|
1233
1319
|
* saving figures.
|
|
@@ -1044,6 +1044,91 @@ run.map(
|
|
|
1044
1044
|
|
|
1045
1045
|
---
|
|
1046
1046
|
|
|
1047
|
+
## Time-dependent numerical fields and 3D surfaces
|
|
1048
|
+
|
|
1049
|
+
A distributed simulation may record `mass_distribution`, `temperature`,
|
|
1050
|
+
`heat_flux`, or `aero_forces` as multidimensional HDF5 datasets. FullPlot can
|
|
1051
|
+
read these directly; it does not require FullFlow at plotting time.
|
|
1052
|
+
|
|
1053
|
+
For FullFlow 4.0.3 output, numeric field histories have time on axis 0:
|
|
1054
|
+
|
|
1055
|
+
| Field | Dataset shape | Interpretation |
|
|
1056
|
+
| --- | --- | --- |
|
|
1057
|
+
| Mass or temperature | `(time, station)` | One scalar per station per timestep |
|
|
1058
|
+
| Aerodynamic forces | `(time, station, 3)` | Three force components per station |
|
|
1059
|
+
| Fixed final field | `(station,)` or `(station, 3)` | No time axis |
|
|
1060
|
+
|
|
1061
|
+
FullFlow's `State(np.ndarray)` and `StateArray` are saved identically as
|
|
1062
|
+
numerical fields. The HDF5 field has a `time_axis` attribute and a reference to
|
|
1063
|
+
the time dataset. FullPlot uses this information where available, so you do not
|
|
1064
|
+
need to transpose time-first arrays just to make a heat map.
|
|
1065
|
+
|
|
1066
|
+
### Plot a FullFlow field
|
|
1067
|
+
|
|
1068
|
+
```python
|
|
1069
|
+
import fullplot as fplt
|
|
1070
|
+
|
|
1071
|
+
run = fplt.open("2thermal_cells.h5").at("Thermal_Cells/transient")
|
|
1072
|
+
field = "components/Nose_Thermal/temperature"
|
|
1073
|
+
position = "components/Nose_Thermal/stations"
|
|
1074
|
+
|
|
1075
|
+
run.map(
|
|
1076
|
+
z=field,
|
|
1077
|
+
x="time",
|
|
1078
|
+
y=position,
|
|
1079
|
+
xlabel="Time [s]",
|
|
1080
|
+
ylabel="Axial Position [m]",
|
|
1081
|
+
zlabel="Temperature [K]",
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
run.plot(
|
|
1085
|
+
y=field,
|
|
1086
|
+
x="time",
|
|
1087
|
+
slice={1: 3}, # temperature at station index 3
|
|
1088
|
+
xlabel="Time [s]",
|
|
1089
|
+
ylabel="Temperature [K]",
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
run.surface(
|
|
1093
|
+
z=field,
|
|
1094
|
+
x="time",
|
|
1095
|
+
y=position,
|
|
1096
|
+
xlabel="Time [s]",
|
|
1097
|
+
ylabel="Axial Position [m]",
|
|
1098
|
+
zlabel="Temperature [K]",
|
|
1099
|
+
)
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
`map()` renders the third variable as color; `surface()` draws a true 3D
|
|
1103
|
+
Matplotlib surface. Both accept `slice=`, `save=`, `show=`, and the usual title
|
|
1104
|
+
and label arguments. `surface()` returns the figure and 3D axes, not a
|
|
1105
|
+
browser-based interactive Plotly object. For `(time, station, component)`, use
|
|
1106
|
+
`slice={2: 0}` to select a force component before plotting.
|
|
1107
|
+
|
|
1108
|
+
### Axis selection and compatibility
|
|
1109
|
+
|
|
1110
|
+
For a 2D field, `x` and `y` must match its two coordinate dimensions (center or
|
|
1111
|
+
edge arrays are supported where appropriate). FullPlot uses coordinate
|
|
1112
|
+
lengths and available `time_axis`/`time_dataset` metadata to determine the
|
|
1113
|
+
orientation. If both orientations remain possible, provide
|
|
1114
|
+
`orientation="xy"` for data indexed `[x, y]` or `orientation="yx"` for `[y, x]`.
|
|
1115
|
+
A fixed station field recorded on every step may be reduced automatically to
|
|
1116
|
+
its unchanged one-dimensional coordinate array. For generic HDF5 data without
|
|
1117
|
+
axis hints, the established row/column behavior remains available.
|
|
1118
|
+
|
|
1119
|
+
`plot()` selects the time dimension automatically if present in the metadata;
|
|
1120
|
+
for other multidimensional data, supply `axis=` or `slice=` as needed. Numeric
|
|
1121
|
+
JSON-string array histories from older FullFlow HDF5 exports can also be read
|
|
1122
|
+
with `read()` and displayed without a conversion script. Unrelated text fields
|
|
1123
|
+
remain text.
|
|
1124
|
+
|
|
1125
|
+
The standalone example
|
|
1126
|
+
[`15field_results.py`](examples/hdf5_plotting/15field_results.py) creates a
|
|
1127
|
+
numeric HDF5 field and makes a heat map, a trace, and a 3D surface. You can
|
|
1128
|
+
also pass a FullFlow thermal example's HDF5 file as its first argument.
|
|
1129
|
+
|
|
1130
|
+
---
|
|
1131
|
+
|
|
1047
1132
|
## Map generation
|
|
1048
1133
|
|
|
1049
1134
|
FullPlot can generate simple rectangular-grid HDF5 maps.
|
|
@@ -1193,6 +1278,7 @@ examples/
|
|
|
1193
1278
|
* log axes and log color scales,
|
|
1194
1279
|
* multidimensional line traces,
|
|
1195
1280
|
* multidimensional slices,
|
|
1281
|
+
* native time-first field histories and 3D surface plots,
|
|
1196
1282
|
* light theme plots,
|
|
1197
1283
|
* module-level API calls,
|
|
1198
1284
|
* saving figures.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Numeric Field Histories: Heat Map, Trace, and 3D Surface
|
|
3
|
+
======================================================
|
|
4
|
+
|
|
5
|
+
This example plots a temperature distribution with time and axial position as
|
|
6
|
+
coordinates. It creates a small standalone HDF5 dataset, so FullFlow is not
|
|
7
|
+
required to run it.
|
|
8
|
+
|
|
9
|
+
To use a real FullFlow result instead, pass its HDF5 filename as an argument:
|
|
10
|
+
|
|
11
|
+
python examples/hdf5_plotting/15field_results.py 2thermal_cells.h5
|
|
12
|
+
|
|
13
|
+
FullFlow 4.0.3 stores field histories with time on the first axis. FullPlot
|
|
14
|
+
reads the axis metadata and handles the orientation automatically.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import h5py
|
|
21
|
+
import numpy as np
|
|
22
|
+
import fullplot as fplt
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
filename = Path(__file__).with_suffix(".h5")
|
|
26
|
+
output_dir = Path(__file__).parent
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Generate an example dataset, or read an existing FullFlow result
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
if len(sys.argv) > 1:
|
|
34
|
+
file = fplt.open(sys.argv[1]).at("Thermal_Cells/transient")
|
|
35
|
+
temperature = "components/Nose_Thermal/temperature"
|
|
36
|
+
stations = "components/Nose_Thermal/stations"
|
|
37
|
+
else:
|
|
38
|
+
time = np.linspace(0.0, 5.0, 51)
|
|
39
|
+
positions = np.linspace(0.0, 1.0, 8)
|
|
40
|
+
field = 293.15 + 12.0 * (1.0 - np.exp(-time[:, None])) * (1.0 + positions[None, :])
|
|
41
|
+
|
|
42
|
+
with h5py.File(filename, "w") as h5:
|
|
43
|
+
h5.create_dataset("time", data=time)
|
|
44
|
+
h5.create_dataset("stations", data=positions)
|
|
45
|
+
dataset = h5.create_dataset("temperature", data=field)
|
|
46
|
+
dataset.attrs["time_axis"] = 0
|
|
47
|
+
dataset.attrs["time_dataset"] = "/time"
|
|
48
|
+
|
|
49
|
+
file = fplt.open(filename)
|
|
50
|
+
temperature = "temperature"
|
|
51
|
+
stations = "stations"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Visualizations
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
file.map(
|
|
59
|
+
z=temperature,
|
|
60
|
+
x="time",
|
|
61
|
+
y=stations,
|
|
62
|
+
xlabel="Time [s]",
|
|
63
|
+
ylabel="Axial Position [m]",
|
|
64
|
+
zlabel="Temperature [K]",
|
|
65
|
+
title="Temperature Distribution",
|
|
66
|
+
save=output_dir / "15temperature_map.png",
|
|
67
|
+
show=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
file.plot(
|
|
71
|
+
y=temperature,
|
|
72
|
+
x="time",
|
|
73
|
+
slice={1: 3},
|
|
74
|
+
xlabel="Time [s]",
|
|
75
|
+
ylabel="Temperature [K]",
|
|
76
|
+
title="Cell 3 Temperature",
|
|
77
|
+
save=output_dir / "15temperature_trace.png",
|
|
78
|
+
show=False,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
file.surface(
|
|
82
|
+
z=temperature,
|
|
83
|
+
x="time",
|
|
84
|
+
y=stations,
|
|
85
|
+
xlabel="Time [s]",
|
|
86
|
+
ylabel="Axial Position [m]",
|
|
87
|
+
zlabel="Temperature [K]",
|
|
88
|
+
title="Temperature Distribution Surface",
|
|
89
|
+
save=output_dir / "15temperature_surface.png",
|
|
90
|
+
show=False,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
fplt.show()
|
|
@@ -7,13 +7,13 @@ The repository ignores `*.h5` files, so the example file is generated locally.
|
|
|
7
7
|
Run the generator first:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
python examples/
|
|
10
|
+
python examples/hdf5_plotting/0generate_plotting_data.py
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Then run any plotting example:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
python examples/
|
|
16
|
+
python examples/hdf5_plotting/5heatmap_2d_dataset.py
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The examples intentionally use plain script-style code. They are written the way
|
|
@@ -24,7 +24,7 @@ a user would typically inspect and plot an HDF5 file.
|
|
|
24
24
|
`0generate_plotting_data.py` creates:
|
|
25
25
|
|
|
26
26
|
```text
|
|
27
|
-
examples/
|
|
27
|
+
examples/hdf5_plotting/plotting_demo.h5
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
The file contains:
|
|
@@ -64,6 +64,7 @@ The most important datasets are:
|
|
|
64
64
|
8log_color_map.py Demonstrates zscale="log" for a heat map colorbar.
|
|
65
65
|
9multidimensional_line_traces.py Plots pressure_map[station, time] as lines.
|
|
66
66
|
10multidimensional_slices.py Slices pressure_3d[case, station, time].
|
|
67
|
+
15field_results.py Plots a time-first field as a heat map, trace, and 3D surface.
|
|
67
68
|
11show_multiple_figures.py Uses show=False and fplt.show().
|
|
68
69
|
12light_theme.py Uses theme="light".
|
|
69
70
|
13module_level_api.py Uses fplt.plot() and fplt.map() directly.
|
|
@@ -118,3 +119,45 @@ then `slice={0: 1}` selects case 1 and leaves:
|
|
|
118
119
|
```text
|
|
119
120
|
pressure_3d[station, time]
|
|
120
121
|
```
|
|
122
|
+
|
|
123
|
+
## Time-dependent numerical fields
|
|
124
|
+
|
|
125
|
+
FullFlow 4.0.3 records both calculated array-valued States and groups of
|
|
126
|
+
individual scalar States as native numeric fields. Time is the first dimension
|
|
127
|
+
of full-history datasets. Example shapes include `(time, station)` for cell
|
|
128
|
+
masses or temperatures and `(time, station, 3)` for three-component forces.
|
|
129
|
+
FullPlot reads these numeric datasets without converting or unpacking them.
|
|
130
|
+
|
|
131
|
+
`15field_results.py` creates a standalone HDF5 field and demonstrates all
|
|
132
|
+
three visualizations:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import fullplot as fplt
|
|
136
|
+
|
|
137
|
+
file = fplt.open("15field_results.h5")
|
|
138
|
+
file.map(z="temperature", x="time", y="stations")
|
|
139
|
+
file.plot(y="temperature", x="time", slice={1: 3})
|
|
140
|
+
file.surface(z="temperature", x="time", y="stations")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
To use a real FullFlow result instead, first run FullFlow's
|
|
144
|
+
`examples/arrays/2thermal_cells.py`, then pass its resulting HDF5 filename:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python examples/hdf5_plotting/15field_results.py ../FullFlow/2thermal_cells.h5
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A FullFlow field includes `time_axis` and `time_dataset` metadata so plotting
|
|
151
|
+
can automatically distinguish time from position, even for square fields.
|
|
152
|
+
Alternatively, `map()` and `surface()` accept `orientation="xy"` or
|
|
153
|
+
`orientation="yx"` to resolve ambiguous generic HDF5 data. Both allow center
|
|
154
|
+
or edge coordinates, `slice=`, `save=`, and `show=`; `surface()` returns
|
|
155
|
+
Matplotlib 3D axes. For a 3D dataset, `slice={2: 0}` selects one vector
|
|
156
|
+
component, leaving a time-by-position field.
|
|
157
|
+
|
|
158
|
+
For 1D traces from multidimensional data, `plot()` uses the recorded time axis
|
|
159
|
+
when metadata is present; otherwise use `axis=` explicitly. FullPlot also
|
|
160
|
+
decodes compatible numeric JSON array histories from older FullFlow files.
|
|
161
|
+
|
|
162
|
+
See the [FullPlot README](../../README.md#time-dependent-numerical-fields-and-3d-surfaces)
|
|
163
|
+
for the full discussion of coordinates, orientation, and legacy data.
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import builtins
|
|
4
4
|
import itertools
|
|
5
|
+
import json
|
|
5
6
|
import posixpath
|
|
6
7
|
from contextlib import nullcontext
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -1275,6 +1276,33 @@ def _to_numpy_index(value):
|
|
|
1275
1276
|
return value
|
|
1276
1277
|
|
|
1277
1278
|
|
|
1279
|
+
def _dataset_array(dataset):
|
|
1280
|
+
"""Read native values, with a narrow fallback for old FullFlow array histories."""
|
|
1281
|
+
raw = np.asarray(dataset[()])
|
|
1282
|
+
if (dataset.file.attrs.get("fullflow_format") != "fullflow-simple-hdf5"
|
|
1283
|
+
or raw.ndim != 1 or raw.dtype.kind not in "OSU" or not raw.size):
|
|
1284
|
+
return raw
|
|
1285
|
+
try:
|
|
1286
|
+
values = [None if not value else np.asarray(json.loads(value)) for value in raw]
|
|
1287
|
+
present = [value for value in values if value is not None]
|
|
1288
|
+
if not present or any(value.ndim == 0 or value.dtype.kind not in "biufc"
|
|
1289
|
+
or value.shape != present[0].shape for value in present):
|
|
1290
|
+
return raw
|
|
1291
|
+
dtype = np.result_type(*(value.dtype for value in present), float)
|
|
1292
|
+
return np.stack([np.full(present[0].shape, np.nan, dtype=dtype)
|
|
1293
|
+
if value is None else value for value in values])
|
|
1294
|
+
except (ValueError, TypeError):
|
|
1295
|
+
return raw
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
def _time_dimension(dataset, array):
|
|
1299
|
+
if "time_axis" in dataset.attrs:
|
|
1300
|
+
return int(dataset.attrs["time_axis"])
|
|
1301
|
+
if dataset.ndim == 1 and array.ndim > 1 and dataset.dtype.kind in "OSU":
|
|
1302
|
+
return 0
|
|
1303
|
+
return None
|
|
1304
|
+
|
|
1305
|
+
|
|
1278
1306
|
def _apply_slice_for_lines(array: np.ndarray, axis: int, slice_spec) -> tuple[np.ndarray, int]:
|
|
1279
1307
|
array = np.asarray(array)
|
|
1280
1308
|
|
|
@@ -1510,14 +1538,16 @@ class H5File:
|
|
|
1510
1538
|
def read(self, name: str):
|
|
1511
1539
|
"""Read one dataset by absolute path, relative path, or unique short name.
|
|
1512
1540
|
|
|
1513
|
-
|
|
1541
|
+
Native datasets retain their NumPy values. Legacy FullFlow JSON array
|
|
1542
|
+
histories are decoded when every sample has the same numeric shape. Use
|
|
1514
1543
|
``trace()`` or ``time()`` when the dataset should become a FullPlot
|
|
1515
1544
|
object with validation and metadata.
|
|
1516
1545
|
"""
|
|
1517
1546
|
|
|
1518
1547
|
with h5py.File(self.filename, "r") as h5:
|
|
1519
1548
|
path = self._resolve_dataset_path(h5, name)
|
|
1520
|
-
|
|
1549
|
+
dataset = h5[path]
|
|
1550
|
+
return dataset[()] if dataset.shape == () else _dataset_array(dataset)
|
|
1521
1551
|
|
|
1522
1552
|
def time(self, name: str = "time") -> TimeAxis:
|
|
1523
1553
|
"""Read a one-dimensional HDF5 dataset as a shared ``TimeAxis``.
|
|
@@ -1659,7 +1689,7 @@ class H5File:
|
|
|
1659
1689
|
x=None,
|
|
1660
1690
|
y2=None,
|
|
1661
1691
|
slice=None,
|
|
1662
|
-
axis: int =
|
|
1692
|
+
axis: int | None = None,
|
|
1663
1693
|
labels=None,
|
|
1664
1694
|
y2labels=None,
|
|
1665
1695
|
xlabel: str | None = None,
|
|
@@ -1902,6 +1932,7 @@ class H5File:
|
|
|
1902
1932
|
dpi: int = 200,
|
|
1903
1933
|
show: bool = True,
|
|
1904
1934
|
figsize=(8, 6),
|
|
1935
|
+
orientation: str | None = None,
|
|
1905
1936
|
):
|
|
1906
1937
|
"""
|
|
1907
1938
|
Plot a 2D dataset as a map.
|
|
@@ -1934,35 +1965,8 @@ class H5File:
|
|
|
1934
1965
|
theme = check_theme(theme)
|
|
1935
1966
|
|
|
1936
1967
|
with h5py.File(self.filename, "r") as h5:
|
|
1937
|
-
z_array, z_name = self.
|
|
1938
|
-
|
|
1939
|
-
z_array = _apply_slice_for_map(z_array, slice)
|
|
1940
|
-
|
|
1941
|
-
if z_array.ndim != 2:
|
|
1942
|
-
raise PlotDataError(
|
|
1943
|
-
f"map requires 2D data after slicing. "
|
|
1944
|
-
f"Selected data has shape {z_array.shape}."
|
|
1945
|
-
)
|
|
1946
|
-
|
|
1947
|
-
rows, cols = z_array.shape
|
|
1948
|
-
|
|
1949
|
-
x_array, x_name = self._read_map_axis(
|
|
1950
|
-
h5=h5,
|
|
1951
|
-
value=x,
|
|
1952
|
-
default=np.arange(cols),
|
|
1953
|
-
default_name="Column Index",
|
|
1954
|
-
valid_lengths=(cols, cols + 1),
|
|
1955
|
-
axis_name="x",
|
|
1956
|
-
)
|
|
1957
|
-
|
|
1958
|
-
y_array, y_name = self._read_map_axis(
|
|
1959
|
-
h5=h5,
|
|
1960
|
-
value=y,
|
|
1961
|
-
default=np.arange(rows),
|
|
1962
|
-
default_name="Row Index",
|
|
1963
|
-
valid_lengths=(rows, rows + 1),
|
|
1964
|
-
axis_name="y",
|
|
1965
|
-
)
|
|
1968
|
+
x_array, y_array, z_array, x_name, y_name, z_name = self._field_data(
|
|
1969
|
+
h5, z, x, y, slice, orientation)
|
|
1966
1970
|
|
|
1967
1971
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1968
1972
|
apply_theme(fig, ax, theme)
|
|
@@ -2026,6 +2030,87 @@ class H5File:
|
|
|
2026
2030
|
|
|
2027
2031
|
return fig, ax
|
|
2028
2032
|
|
|
2033
|
+
def _field_data(self, h5, z, x, y, slice_spec, orientation):
|
|
2034
|
+
if orientation not in (None, "xy", "yx"):
|
|
2035
|
+
raise PlotDataError("orientation must be 'xy', 'yx', or None.")
|
|
2036
|
+
data, name = self._read_map_z(h5, z)
|
|
2037
|
+
time_axis = None
|
|
2038
|
+
time_path = None
|
|
2039
|
+
if isinstance(z, str):
|
|
2040
|
+
dataset = h5[self._resolve_dataset_path(h5, z)]
|
|
2041
|
+
time_axis = _time_dimension(dataset, data)
|
|
2042
|
+
time_path = dataset.attrs.get("time_dataset")
|
|
2043
|
+
if time_path is None and time_axis is not None and "time" in dataset.parent:
|
|
2044
|
+
time_path = dataset.parent["time"].name
|
|
2045
|
+
normalized = _normalize_slice_spec(slice_spec, data.ndim)
|
|
2046
|
+
remaining = [axis for axis in range(data.ndim)
|
|
2047
|
+
if not _is_integer_index(normalized.get(axis, builtins.slice(None)))]
|
|
2048
|
+
time_axis = remaining.index(time_axis) if time_axis in remaining else None
|
|
2049
|
+
data = _apply_slice_for_map(data, slice_spec)
|
|
2050
|
+
if data.ndim != 2:
|
|
2051
|
+
raise PlotDataError(f"Field plotting requires 2D data after slicing; got {data.shape}.")
|
|
2052
|
+
if x is None and time_axis is not None and time_path is not None:
|
|
2053
|
+
x = time_path
|
|
2054
|
+
x_values, x_name = self._read_map_axis(h5, x, None, "Column Index", None, "x")
|
|
2055
|
+
y_values, y_name = self._read_map_axis(h5, y, None, "Row Index", None, "y")
|
|
2056
|
+
candidates = []
|
|
2057
|
+
for candidate in ("yx", "xy"):
|
|
2058
|
+
rows, cols = data.shape if candidate == "yx" else data.shape[::-1]
|
|
2059
|
+
if ((x_values is None or len(x_values) in (cols, cols + 1))
|
|
2060
|
+
and (y_values is None or len(y_values) in (rows, rows + 1))):
|
|
2061
|
+
candidates.append(candidate)
|
|
2062
|
+
hint = None
|
|
2063
|
+
if time_axis is not None and time_path is not None:
|
|
2064
|
+
for coordinate, is_x in ((x, True), (y, False)):
|
|
2065
|
+
if isinstance(coordinate, str) and h5[self._resolve_dataset_path(h5, coordinate)] == h5[time_path]:
|
|
2066
|
+
hint = "xy" if (time_axis == 0) == is_x else "yx"
|
|
2067
|
+
if not candidates:
|
|
2068
|
+
raise PlotDataError(f"Coordinate lengths do not match field shape {data.shape}.")
|
|
2069
|
+
if orientation is None:
|
|
2070
|
+
if hint is not None:
|
|
2071
|
+
orientation = hint
|
|
2072
|
+
elif len(candidates) == 1:
|
|
2073
|
+
orientation = candidates[0]
|
|
2074
|
+
elif x is None and y is None:
|
|
2075
|
+
orientation = "yx"
|
|
2076
|
+
else:
|
|
2077
|
+
raise PlotDataError("Ambiguous field orientation; specify orientation='xy' (x first) or 'yx' (y first).")
|
|
2078
|
+
if orientation not in candidates:
|
|
2079
|
+
raise PlotDataError(f"Coordinate lengths do not match field shape {data.shape} and orientation {orientation!r}.")
|
|
2080
|
+
if orientation == "xy":
|
|
2081
|
+
data = data.T
|
|
2082
|
+
if x_values is None:
|
|
2083
|
+
x_values = np.arange(data.shape[1])
|
|
2084
|
+
if y_values is None:
|
|
2085
|
+
y_values = np.arange(data.shape[0])
|
|
2086
|
+
return x_values, y_values, data, x_name, y_name, name
|
|
2087
|
+
|
|
2088
|
+
def surface(self, z, x=None, y=None, slice=None, *, orientation=None,
|
|
2089
|
+
xlabel=None, ylabel=None, zlabel=None, title=None, theme="dark",
|
|
2090
|
+
save=None, dpi=200, show=False, figsize=(9, 6)):
|
|
2091
|
+
"""Plot a numerical 3D surface using the same axes and orientation as map()."""
|
|
2092
|
+
theme = check_theme(theme)
|
|
2093
|
+
with h5py.File(self.filename, "r") as h5:
|
|
2094
|
+
xs, ys, values, x_name, y_name, z_name = self._field_data(
|
|
2095
|
+
h5, z, x, y, slice, orientation)
|
|
2096
|
+
if len(xs) == values.shape[1] + 1:
|
|
2097
|
+
xs = (xs[:-1] + xs[1:]) / 2
|
|
2098
|
+
if len(ys) == values.shape[0] + 1:
|
|
2099
|
+
ys = (ys[:-1] + ys[1:]) / 2
|
|
2100
|
+
xx, yy = np.meshgrid(xs, ys)
|
|
2101
|
+
fig = plt.figure(figsize=figsize)
|
|
2102
|
+
ax = fig.add_subplot(111, projection="3d")
|
|
2103
|
+
apply_theme(fig, ax, theme)
|
|
2104
|
+
ax.plot_surface(xx, yy, values, cmap=theme_settings(theme)["map_cmap"])
|
|
2105
|
+
ax.set_xlabel(xlabel or x_name)
|
|
2106
|
+
ax.set_ylabel(ylabel or y_name)
|
|
2107
|
+
ax.set_zlabel(zlabel or z_name)
|
|
2108
|
+
if title is not None:
|
|
2109
|
+
ax.set_title(title)
|
|
2110
|
+
fig.tight_layout()
|
|
2111
|
+
self._save_and_show(fig, save, dpi, show)
|
|
2112
|
+
return fig, ax
|
|
2113
|
+
|
|
2029
2114
|
def _resolve_dataset_path(self, h5: h5py.File, selector: str) -> str:
|
|
2030
2115
|
if not isinstance(selector, str):
|
|
2031
2116
|
raise DatasetNotFoundError(f"Dataset selector must be a string, got {type(selector)}.")
|
|
@@ -2239,7 +2324,10 @@ class H5File:
|
|
|
2239
2324
|
path = self._resolve_dataset_path(h5, selector)
|
|
2240
2325
|
base_label = _basename(path)
|
|
2241
2326
|
|
|
2242
|
-
array =
|
|
2327
|
+
array = _dataset_array(h5[path])
|
|
2328
|
+
if axis is None:
|
|
2329
|
+
time_axis = _time_dimension(h5[path], array)
|
|
2330
|
+
axis = -1 if time_axis is None else time_axis
|
|
2243
2331
|
|
|
2244
2332
|
if not _is_numeric_dtype(array.dtype):
|
|
2245
2333
|
raise PlotDataError(f"Dataset {path!r} is not numeric.")
|
|
@@ -2321,7 +2409,7 @@ class H5File:
|
|
|
2321
2409
|
|
|
2322
2410
|
for selector in z:
|
|
2323
2411
|
path = self._resolve_dataset_path(h5, selector)
|
|
2324
|
-
array =
|
|
2412
|
+
array = _dataset_array(h5[path])
|
|
2325
2413
|
|
|
2326
2414
|
if not _is_numeric_dtype(array.dtype):
|
|
2327
2415
|
raise PlotDataError(f"Dataset {path!r} is not numeric.")
|
|
@@ -2343,7 +2431,7 @@ class H5File:
|
|
|
2343
2431
|
|
|
2344
2432
|
z_path = self._resolve_dataset_path(h5, z)
|
|
2345
2433
|
z_name = _basename(z_path)
|
|
2346
|
-
z_array =
|
|
2434
|
+
z_array = _dataset_array(h5[z_path])
|
|
2347
2435
|
|
|
2348
2436
|
if not _is_numeric_dtype(z_array.dtype):
|
|
2349
2437
|
raise PlotDataError("z must be numeric.")
|
|
@@ -2356,8 +2444,12 @@ class H5File:
|
|
|
2356
2444
|
|
|
2357
2445
|
if isinstance(value, str):
|
|
2358
2446
|
path = self._resolve_dataset_path(h5, value)
|
|
2359
|
-
array =
|
|
2447
|
+
array = _dataset_array(h5[path])
|
|
2360
2448
|
name = _basename(path)
|
|
2449
|
+
if array.ndim == 2 and _time_dimension(h5[path], array) == 0:
|
|
2450
|
+
if not len(array) or not np.array_equal(array, np.broadcast_to(array[0], array.shape), equal_nan=True):
|
|
2451
|
+
raise PlotDataError(f"{axis_name} coordinate history must be constant over time.")
|
|
2452
|
+
array = array[0]
|
|
2361
2453
|
else:
|
|
2362
2454
|
array = np.asarray(value)
|
|
2363
2455
|
name = default_name
|
|
@@ -2368,7 +2460,7 @@ class H5File:
|
|
|
2368
2460
|
if not _is_numeric_dtype(array.dtype):
|
|
2369
2461
|
raise PlotDataError(f"{axis_name} must be numeric.")
|
|
2370
2462
|
|
|
2371
|
-
if len(array) not in valid_lengths:
|
|
2463
|
+
if valid_lengths is not None and len(array) not in valid_lengths:
|
|
2372
2464
|
valid_text = " or ".join(str(length) for length in valid_lengths)
|
|
2373
2465
|
raise PlotDataError(
|
|
2374
2466
|
f"{axis_name} has length {len(array)}, but expected length {valid_text}."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|