Skip to content

Commit 62de095

Browse files
sanathkeshavGitHub Enterprise
authored and
GitHub Enterprise
committed
Merge pull request #21 from DAE/dev
Dev
2 parents 99cafd2 + cdc1aea commit 62de095

File tree

10 files changed

+811
-220
lines changed

10 files changed

+811
-220
lines changed

FANS_Dashboard/FANS_Dashboard.ipynb

Lines changed: 342 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 77 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import h5py
22
import numpy as np
33
from collections import defaultdict
4+
from postprocessing import compute_rank2tensor_measures
45

56
def recursively_find_structure(group, current_path=""):
67
"""
@@ -106,118 +107,91 @@ def extract_and_organize_data(file_path, hierarchy, quantities_to_load, microstr
106107

107108
return organized_data
108109

109-
import plotly.graph_objects as go
110-
from plotly.subplots import make_subplots
111110

112-
def plot_subplots(data1, data2, labels, title="Subplot Grid", nrows=None, ncols=None):
111+
112+
113+
114+
115+
def write_processed_data_to_h5(file_path, processed_data, overwrite=True):
113116
"""
114-
Plot a grid of subplots using Plotly, handling both single-component (scalar vs scalar) and multi-component data.
117+
Writes processed data back into the HDF5 file at the correct locations.
115118
116119
Parameters:
117-
- data1: numpy array, first set of data to plot (e.g., strain, time)
118-
- data2: numpy array, second set of data to plot (e.g., stress)
119-
- labels: tuple of strings, labels for the x and y axes (e.g., ("Strain", "Stress"))
120-
- title: string, title of the overall plot
121-
- nrows: int, number of rows in the subplot grid (optional)
122-
- ncols: int, number of columns in the subplot grid (optional)
123-
"""
124-
# Ensure data1 and data2 are 2D arrays
125-
if data1.ndim == 1:
126-
data1 = data1[:, np.newaxis]
127-
if data2.ndim == 1:
128-
data2 = data2[:, np.newaxis]
129-
130-
# Set the number of components based on data shape
131-
n_components = data1.shape[1]
132-
133-
# If nrows or ncols is not specified, determine an optimal grid layout
134-
if nrows is None or ncols is None:
135-
nrows = int(np.ceil(np.sqrt(n_components)))
136-
ncols = int(np.ceil(n_components / nrows))
120+
- file_path: str, path to the HDF5 file.
121+
- processed_data: dict, structured data with computed measures to write back.
122+
- overwrite: bool, whether to overwrite existing datasets.
137123
138-
# Create the subplot figure
139-
fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=[f'Component {i+1}' for i in range(n_components)])
140-
141-
# Add traces for each component
142-
for i in range(n_components):
143-
row = i // ncols + 1
144-
col = i % ncols + 1
145-
fig.add_trace(go.Scatter(
146-
x=data1[:, i],
147-
y=data2[:, i],
148-
mode='lines+markers',
149-
marker=dict(symbol='x', size=4),
150-
line=dict(width=1),
151-
name=f'Component {i+1}'
152-
), row=row, col=col)
153-
154-
# Update layout with text labels and styling
155-
fig.update_layout(
156-
height=600,
157-
width=900,
158-
title_text=title,
159-
showlegend=False,
160-
template="plotly_white",
161-
)
162-
163-
# Update axes with text labels
164-
for i in range(n_components):
165-
row = i // ncols + 1
166-
col = i % ncols + 1
167-
fig.update_xaxes(title_text=labels[0], row=row, col=col, showgrid=True)
168-
fig.update_yaxes(title_text=labels[1], row=row, col=col, showgrid=True)
169-
170-
# Adjust layout for tight spacing
171-
fig.update_layout(margin=dict(l=20, r=20, t=50, b=20), title_x=0.5)
172-
173-
# Show the plot
174-
fig.show()
175-
176-
def compute_additional_stress_measures(data):
124+
Returns:
125+
- None
126+
"""
127+
with h5py.File(file_path, 'a') as h5file:
128+
for microstructure, loads in processed_data.items():
129+
for load_case, data in loads.items():
130+
time_steps = data.pop('time_steps', [])
131+
for measure_name, data_array in data.items():
132+
for time_step_idx, time_step in enumerate(time_steps):
133+
time_step_group = f"{microstructure}/{load_case}/time_step{time_step}"
134+
dataset_path = f"{time_step_group}/{measure_name}"
135+
136+
# Ensure the group exists
137+
if time_step_group not in h5file:
138+
raise ValueError(f"Group '{time_step_group}' does not exist in the HDF5 file.")
139+
140+
# Check if the dataset already exists
141+
if dataset_path in h5file:
142+
if overwrite:
143+
del h5file[dataset_path]
144+
h5file.create_dataset(dataset_path, data=data_array[time_step_idx])
145+
else:
146+
print(f"Dataset exists and overwrite=False: {dataset_path}")
147+
else:
148+
h5file.create_dataset(dataset_path, data=data_array[time_step_idx])
149+
150+
151+
def postprocess_and_write_to_h5(file_path, hierarchy, quantities_to_process, measures,
152+
microstructures_to_load=None, load_cases_to_load=None):
177153
"""
178-
Computes von Mises stress, hydrostatic stress, and deviatoric stress directly from the stress tensor in Mandel notation.
179-
Adds these measures to the `data` dictionary for all microstructures and load cases using vectorized operations.
154+
A higher-level function that extracts specific data from an HDF5 file, processes it to compute various tensor measures,
155+
and writes the results back into the HDF5 file using the naming convention 'quantity_measure'.
180156
181157
Parameters:
182-
- data: dictionary, contains strain and stress matrices as well as other simulation data
158+
- file_path: str
159+
Path to the HDF5 file containing the data.
160+
- hierarchy: dict
161+
The structure of the HDF5 file as identified by the `identify_hierarchy` function.
162+
- quantities_to_process: list of str
163+
List of quantities (e.g., 'stress_average', 'strain_average') to extract from the HDF5 file and process.
164+
- measures: list of str
165+
List of tensor measures to compute for the extracted quantities.
166+
Available options can be found in the `compute_rank2tensor_measures` function in the `postprocessing` module.
167+
- microstructures_to_load: list of str, optional
168+
List of microstructures to process. If None, all microstructures found in the hierarchy will be processed.
169+
- load_cases_to_load: list of str, optional
170+
List of load cases to process. If None, all load cases found in the hierarchy will be processed.
183171
184172
Returns:
185-
- updated_data: dictionary, the original data dictionary with added von Mises, hydrostatic, and deviatoric stress
173+
- processed_data: dict
174+
A dictionary containing the processed data, organized by microstructure and load case.
175+
The computed measures are stored under keys following the 'quantity_measure' naming convention.
176+
Additionally, the processed data is also written back into the HDF5 file at the corresponding locations.
186177
"""
178+
# Extract the data (loads all time steps if time_steps_to_load is None or empty)
179+
extracted_data = extract_and_organize_data(file_path, hierarchy, quantities_to_process,
180+
microstructures_to_load, load_cases_to_load)
181+
182+
# Process the data and prepare for writing
183+
processed_data = defaultdict(lambda: defaultdict(dict))
184+
for microstructure, loads in extracted_data.items():
185+
for load_case, quantities in loads.items():
186+
time_steps = quantities.pop('time_steps', [])
187+
for quantity_name, tensor_data in quantities.items():
188+
measures_results = compute_rank2tensor_measures(tensor_data, measures)
189+
for measure_name, measure_data in measures_results.items():
190+
key = f"{quantity_name}_{measure_name}"
191+
processed_data[microstructure][load_case][key] = measure_data
192+
processed_data[microstructure][load_case]['time_steps'] = time_steps
193+
194+
# Write the processed data back to the HDF5 file
195+
write_processed_data_to_h5(file_path, processed_data)
187196

188-
for microstructure, loads in data.items():
189-
for load_case, measures in loads.items():
190-
if 'stress_average' in measures:
191-
# Extract the stress matrix in Mandel notation
192-
stress_matrix = measures['stress_average']
193-
194-
# Compute the hydrostatic stress (mean of diagonal components)
195-
hydrostatic_stress = np.mean(stress_matrix[:, :3], axis=1)
196-
197-
# Deviatoric stress components (s11, s22, s33)
198-
deviatoric_stress = stress_matrix[:, :3] - hydrostatic_stress[:, np.newaxis]
199-
200-
# Shear components (s12, s23, s13) from Mandel notation
201-
deviatoric_shear = stress_matrix[:, 3:6]
202-
203-
# Compute von Mises stress using vectorized operations
204-
von_mises_stress = np.sqrt(
205-
0.5 * (
206-
(deviatoric_stress[:, 0] - deviatoric_stress[:, 1])**2 +
207-
(deviatoric_stress[:, 1] - deviatoric_stress[:, 2])**2 +
208-
(deviatoric_stress[:, 2] - deviatoric_stress[:, 0])**2 +
209-
6 * (deviatoric_shear[:, 0]**2 + deviatoric_shear[:, 1]**2 + deviatoric_shear[:, 2]**2)
210-
)
211-
)
212-
213-
# Reconstruct the deviatoric stress matrix in Mandel notation
214-
deviatoric_stress_matrix = np.zeros_like(stress_matrix)
215-
deviatoric_stress_matrix[:, :3] = deviatoric_stress # Deviatoric normal stresses
216-
deviatoric_stress_matrix[:, 3:6] = deviatoric_shear # Deviatoric shear stress components
217-
218-
# Store the computed stresses in the data dictionary
219-
measures['von_mises_stress'] = von_mises_stress
220-
measures['hydrostatic_stress'] = hydrostatic_stress
221-
measures['deviatoric_stress'] = deviatoric_stress_matrix
222-
223-
return data
197+
return processed_data

FANS_Dashboard/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# FANS Dashboard
2+
This folder contains a Jupyter Notebook designed to post-process, interpret, and visualize results generated by FANS. The notebook provides tools for exploring the hierarchical structure of HDF5 files, extracting and summarizing simulation data, and preparing the results for visualization in ParaView.
3+
4+
For further details follow along `FANS_Dashboard.ipynb`

test/h52xdmf.py renamed to FANS_Dashboard/h52xdmf.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,16 @@ def dataset_to_xdmf(dset, grid, time=None):
6666
else:
6767
attr_name = dset.name
6868

69+
### Careful: This is a hack to fix the attribute center for displacement datasets
70+
attr_center = "Node" if "displacement" in dset.name else "Cell"
71+
### Be very careful with this hack. It is not a general solution.
72+
6973
attr = ET.SubElement(
7074
grid,
7175
"Attribute",
7276
Name=attr_name,
7377
AttributeType=element,
74-
Center="Cell",
78+
Center=attr_center,
7579
)
7680

7781
data_item = ET.SubElement(
@@ -199,18 +203,10 @@ def crawl_h5_group(group, grid_dict, time=None):
199203
default=[1.0, 1.0, 1.0],
200204
metavar=('Lx', 'Ly', 'Lz'),
201205
help=(
202-
"Cube length in x, y, z dimensions.\n"
206+
"Microstructure length in x, y, z dimensions.\n"
203207
"Provide three floats. Default is [1.0, 1.0, 1.0]."
204208
)
205209
)
206-
parser.add_argument(
207-
'-t', '--time-series',
208-
action='store_true',
209-
help=(
210-
"Treat datasets as a time series based on time_step groups.\n"
211-
"If this flag is set, the script will search for datasets within groups named '<keyword>{value}' and creates a temporal collection in the XDMF file."
212-
)
213-
)
214210
parser.add_argument(
215211
'-k', '--time-keyword',
216212
type=str,
@@ -220,7 +216,15 @@ def crawl_h5_group(group, grid_dict, time=None):
220216
"This keyword should be followed by a value to denote the time in the group names."
221217
)
222218
)
223-
219+
parser.add_argument(
220+
'-t', '--time-series',
221+
action='store_true',
222+
help=(
223+
"Treat datasets as a time series based on '<time-keyword>{value}' groups.\n"
224+
"If this flag is set, the script will search for datasets within groups named '<time-keyword>{value}' and creates a temporal collection in the XDMF file."
225+
)
226+
)
227+
224228
args = parser.parse_args()
225229

226230
for h5_filepath in args.h5_filepath:

FANS_Dashboard/plotting.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import numpy as np
2+
import plotly.graph_objects as go
3+
from plotly.subplots import make_subplots
4+
5+
def plot_subplots(data1, data2, labels_x=None, labels_y=None, subplot_titles=None, title="Subplot Grid", nrows=None, ncols=None,
6+
linewidth=1, markersize=4, linecolor="blue", markercolor="red", fontsize=12):
7+
"""
8+
Plot a grid of subplots using Plotly, handling both single-component (scalar vs scalar) and multi-component data.
9+
10+
Parameters:
11+
- data1: numpy array, first set of data to plot (e.g., strain, time) with shape (n_datapoints, n_plots)
12+
- data2: numpy array, second set of data to plot (e.g., stress) with shape (n_datapoints, n_plots)
13+
- labels_x: list of strings, labels for the x axes of each subplot (optional, default=None)
14+
- labels_y: list of strings, labels for the y axes of each subplot (optional, default=None)
15+
- subplot_titles: list of strings, titles for each subplot (optional, default=None)
16+
- title: string, title of the overall plot
17+
- nrows: int, number of rows in the subplot grid (optional)
18+
- ncols: int, number of columns in the subplot grid (optional)
19+
- linewidth: int, line width for the plots (optional, default=1)
20+
- markersize: int, size of the markers (optional, default=4)
21+
- linecolor: string, color of the lines (optional, default="blue")
22+
- markercolor: string, color of the markers (optional, default="red")
23+
- fontsize: int, font size for axis labels, subplot titles, and tick labels (optional, default=12)
24+
"""
25+
# Validate data shapes
26+
if not isinstance(data1, np.ndarray) or not isinstance(data2, np.ndarray):
27+
raise ValueError("data1 and data2 must be numpy arrays.")
28+
29+
if data1.shape[0] != data2.shape[0]:
30+
raise ValueError("data1 and data2 must have the same number of data points (rows).")
31+
32+
if data1.shape[1] != data2.shape[1]:
33+
raise ValueError("data1 and data2 must have the same number of components (columns).")
34+
35+
# Set the number of components based on data shape
36+
n_components = data1.shape[1]
37+
38+
# If nrows or ncols is not specified, determine an optimal grid layout
39+
if nrows is None or ncols is None:
40+
nrows = int(np.ceil(np.sqrt(n_components)))
41+
ncols = int(np.ceil(n_components / nrows))
42+
43+
# Handle subplot titles
44+
if subplot_titles is None:
45+
subplot_titles = [f'Component {i+1}' for i in range(n_components)]
46+
elif len(subplot_titles) != n_components:
47+
raise ValueError(f"The length of subplot_titles must match the number of components ({n_components}).")
48+
49+
# Handle labels_x and labels_y
50+
if labels_x is None:
51+
labels_x = [""] * n_components
52+
elif len(labels_x) != n_components:
53+
raise ValueError(f"The length of labels_x must match the number of components ({n_components}).")
54+
55+
if labels_y is None:
56+
labels_y = [""] * n_components
57+
elif len(labels_y) != n_components:
58+
raise ValueError(f"The length of labels_y must match the number of components ({n_components}).")
59+
60+
# Create the subplot figure
61+
fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=subplot_titles)
62+
63+
# Add traces for each component
64+
for i in range(n_components):
65+
row = i // ncols + 1
66+
col = i % ncols + 1
67+
fig.add_trace(go.Scatter(
68+
x=data1[:, i],
69+
y=data2[:, i],
70+
mode='lines+markers',
71+
marker=dict(symbol='x', size=markersize, color=markercolor),
72+
line=dict(width=linewidth, color=linecolor),
73+
name=f'Component {i+1}'
74+
), row=row, col=col)
75+
76+
# Update axes with text labels
77+
fig.update_xaxes(title_text=labels_x[i], row=row, col=col, showgrid=True, mirror=True,
78+
ticks="inside", tickwidth=2, ticklen=6, title_font=dict(size=fontsize),
79+
tickfont=dict(size=fontsize))
80+
fig.update_yaxes(title_text=labels_y[i], row=row, col=col, showgrid=True, mirror=True,
81+
ticks="inside", tickwidth=2, ticklen=6, title_font=dict(size=fontsize),
82+
tickfont=dict(size=fontsize))
83+
84+
# Update layout with the overall plot title and styling
85+
fig.update_layout(
86+
height=600,
87+
width=900,
88+
title_text=title,
89+
title_font=dict(size=fontsize),
90+
showlegend=False,
91+
template="plotly_white",
92+
margin=dict(l=20, r=20, t=50, b=20),
93+
title_x=0.5,
94+
autosize=False,
95+
)
96+
97+
# Add a box outline around all subplots
98+
for i in range(1, nrows * ncols + 1):
99+
fig.update_xaxes(showline=True, linewidth=2, linecolor='black', row=(i-1)//ncols + 1, col=(i-1)%ncols + 1)
100+
fig.update_yaxes(showline=True, linewidth=2, linecolor='black', row=(i-1)//ncols + 1, col=(i-1)%ncols + 1)
101+
102+
# Update subplot titles with the specified fontsize
103+
for annotation in fig['layout']['annotations']:
104+
annotation['font'] = dict(size=fontsize)
105+
106+
# Increase the DPI/quality of the plots by setting higher resolution dimensions
107+
fig.update_layout(
108+
height=1200,
109+
width=1800,
110+
autosize=False,
111+
)
112+
113+
# Show the plot
114+
fig.show()
115+
116+
117+
118+
119+
120+

0 commit comments

Comments
 (0)