Skip to content

Commit 7b1ba38

Browse files
committed
dashboard
1 parent e1401c5 commit 7b1ba38

File tree

8 files changed

+1418
-2
lines changed

8 files changed

+1418
-2
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import numpy as np
2+
import plotly.graph_objs as go
3+
import meshio
4+
5+
6+
def compute_YoungsModulus3D(C_batch):
7+
"""
8+
Compute Young's modulus for all directions in 3D for a batch of stiffness tensors.
9+
10+
Args:
11+
C_batch (ndarray): Batch of stiffness tensors in Mandel notation, shape (n, 6, 6).
12+
13+
Returns:
14+
tuple: A tuple containing:
15+
- X_batch (ndarray): X-coordinates for plotting the modulus surface, shape (n, n_theta, n_phi).
16+
- Y_batch (ndarray): Y-coordinates for plotting the modulus surface, shape (n, n_theta, n_phi).
17+
- Z_batch (ndarray): Z-coordinates for plotting the modulus surface, shape (n, n_theta, n_phi).
18+
- E_batch (ndarray): Young's modulus in all directions, shape (n, n_theta, n_phi).
19+
"""
20+
n = C_batch.shape[0]
21+
n_theta = 180
22+
n_phi = 360
23+
24+
theta = np.linspace(0, np.pi, n_theta)
25+
phi = np.linspace(0, 2 * np.pi, n_phi)
26+
theta_grid, phi_grid = np.meshgrid(theta, phi, indexing="ij")
27+
28+
d_x = np.sin(theta_grid) * np.cos(phi_grid) # Shape (n_theta, n_phi)
29+
d_y = np.sin(theta_grid) * np.sin(phi_grid)
30+
d_z = np.cos(theta_grid)
31+
32+
N = np.stack(
33+
(
34+
d_x**2,
35+
d_y**2,
36+
d_z**2,
37+
np.sqrt(2) * d_x * d_y,
38+
np.sqrt(2) * d_x * d_z,
39+
np.sqrt(2) * d_y * d_z,
40+
),
41+
axis=-1,
42+
) # Shape (n_theta, n_phi, 6)
43+
44+
N_flat = N.reshape(-1, 6) # Shape (n_points, 6)
45+
46+
# Invert stiffness tensors to get compliance tensors
47+
S_batch = np.linalg.inv(C_batch) # Shape (n, 6, 6)
48+
49+
# Compute E for each tensor in the batch
50+
NSN = np.einsum("pi,nij,pj->np", N_flat, S_batch, N_flat) # Shape (n, n_points)
51+
E_batch = 1.0 / NSN # Shape (n, n_points)
52+
53+
# Reshape E_batch back to (n, n_theta, n_phi)
54+
E_batch = E_batch.reshape(n, *d_x.shape)
55+
56+
X_batch = E_batch * d_x # Shape (n, n_theta, n_phi)
57+
Y_batch = E_batch * d_y
58+
Z_batch = E_batch * d_z
59+
60+
return X_batch, Y_batch, Z_batch, E_batch
61+
62+
63+
def plot_YoungsModulus3D(C, title="Young's Modulus Surface"):
64+
"""
65+
Plot a 3D surface of Young's modulus.
66+
67+
Args:
68+
C (ndarray): Stiffness tensor in Mandel notation. Can be a single tensor of shape (6,6) or a batch of tensors of shape (n,6,6).
69+
title (str): Title of the plot.
70+
71+
Raises:
72+
ValueError: If C is not of shape (6,6) or (1,6,6).
73+
"""
74+
if C.shape == (6, 6):
75+
C_batch = C[np.newaxis, :, :]
76+
elif C.shape == (1, 6, 6):
77+
C_batch = C
78+
else:
79+
raise ValueError(
80+
"C must be either a (6,6) tensor or a batch with one tensor of shape (1,6,6)."
81+
)
82+
83+
X_batch, Y_batch, Z_batch, E_batch = compute_YoungsModulus3D(C_batch)
84+
X, Y, Z, E = X_batch[0], Y_batch[0], Z_batch[0], E_batch[0]
85+
86+
surface = go.Surface(x=X, y=Y, z=Z, surfacecolor=E, colorscale="Viridis")
87+
layout = go.Layout(
88+
title=title,
89+
scene=dict(
90+
xaxis=dict(title="X"),
91+
yaxis=dict(title="Y"),
92+
zaxis=dict(title="Z"),
93+
aspectmode="auto",
94+
),
95+
)
96+
97+
fig = go.Figure(data=[surface], layout=layout)
98+
fig.show()
99+
100+
101+
def export_YoungsModulus3D_to_vtk(C, prefix="youngs_modulus_surface"):
102+
"""
103+
Export the computed Young's modulus surfaces to VTK files for Paraview visualization.
104+
105+
Args:
106+
C (ndarray): Stiffness tensor in Mandel notation. Can be a single tensor of shape (6,6) or a batch of tensors of shape (n,6,6).
107+
prefix (str): Prefix for the output files.
108+
109+
Returns:
110+
None
111+
"""
112+
X_batch, Y_batch, Z_batch, E_batch = compute_YoungsModulus3D(C)
113+
n, n_theta, n_phi = X_batch.shape
114+
115+
for k in range(n):
116+
points = np.vstack(
117+
(X_batch[k].ravel(), Y_batch[k].ravel(), Z_batch[k].ravel())
118+
).T
119+
cells = [
120+
(
121+
"quad",
122+
np.array(
123+
[
124+
[
125+
i * n_phi + j,
126+
(i + 1) * n_phi + j,
127+
(i + 1) * n_phi + (j + 1),
128+
i * n_phi + (j + 1),
129+
]
130+
for i in range(n_theta - 1)
131+
for j in range(n_phi - 1)
132+
],
133+
dtype=np.int32,
134+
),
135+
)
136+
]
137+
mesh = meshio.Mesh(
138+
points=points,
139+
cells=cells,
140+
point_data={"Youngs_Modulus": E_batch[k].ravel()},
141+
)
142+
filename = f"{prefix}_{k}.vtk"
143+
meshio.write(filename, mesh)
144+
print(f"Exported {filename}")

MSUtils/fans_dashboard/__init__.py

Whitespace-only changes.

MSUtils/fans_dashboard/plotting.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import numpy as np
2+
import plotly.graph_objects as go
3+
from plotly.subplots import make_subplots
4+
5+
6+
def plot_subplots(
7+
data1,
8+
data2,
9+
labels_x=None,
10+
labels_y=None,
11+
subplot_titles=None,
12+
title="",
13+
nrows=None,
14+
ncols=None,
15+
linewidth=1,
16+
markersize=4,
17+
linecolor=None,
18+
markercolor=None,
19+
fontsize=12,
20+
fig=None,
21+
):
22+
"""
23+
Plot a grid of subplots using Plotly, handling both single-component (scalar vs scalar) and multi-component data.
24+
25+
Parameters:
26+
- data1: numpy array, first set of data to plot (e.g., strain, time) with shape (n_datapoints, n_plots)
27+
- data2: numpy array, second set of data to plot (e.g., stress) with shape (n_datapoints, n_plots)
28+
- labels_x: list of strings, labels for the x axes of each subplot (optional, default=None)
29+
- labels_y: list of strings, labels for the y axes of each subplot (optional, default=None)
30+
- subplot_titles: list of strings, titles for each subplot (optional, default=None)
31+
- title: string, title of the overall plot
32+
- nrows: int, number of rows in the subplot grid (optional)
33+
- ncols: int, number of columns in the subplot grid (optional)
34+
- linewidth: int, line width for the plots (optional, default=1)
35+
- markersize: int, size of the markers (optional, default=4)
36+
- linecolor: list of strings, colors of the lines for each subplot (optional, default=None, all blue)
37+
- markercolor: list of strings, colors of the markers for each subplot (optional, default=None, all blue)
38+
- fontsize: int, font size for axis labels, subplot titles, and tick labels (optional, default=12)
39+
- fig: existing Plotly figure to overlay the new subplots (optional, default=None, creates a new figure)
40+
"""
41+
# Validate data shapes
42+
if not isinstance(data1, np.ndarray) or not isinstance(data2, np.ndarray):
43+
raise ValueError("data1 and data2 must be numpy arrays.")
44+
45+
if data1.shape[0] != data2.shape[0]:
46+
raise ValueError(
47+
"data1 and data2 must have the same number of data points (rows)."
48+
)
49+
50+
if data1.shape[1] != data2.shape[1]:
51+
raise ValueError(
52+
"data1 and data2 must have the same number of components (columns)."
53+
)
54+
55+
# Set the number of components based on data shape
56+
n_components = data1.shape[1]
57+
58+
# Initialize linecolor and markercolor lists if not provided
59+
if linecolor is None:
60+
linecolor = ["blue"] * n_components
61+
elif len(linecolor) != n_components:
62+
raise ValueError(
63+
f"The length of linecolor must match the number of components ({n_components})."
64+
)
65+
66+
if markercolor is None:
67+
markercolor = ["blue"] * n_components
68+
elif len(markercolor) != n_components:
69+
raise ValueError(
70+
f"The length of markercolor must match the number of components ({n_components})."
71+
)
72+
73+
# If nrows or ncols is not specified, determine an optimal grid layout
74+
if nrows is None or ncols is None:
75+
nrows = int(np.ceil(np.sqrt(n_components)))
76+
ncols = int(np.ceil(n_components / nrows))
77+
78+
# Handle subplot titles
79+
if subplot_titles is None:
80+
subplot_titles = [f"Component {i+1}" for i in range(n_components)]
81+
elif len(subplot_titles) != n_components:
82+
raise ValueError(
83+
f"The length of subplot_titles must match the number of components ({n_components})."
84+
)
85+
86+
# Handle labels_x and labels_y
87+
if labels_x is None:
88+
labels_x = [""] * n_components
89+
elif len(labels_x) != n_components:
90+
raise ValueError(
91+
f"The length of labels_x must match the number of components ({n_components})."
92+
)
93+
94+
if labels_y is None:
95+
labels_y = [""] * n_components
96+
elif len(labels_y) != n_components:
97+
raise ValueError(
98+
f"The length of labels_y must match the number of components ({n_components})."
99+
)
100+
101+
# Create the subplot figure if not provided
102+
if fig is None:
103+
fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=subplot_titles)
104+
105+
# Add traces for each component
106+
for i in range(n_components):
107+
row = i // ncols + 1
108+
col = i % ncols + 1
109+
fig.add_trace(
110+
go.Scatter(
111+
x=data1[:, i],
112+
y=data2[:, i],
113+
mode="lines+markers",
114+
marker=dict(symbol="x", size=markersize, color=markercolor[i]),
115+
line=dict(width=linewidth, color=linecolor[i]),
116+
name=f"Component {i+1}",
117+
),
118+
row=row,
119+
col=col,
120+
)
121+
122+
# Update axes with text labels
123+
fig.update_xaxes(
124+
title_text=labels_x[i],
125+
row=row,
126+
col=col,
127+
showgrid=True,
128+
mirror=True,
129+
ticks="inside",
130+
tickwidth=2,
131+
ticklen=6,
132+
title_font=dict(size=fontsize),
133+
tickfont=dict(size=fontsize),
134+
automargin=True,
135+
)
136+
fig.update_yaxes(
137+
title_text=labels_y[i],
138+
row=row,
139+
col=col,
140+
showgrid=True,
141+
mirror=True,
142+
ticks="inside",
143+
tickwidth=2,
144+
ticklen=6,
145+
title_font=dict(size=fontsize),
146+
tickfont=dict(size=fontsize),
147+
automargin=True,
148+
)
149+
150+
# Update layout with the overall plot title and styling
151+
fig.update_layout(
152+
height=500,
153+
width=800,
154+
title_text=title,
155+
title_font=dict(size=fontsize),
156+
showlegend=False, # Legends removed
157+
template="plotly_white",
158+
margin=dict(l=50, r=50, t=50, b=50), # Adjust margins to prevent overlap
159+
title_x=0.5,
160+
autosize=False,
161+
)
162+
163+
# Add a box outline around all subplots
164+
for i in range(1, nrows * ncols + 1):
165+
fig.update_xaxes(
166+
showline=True,
167+
linewidth=2,
168+
linecolor="black",
169+
row=(i - 1) // ncols + 1,
170+
col=(i - 1) % ncols + 1,
171+
)
172+
fig.update_yaxes(
173+
showline=True,
174+
linewidth=2,
175+
linecolor="black",
176+
row=(i - 1) // ncols + 1,
177+
col=(i - 1) % ncols + 1,
178+
)
179+
180+
# Update subplot titles with the specified fontsize
181+
for annotation in fig["layout"]["annotations"]:
182+
annotation["font"] = dict(size=fontsize)
183+
184+
# Return the figure for further customization or overlaying
185+
return fig

0 commit comments

Comments
 (0)