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.
Files changed (55) hide show
  1. {fullplot-2.0.0 → fullplot-2.0.1}/CHANGELOG.md +5 -0
  2. {fullplot-2.0.0 → fullplot-2.0.1}/PKG-INFO +87 -1
  3. {fullplot-2.0.0 → fullplot-2.0.1}/README.md +86 -0
  4. fullplot-2.0.1/examples/hdf5_plotting/15field_results.py +93 -0
  5. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/PLOTTING.md +46 -3
  6. {fullplot-2.0.0 → fullplot-2.0.1}/pyproject.toml +1 -1
  7. {fullplot-2.0.0 → fullplot-2.0.1}/pyproject.toml.orig +1 -1
  8. {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/__init__.py +1 -1
  9. {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/fullplot.py +129 -37
  10. fullplot-2.0.0/tests/test_imports.py +0 -9
  11. {fullplot-2.0.0 → fullplot-2.0.1}/LICENSE +0 -0
  12. {fullplot-2.0.0 → fullplot-2.0.1}/THIRD_PARTY_LICENSES.md +0 -0
  13. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/0generate_plotting_data.py +0 -0
  14. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/10multidimensional_slices.py +0 -0
  15. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/11show_multiple_figures.py +0 -0
  16. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/12light_theme.py +0 -0
  17. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/13module_level_api.py +0 -0
  18. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/14save_formats.py +0 -0
  19. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/1inspect_hdf5.py +0 -0
  20. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/2single_trace.py +0 -0
  21. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/3multiple_traces.py +0 -0
  22. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/4dual_axis.py +0 -0
  23. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/5heatmap_2d_dataset.py +0 -0
  24. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/6heatmap_stacked_1d.py +0 -0
  25. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/7log_axes.py +0 -0
  26. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/8log_color_map.py +0 -0
  27. {fullplot-2.0.0 → fullplot-2.0.1}/examples/hdf5_plotting/9multidimensional_line_traces.py +0 -0
  28. {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/0generate_linearization_data.py +0 -0
  29. {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/1inspect_linearization.py +0 -0
  30. {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/2read_transient_points.py +0 -0
  31. {fullplot-2.0.0 → fullplot-2.0.1}/examples/linearization/LINEARIZATION.md +0 -0
  32. {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/1_generate_map.py +0 -0
  33. {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/2_axis_types.py +0 -0
  34. {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/3_metadata_and_outputs.py +0 -0
  35. {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/4_hdf5_layout.py +0 -0
  36. {fullplot-2.0.0 → fullplot-2.0.1}/examples/maps/MAPS.md +0 -0
  37. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/0_generate_trace_demo_file.py +0 -0
  38. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/10_windowed_trace_keeps_full_time.py +0 -0
  39. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/11_shared_time_axis_window.py +0 -0
  40. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/1_quick_hdf5_trace_plot.py +0 -0
  41. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/2_extract_trace_and_filter.py +0 -0
  42. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/3_create_redlines_bluelines.py +0 -0
  43. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/4_command_and_sequence_traces.py +0 -0
  44. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/5_trace_math_resample_error.py +0 -0
  45. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/6_window_scale_offset_derivative.py +0 -0
  46. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/7_save_processed_traces.py +0 -0
  47. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/8_missing_nan_values_create_gaps.py +0 -0
  48. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/9_shared_time_axis_alignment.py +0 -0
  49. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/TRACES.md +0 -0
  50. {fullplot-2.0.0 → fullplot-2.0.1}/examples/traces/_trace_demo_data.py +0 -0
  51. {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/cli.py +0 -0
  52. {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/hdf5.py +0 -0
  53. {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/linearization.py +0 -0
  54. {fullplot-2.0.0 → fullplot-2.0.1}/src/fullplot/maps.py +0 -0
  55. {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.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/plotting/0generate_plotting_data.py
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/plotting/5heatmap_2d_dataset.py
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/plotting/plotting_demo.h5
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fullplot"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  description = "HDF5 plotting, trace processing, linearization reading, and map generation for engineering data."
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-only"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fullplot"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  description = "HDF5 plotting, trace processing, linearization reading, and map generation for engineering data."
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-only"
@@ -29,7 +29,7 @@ from fullplot.maps import (
29
29
  generate_map,
30
30
  )
31
31
 
32
- __version__ = "2.0.0"
32
+ __version__ = "2.0.1"
33
33
 
34
34
  __all__ = [
35
35
  "__version__",
@@ -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
- The returned object is the raw NumPy value from ``h5py``. Use
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
- return h5[path][()]
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 = -1,
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._read_map_z(h5, z)
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 = np.asarray(h5[path][()])
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 = np.asarray(h5[path][()])
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 = np.asarray(h5[z_path][()])
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 = np.asarray(h5[path][()])
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}."
@@ -1,9 +0,0 @@
1
- import fullplot as fplt
2
-
3
-
4
- def test_public_api_imports():
5
- assert fplt.Trace is not None
6
- assert fplt.H5File is not None
7
- assert fplt.Axis is not None
8
- assert fplt.generate_map is not None
9
- assert isinstance(fplt.__version__, str)
File without changes
File without changes
File without changes
File without changes
File without changes