Disk Harmonic Analysis#
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import seaborn as sns
from sklearn.decomposition import PCA
from ktch.datasets import load_surface_leaf_bending
from ktch.harmonic import DiskHarmonicAnalysis, xy2polar
Load the surface leaf bending dataset#
This dataset contains 60 synthetic 3D leaf surfaces with bending deformation, each paired with a disk parameterization mesh (unit disk, z=0).
data = load_surface_leaf_bending()
print(f"Number of specimens: {len(data.vertices)}")
print(f"Specimen 1 — vertices: {data.vertices[0].shape}, faces: {data.faces[0].shape}")
print(f"Specimen 1 — param vertices: {data.param_vertices[0].shape}")
Downloading data from 'https://pub-c1d6dba6c94843f88f0fd096d19c0831.r2.dev/datasets/surface_leaf_bending/v1/surface_leaf_bending.zip' to file '/home/runner/.cache/ktch-data/surface_leaf_bending/v1/surface_leaf_bending.zip'.
Number of specimens: 60
Specimen 1 — vertices: (8099, 3), faces: (15615, 3)
Specimen 1 — param vertices: (8099, 3)
df_meta = pd.DataFrame(data.meta)
df_meta["bending_angle"] = (
np.degrees(df_meta["alphaB"]).round().astype(int).astype(str) + "\u00b0"
)
df_meta["aspect_ratio"] = df_meta["alpha"]
df_meta
| lmax | c | alpha | A | alphaB | kB | alpha_random | alphaB_random | bending_angle | aspect_ratio | |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.060572 | 2.528513 | 140° | 0.08 |
| 2 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.090911 | 2.318855 | 140° | 0.08 |
| 3 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.088957 | 2.417189 | 140° | 0.08 |
| 4 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.074596 | 2.506030 | 140° | 0.08 |
| 5 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.098990 | 2.294188 | 140° | 0.08 |
| 6 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.073196 | 2.492090 | 140° | 0.08 |
| 7 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.077120 | 2.287193 | 140° | 0.08 |
| 8 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.083971 | 2.280600 | 140° | 0.08 |
| 9 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.073029 | 2.431389 | 140° | 0.08 |
| 10 | 0.4 | 0.65 | 0.08 | 1.0 | 2.443461 | 0.2 | 0.099243 | 2.553002 | 140° | 0.08 |
| 11 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.089302 | 1.409695 | 80° | 0.08 |
| 12 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.076330 | 1.242604 | 80° | 0.08 |
| 13 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.060501 | 1.299788 | 80° | 0.08 |
| 14 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.087227 | 1.536335 | 80° | 0.08 |
| 15 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.098797 | 1.548685 | 80° | 0.08 |
| 16 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.076723 | 1.280753 | 80° | 0.08 |
| 17 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.093675 | 1.558039 | 80° | 0.08 |
| 18 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.095911 | 1.566630 | 80° | 0.08 |
| 19 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.078236 | 1.387295 | 80° | 0.08 |
| 20 | 0.4 | 0.65 | 0.08 | 1.0 | 1.396263 | 0.2 | 0.095689 | 1.447270 | 80° | 0.08 |
| 21 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.082873 | 0.390205 | 20° | 0.08 |
| 22 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.064031 | 0.266366 | 20° | 0.08 |
| 23 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.089630 | 0.373168 | 20° | 0.08 |
| 24 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.078860 | 0.330521 | 20° | 0.08 |
| 25 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.087673 | 0.318159 | 20° | 0.08 |
| 26 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.088925 | 0.305914 | 20° | 0.08 |
| 27 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.077160 | 0.372707 | 20° | 0.08 |
| 28 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.083355 | 0.268017 | 20° | 0.08 |
| 29 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.083693 | 0.406805 | 20° | 0.08 |
| 30 | 0.4 | 0.65 | 0.08 | 1.0 | 0.349066 | 0.2 | 0.081946 | 0.311726 | 20° | 0.08 |
| 31 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.173997 | 2.326146 | 140° | 0.16 |
| 32 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.178168 | 2.367912 | 140° | 0.16 |
| 33 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.166143 | 2.497124 | 140° | 0.16 |
| 34 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.142324 | 2.521001 | 140° | 0.16 |
| 35 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.159544 | 2.492420 | 140° | 0.16 |
| 36 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.176739 | 2.296786 | 140° | 0.16 |
| 37 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.170455 | 2.304695 | 140° | 0.16 |
| 38 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.148710 | 2.478740 | 140° | 0.16 |
| 39 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.141903 | 2.555287 | 140° | 0.16 |
| 40 | 0.4 | 0.65 | 0.16 | 1.0 | 2.443461 | 0.2 | 0.171313 | 2.610918 | 140° | 0.16 |
| 41 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.176521 | 1.244129 | 80° | 0.16 |
| 42 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.167533 | 1.252522 | 80° | 0.16 |
| 43 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.175097 | 1.438110 | 80° | 0.16 |
| 44 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.146855 | 1.570428 | 80° | 0.16 |
| 45 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.143005 | 1.422087 | 80° | 0.16 |
| 46 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.174511 | 1.497144 | 80° | 0.16 |
| 47 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.170496 | 1.470243 | 80° | 0.16 |
| 48 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.151212 | 1.282264 | 80° | 0.16 |
| 49 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.176779 | 1.257857 | 80° | 0.16 |
| 50 | 0.4 | 0.65 | 0.16 | 1.0 | 1.396263 | 0.2 | 0.157964 | 1.505475 | 80° | 0.16 |
| 51 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.166466 | 0.346456 | 20° | 0.16 |
| 52 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.177635 | 0.386719 | 20° | 0.16 |
| 53 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.155776 | 0.441191 | 20° | 0.16 |
| 54 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.175787 | 0.407493 | 20° | 0.16 |
| 55 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.146865 | 0.504861 | 20° | 0.16 |
| 56 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.163440 | 0.445301 | 20° | 0.16 |
| 57 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.160243 | 0.366476 | 20° | 0.16 |
| 58 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.158335 | 0.354346 | 20° | 0.16 |
| 59 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.148304 | 0.235357 | 20° | 0.16 |
| 60 | 0.4 | 0.65 | 0.16 | 1.0 | 0.349066 | 0.2 | 0.160054 | 0.206858 | 20° | 0.16 |
Visualize 3D surfaces#
representative_ids = [0, 10, 20, 30, 40, 50]
bending_order = [str(deg) + "\u00b0" for deg in [20, 80, 140]]
for sid in representative_ids:
label = (
f"Specimen {sid + 1} "
f"(bending={df_meta.iloc[sid]['bending_angle']}, "
f"\u03b1={df_meta.iloc[sid]['aspect_ratio']})"
)
fig = plot_mesh3d(
data.vertices[sid], data.faces[sid], title=label
)
fig.show()
Parameter mesh (disk parameterization)#
The parameter mesh maps each surface vertex to a point on the unit disk.
fig = plot_mesh3d(
data.param_vertices[0], data.param_faces[0],
title="Parameter mesh (specimen 1)", opacity=0.3,
)
fig.show()
Prepare disk parameterization#
DHA requires polar coordinates (r, theta) on the unit disk. We extract the
(x, y) coordinates from the parameter mesh and convert them using xy2polar.
r_theta = [
xy2polar(data.param_vertices[i][:, :2], centered=True)
for i in range(len(data.vertices))
]
print(f"r_theta[0].shape: {r_theta[0].shape}")
print(f"r range: [{r_theta[0][:, 0].min():.4f}, {r_theta[0][:, 0].max():.4f}]")
print(f"theta range: [{r_theta[0][:, 1].min():.4f}, {r_theta[0][:, 1].max():.4f}]")
r_theta[0].shape: (8099, 2)
r range: [0.0066, 1.0000]
theta range: [0.0006, 6.2821]
X = data.vertices
Disk Harmonic Analysis#
dha = DiskHarmonicAnalysis(n_harmonics=15, n_dim=3)
coef = dha.fit_transform(X, r_theta=r_theta)
coef.shape
(60, 768)
Reconstruction from coefficients#
Reconstructing with different numbers of harmonics shows how DHA captures
shape at different levels of detail. The inverse_transform method
returns a regular (n_theta, n_r, 3) grid for each specimen.
sid = 0
I, J, K = data.faces[sid].T
x_orig, y_orig, z_orig = data.vertices[sid].T
n_max_values = [5, 10, 15]
for n_max in n_max_values:
surf = dha.inverse_transform(coef[sid:sid+1], n_max=n_max)
x_r, y_r, z_r = surf[0].transpose(2, 0, 1)
fig = go.Figure(
data=[
go.Mesh3d(
x=x_orig, y=y_orig, z=z_orig,
i=I, j=J, k=K,
opacity=0.3,
color="lightblue",
showscale=False,
name="original",
),
go.Surface(
x=x_r, y=y_r, z=z_r,
opacity=0.5,
showscale=False,
name=f"n_max={n_max}",
),
]
)
fig.update_layout(
title=f"Reconstruction with n_max={n_max}",
width=700, height=500,
scene=dict(aspectmode="data"),
)
fig.show()