gratinglab 0.1.0__py3-none-any.whl
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.
- gratinglab/__init__.py +35 -0
- gratinglab/checks.py +263 -0
- gratinglab/compare.py +233 -0
- gratinglab/convergence.py +455 -0
- gratinglab/geometry.py +483 -0
- gratinglab/gui/__init__.py +0 -0
- gratinglab/gui/app.py +80 -0
- gratinglab/gui/diagram.py +699 -0
- gratinglab/gui/diagram3d.py +609 -0
- gratinglab/gui/docs.py +189 -0
- gratinglab/gui/icon.py +101 -0
- gratinglab/gui/integral_options.py +219 -0
- gratinglab/gui/mathtext.py +209 -0
- gratinglab/gui/orders.py +152 -0
- gratinglab/gui/provenance.py +273 -0
- gratinglab/gui/qt/__init__.py +20 -0
- gratinglab/gui/qt/geometry_panel.py +239 -0
- gratinglab/gui/qt/geometry_tab.py +387 -0
- gratinglab/gui/qt/integral_tab.py +447 -0
- gratinglab/gui/qt/main_window.py +514 -0
- gratinglab/gui/qt/profile_plot_panel.py +67 -0
- gratinglab/gui/qt/scalar_tab.py +498 -0
- gratinglab/gui/qt/scene3d_panel.py +347 -0
- gratinglab/gui/qt/setup_tab.py +215 -0
- gratinglab/gui/qt/sizing.py +25 -0
- gratinglab/gui/qt/theory_viewer.py +89 -0
- gratinglab/gui/qt/worker.py +203 -0
- gratinglab/gui/richtext.py +463 -0
- gratinglab/gui/scalar_options.py +139 -0
- gratinglab/gui/state.py +379 -0
- gratinglab/illumination.py +212 -0
- gratinglab/io/__init__.py +0 -0
- gratinglab/io/efficiency_table.py +382 -0
- gratinglab/io/ggp.py +248 -0
- gratinglab/materials/__init__.py +218 -0
- gratinglab/materials/data/SOURCES.md +53 -0
- gratinglab/materials/fresnel.py +282 -0
- gratinglab/materials/optical.py +319 -0
- gratinglab/materials/refractiveindex.py +306 -0
- gratinglab/metrology/__init__.py +121 -0
- gratinglab/metrology/analyzer.py +875 -0
- gratinglab/metrology/boundary/__init__.py +27 -0
- gratinglab/metrology/boundary/average.py +143 -0
- gratinglab/metrology/boundary/pipeline.py +211 -0
- gratinglab/metrology/cli.py +52 -0
- gratinglab/metrology/config.py +170 -0
- gratinglab/metrology/core/__init__.py +23 -0
- gratinglab/metrology/core/analysis.py +315 -0
- gratinglab/metrology/core/image_flatten.py +131 -0
- gratinglab/metrology/core/processing.py +344 -0
- gratinglab/metrology/core/tip.py +233 -0
- gratinglab/metrology/data/__init__.py +16 -0
- gratinglab/metrology/data/aggregation.py +193 -0
- gratinglab/metrology/gui/__init__.py +56 -0
- gratinglab/metrology/gui/icon.py +136 -0
- gratinglab/metrology/gui/qt/__init__.py +19 -0
- gratinglab/metrology/gui/qt/app.py +28 -0
- gratinglab/metrology/gui/qt/boundary_view.py +324 -0
- gratinglab/metrology/gui/qt/canvas.py +151 -0
- gratinglab/metrology/gui/qt/efficiency_dialog.py +273 -0
- gratinglab/metrology/gui/qt/import_view.py +380 -0
- gratinglab/metrology/gui/qt/main_window.py +398 -0
- gratinglab/metrology/gui/qt/wiki_view.py +99 -0
- gratinglab/metrology/gui/qt/worker.py +59 -0
- gratinglab/metrology/gui/state.py +168 -0
- gratinglab/metrology/io/__init__.py +8 -0
- gratinglab/metrology/io/file_io.py +253 -0
- gratinglab/metrology/io/metrics.py +38 -0
- gratinglab/metrology/io/spm.py +210 -0
- gratinglab/metrology/settings.py +216 -0
- gratinglab/metrology/stats/__init__.py +16 -0
- gratinglab/metrology/stats/analysis.py +329 -0
- gratinglab/metrology/stats/icc.py +128 -0
- gratinglab/metrology/visualization/__init__.py +16 -0
- gratinglab/metrology/visualization/diagnostics.py +111 -0
- gratinglab/metrology/visualization/profiles.py +234 -0
- gratinglab/metrology/visualization/statistics.py +412 -0
- gratinglab/metrology/wiki/__init__.py +91 -0
- gratinglab/metrology/wiki/facet-fitting.md +95 -0
- gratinglab/metrology/wiki/file-formats.md +116 -0
- gratinglab/metrology/wiki/flattening.md +105 -0
- gratinglab/metrology/wiki/icc-correction.md +111 -0
- gratinglab/metrology/wiki/overview.md +51 -0
- gratinglab/metrology/wiki/reading-outputs.md +154 -0
- gratinglab/metrology/wiki/row-groups.md +75 -0
- gratinglab/metrology/wiki/tip-correction.md +101 -0
- gratinglab/metrology/workflows.py +383 -0
- gratinglab/problem.py +79 -0
- gratinglab/profiles.py +531 -0
- gratinglab/resolution.py +198 -0
- gratinglab/result.py +268 -0
- gratinglab/solvers/__init__.py +34 -0
- gratinglab/solvers/base.py +207 -0
- gratinglab/solvers/integral/__init__.py +406 -0
- gratinglab/solvers/integral/_boundary.py +50 -0
- gratinglab/solvers/integral/_core.py +126 -0
- gratinglab/solvers/integral/_finite.py +396 -0
- gratinglab/solvers/integral/_greens.py +234 -0
- gratinglab/solvers/integral/_kernels.py +308 -0
- gratinglab/solvers/integral/_nystrom.py +474 -0
- gratinglab/solvers/scalar.py +1144 -0
- gratinglab-0.1.0.dist-info/METADATA +319 -0
- gratinglab-0.1.0.dist-info/RECORD +107 -0
- gratinglab-0.1.0.dist-info/WHEEL +5 -0
- gratinglab-0.1.0.dist-info/entry_points.txt +4 -0
- gratinglab-0.1.0.dist-info/licenses/LICENSE +28 -0
- gratinglab-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Intraclass correlation for row-group measurements
|
|
3
|
+
|
|
4
|
+
Row-group analysis produces many blaze-angle measurements per image, but they
|
|
5
|
+
re-measure the same physical grooves from different horizontal bands of one scan.
|
|
6
|
+
The rest of the statistics in this package divide by sqrt(N) over that full count,
|
|
7
|
+
which is only valid if the measurements are independent.
|
|
8
|
+
|
|
9
|
+
The intraclass correlation coefficient (ICC) is how that assumption gets tested:
|
|
10
|
+
the fraction of total variance that sits *between* row groups rather than within
|
|
11
|
+
them.
|
|
12
|
+
|
|
13
|
+
ICC = 0 groups are indistinguishable; measurements behave independently
|
|
14
|
+
and dividing by sqrt(N) is roughly right
|
|
15
|
+
ICC = 1 all variance is between groups; the measurements within a group
|
|
16
|
+
carry no new information, and the effective sample size is closer
|
|
17
|
+
to the number of groups than the number of measurements
|
|
18
|
+
|
|
19
|
+
This module only measures. The correction that this measurement sizes is applied
|
|
20
|
+
in analyzer.py, which uses effective_sample_size() below to compute a
|
|
21
|
+
row-group-corrected SEM.
|
|
22
|
+
"""
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
# Interpretation thresholds, carried over from an earlier prototype
|
|
26
|
+
# (correlation_source_analysis.py, see git history prior to 2026-08-09).
|
|
27
|
+
ICC_NEGLIGIBLE = 0.1
|
|
28
|
+
ICC_MODERATE = 0.2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def interpret_icc(icc):
|
|
32
|
+
"""One-line reading of an ICC value"""
|
|
33
|
+
if icc < ICC_NEGLIGIBLE:
|
|
34
|
+
return "negligible - simple statistics defensible"
|
|
35
|
+
if icc < ICC_MODERATE:
|
|
36
|
+
return "moderate - hierarchical statistics recommended"
|
|
37
|
+
return "substantial - hierarchical statistics needed"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compute_icc(values, group_labels):
|
|
41
|
+
"""
|
|
42
|
+
One-way ICC of `values` grouped by `group_labels`.
|
|
43
|
+
|
|
44
|
+
Uses the same variance-ratio form as the original quick_icc_check:
|
|
45
|
+
between-group variance over total variance, where within-group variance is
|
|
46
|
+
the mean of the per-group variances.
|
|
47
|
+
|
|
48
|
+
Parameters:
|
|
49
|
+
values: measurements (e.g. blaze angles)
|
|
50
|
+
group_labels: which group each measurement belongs to, same length
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
dict with icc, between_var, within_var, n_groups, n_measurements,
|
|
54
|
+
mean_group_size and interpretation. Returns icc = nan when there are
|
|
55
|
+
fewer than two groups, since between-group variance is undefined - a
|
|
56
|
+
caller must not read that as "no correlation".
|
|
57
|
+
"""
|
|
58
|
+
values = np.asarray(values, dtype=float)
|
|
59
|
+
labels = np.asarray(group_labels)
|
|
60
|
+
|
|
61
|
+
if len(values) != len(labels):
|
|
62
|
+
raise ValueError(f"values and group_labels must be the same length, "
|
|
63
|
+
f"got {len(values)} and {len(labels)}")
|
|
64
|
+
|
|
65
|
+
unique = np.unique(labels)
|
|
66
|
+
n_groups = len(unique)
|
|
67
|
+
|
|
68
|
+
result = {
|
|
69
|
+
'n_measurements': len(values),
|
|
70
|
+
'n_groups': n_groups,
|
|
71
|
+
'mean_group_size': len(values) / n_groups if n_groups else np.nan,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if n_groups < 2 or len(values) < 2:
|
|
75
|
+
result.update({'icc': np.nan, 'between_var': np.nan,
|
|
76
|
+
'within_var': np.nan,
|
|
77
|
+
'interpretation': 'undefined - need at least 2 groups'})
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
group_means = np.array([values[labels == g].mean() for g in unique])
|
|
81
|
+
# Groups of size 1 contribute no within-group variance information
|
|
82
|
+
group_vars = np.array([values[labels == g].var(ddof=1)
|
|
83
|
+
if (labels == g).sum() > 1 else np.nan
|
|
84
|
+
for g in unique])
|
|
85
|
+
|
|
86
|
+
between_var = float(group_means.var(ddof=1))
|
|
87
|
+
within_var = float(np.nanmean(group_vars)) if not np.all(np.isnan(group_vars)) else 0.0
|
|
88
|
+
|
|
89
|
+
total = between_var + within_var
|
|
90
|
+
icc = float(between_var / total) if total > 0 else 0.0
|
|
91
|
+
|
|
92
|
+
result.update({
|
|
93
|
+
'icc': icc,
|
|
94
|
+
'between_var': between_var,
|
|
95
|
+
'within_var': within_var,
|
|
96
|
+
'interpretation': interpret_icc(icc),
|
|
97
|
+
})
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def effective_sample_size(n_measurements, mean_group_size, icc):
|
|
102
|
+
"""
|
|
103
|
+
Sample size the measurements are actually worth, given their correlation.
|
|
104
|
+
|
|
105
|
+
n_eff = n / (1 + (m - 1) * ICC), the standard design-effect correction, where
|
|
106
|
+
m is the mean measurements per group. At ICC = 0 this returns n; as ICC rises
|
|
107
|
+
it falls toward the number of groups.
|
|
108
|
+
|
|
109
|
+
This is the number that shows how far the current SEMs are off: they are
|
|
110
|
+
computed with n, and dividing by sqrt(n) instead of sqrt(n_eff) understates
|
|
111
|
+
the standard error by sqrt(n / n_eff).
|
|
112
|
+
"""
|
|
113
|
+
if not np.isfinite(icc):
|
|
114
|
+
return np.nan
|
|
115
|
+
design_effect = 1.0 + (mean_group_size - 1.0) * icc
|
|
116
|
+
return n_measurements / design_effect if design_effect > 0 else np.nan
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def sem_inflation_factor(mean_group_size, icc):
|
|
120
|
+
"""
|
|
121
|
+
How much the reported SEM should grow: sqrt(design effect).
|
|
122
|
+
|
|
123
|
+
A factor of 2 means the true standard error is twice what is currently
|
|
124
|
+
reported, and every confidence interval is half the width it should be.
|
|
125
|
+
"""
|
|
126
|
+
if not np.isfinite(icc):
|
|
127
|
+
return np.nan
|
|
128
|
+
return float(np.sqrt(1.0 + (mean_group_size - 1.0) * icc))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visualization and plotting functions
|
|
3
|
+
Diagnostic plots, statistics, and AFM profiles
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .diagnostics import plot_analyzed_regions_overlay, plot_flattening_diagnostic
|
|
7
|
+
from .statistics import plot_summary_statistics, plot_multi_file_comparison
|
|
8
|
+
from .profiles import plot_sample_profiles_by_temperature
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'plot_analyzed_regions_overlay',
|
|
12
|
+
'plot_flattening_diagnostic',
|
|
13
|
+
'plot_summary_statistics',
|
|
14
|
+
'plot_multi_file_comparison',
|
|
15
|
+
'plot_sample_profiles_by_temperature'
|
|
16
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diagnostic plotting functions
|
|
3
|
+
Shows analysis details like flattening and analyzed regions
|
|
4
|
+
"""
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from scipy.signal import find_peaks
|
|
7
|
+
from matplotlib.lines import Line2D
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def plot_analyzed_regions_overlay(raw_x, flat_y, groove_centers, period_nm, quality_list, facet_trim):
|
|
11
|
+
"""
|
|
12
|
+
Create an overlay plot showing which portions of each groove are actually analyzed.
|
|
13
|
+
|
|
14
|
+
Parameters:
|
|
15
|
+
raw_x: x-axis positions (µm)
|
|
16
|
+
flat_y: flattened height profile (nm)
|
|
17
|
+
groove_centers: indices of detected groove centers
|
|
18
|
+
period_nm: groove period (nm)
|
|
19
|
+
quality_list: list of quality dictionaries with region info
|
|
20
|
+
facet_trim: fraction of facet trimmed from edges
|
|
21
|
+
"""
|
|
22
|
+
fig, ax = plt.subplots(figsize=(14, 6))
|
|
23
|
+
|
|
24
|
+
ax.plot(raw_x, flat_y, 'k-', linewidth=1, alpha=0.4, label='Flattened profile', zorder=1)
|
|
25
|
+
ax.plot(raw_x[groove_centers], flat_y[groove_centers], 'ro',
|
|
26
|
+
markersize=6, label='Groove centers', zorder=3, alpha=0.7)
|
|
27
|
+
|
|
28
|
+
for i, qual in enumerate(quality_list):
|
|
29
|
+
if 'regions' in qual:
|
|
30
|
+
reg = qual['regions']
|
|
31
|
+
blaze_x_um = reg['blaze_x_trim'] / 1000
|
|
32
|
+
blaze_y = reg['blaze_y_trim']
|
|
33
|
+
ax.plot(blaze_x_um, blaze_y, 'b-', linewidth=3, alpha=0.8, zorder=2)
|
|
34
|
+
|
|
35
|
+
blaze_x_full_um = reg['blaze_x_full'] / 1000
|
|
36
|
+
blaze_y_full = reg['blaze_y_full']
|
|
37
|
+
ax.plot(blaze_x_full_um, blaze_y_full, 'b-', linewidth=1.5, alpha=0.3, zorder=1)
|
|
38
|
+
|
|
39
|
+
if len(blaze_x_um) > 0:
|
|
40
|
+
fit_line = reg['blaze_fit']
|
|
41
|
+
ax.plot(blaze_x_um, fit_line, 'cyan', linewidth=2,
|
|
42
|
+
linestyle='--', alpha=0.7, zorder=2)
|
|
43
|
+
|
|
44
|
+
custom_lines = [
|
|
45
|
+
Line2D([0], [0], color='k', linewidth=1, alpha=0.4),
|
|
46
|
+
Line2D([0], [0], color='b', linewidth=1.5, alpha=0.3),
|
|
47
|
+
Line2D([0], [0], color='b', linewidth=3, alpha=0.8),
|
|
48
|
+
Line2D([0], [0], color='cyan', linewidth=2, linestyle='--', alpha=0.7),
|
|
49
|
+
Line2D([0], [0], marker='o', color='w', markerfacecolor='r', markersize=6)
|
|
50
|
+
]
|
|
51
|
+
ax.legend(custom_lines,
|
|
52
|
+
['Full profile (flattened)', 'Full blaze facets',
|
|
53
|
+
'Analyzed regions (trimmed)', 'Linear fits', 'Groove centers'],
|
|
54
|
+
loc='best', fontsize=10)
|
|
55
|
+
|
|
56
|
+
ax.set_xlabel('Position (µm)', fontsize=12)
|
|
57
|
+
ax.set_ylabel('Height (nm)', fontsize=12)
|
|
58
|
+
ax.set_title(f'Analyzed Regions Overlay - Trim fraction: {facet_trim:.0%} (2.5x extra trough trimming)', fontsize=13)
|
|
59
|
+
ax.grid(True, alpha=0.3)
|
|
60
|
+
plt.tight_layout()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def plot_flattening_diagnostic(raw_x, raw_y, flat_y, background, flatten_method,
|
|
64
|
+
flatten_feature, period_nm_est):
|
|
65
|
+
"""
|
|
66
|
+
Show before/after flattening comparison
|
|
67
|
+
|
|
68
|
+
Parameters:
|
|
69
|
+
raw_x: x-axis positions (µm)
|
|
70
|
+
raw_y: raw height profile (nm)
|
|
71
|
+
flat_y: flattened height profile (nm)
|
|
72
|
+
background: fitted background curve (nm)
|
|
73
|
+
flatten_method: method used for flattening
|
|
74
|
+
flatten_feature: feature used (for level_grooves method)
|
|
75
|
+
period_nm_est: estimated period (nm)
|
|
76
|
+
"""
|
|
77
|
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6))
|
|
78
|
+
|
|
79
|
+
ax1.plot(raw_x, raw_y, 'b-', linewidth=1, label='Raw profile', alpha=0.7)
|
|
80
|
+
ax1.plot(raw_x, background, 'r--', linewidth=2, label=f'Background ({flatten_method})')
|
|
81
|
+
|
|
82
|
+
if flatten_method == 'level_grooves':
|
|
83
|
+
if flatten_feature in ['peaks', 'both']:
|
|
84
|
+
dx_nm = (raw_x[1] - raw_x[0]) * 1000
|
|
85
|
+
min_distance = int(0.3 * period_nm_est / dx_nm)
|
|
86
|
+
peaks, _ = find_peaks(raw_y, distance=min_distance)
|
|
87
|
+
ax1.plot(raw_x[peaks], raw_y[peaks], 'go', markersize=8,
|
|
88
|
+
label=f'Peaks used ({len(peaks)})', zorder=5)
|
|
89
|
+
if flatten_feature in ['troughs', 'both']:
|
|
90
|
+
y_inv = -raw_y
|
|
91
|
+
dx_nm = (raw_x[1] - raw_x[0]) * 1000
|
|
92
|
+
min_distance = int(0.3 * period_nm_est / dx_nm)
|
|
93
|
+
troughs, _ = find_peaks(y_inv, distance=min_distance)
|
|
94
|
+
ax1.plot(raw_x[troughs], raw_y[troughs], 'mo', markersize=8,
|
|
95
|
+
label=f'Troughs used ({len(troughs)})', zorder=5)
|
|
96
|
+
|
|
97
|
+
ax1.set_xlabel('Displacement (µm)', fontsize=11)
|
|
98
|
+
ax1.set_ylabel('Height (nm)', fontsize=11)
|
|
99
|
+
ax1.set_title('Before Flattening: Raw Profile + Background Fit', fontsize=12)
|
|
100
|
+
ax1.legend(fontsize=10)
|
|
101
|
+
ax1.grid(True, alpha=0.3)
|
|
102
|
+
|
|
103
|
+
ax2.plot(raw_x, flat_y, 'g-', linewidth=1, label='Flattened profile')
|
|
104
|
+
ax2.axhline(y=0, color='k', linestyle='--', linewidth=0.5, alpha=0.5)
|
|
105
|
+
ax2.set_xlabel('Displacement (µm)', fontsize=11)
|
|
106
|
+
ax2.set_ylabel('Height (nm)', fontsize=11)
|
|
107
|
+
ax2.set_title('After Flattening: Background Removed', fontsize=12)
|
|
108
|
+
ax2.legend(fontsize=10)
|
|
109
|
+
ax2.grid(True, alpha=0.3)
|
|
110
|
+
|
|
111
|
+
plt.tight_layout()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AFM profile plotting functions
|
|
3
|
+
Complex multi-panel layouts organized by temperature
|
|
4
|
+
"""
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plot_sample_profiles_by_temperature(grouped_results, grouped_labels, temp_order):
|
|
10
|
+
"""
|
|
11
|
+
Plot AFM profiles organized by temperature (one figure per temperature)
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
grouped_results: dict mapping temperature key to result
|
|
15
|
+
grouped_labels: dict mapping temperature key to label
|
|
16
|
+
temp_order: list of temperature keys in order
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
for temp_key in temp_order:
|
|
20
|
+
result = grouped_results[temp_key]
|
|
21
|
+
label = grouped_labels[temp_key]
|
|
22
|
+
|
|
23
|
+
# Check if this is a combined result with multiple scans
|
|
24
|
+
if 'individual_scans' in result:
|
|
25
|
+
# Multiple scans at this temperature
|
|
26
|
+
_plot_multiple_scans(result, label)
|
|
27
|
+
else:
|
|
28
|
+
# Single scan at this temperature
|
|
29
|
+
_plot_single_scan(result, label)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _plot_multiple_scans(result, label):
|
|
33
|
+
"""
|
|
34
|
+
Plot multiple AFM scans on separate subplots (for same temperature)
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
result: combined result dictionary with 'individual_scans' key
|
|
38
|
+
label: sample label
|
|
39
|
+
"""
|
|
40
|
+
scans = result['individual_scans']
|
|
41
|
+
n_scans = len(scans)
|
|
42
|
+
|
|
43
|
+
# Create figure with one subplot per scan
|
|
44
|
+
subplot_height = 3
|
|
45
|
+
total_height = min(subplot_height * n_scans, 11)
|
|
46
|
+
fig, axes = plt.subplots(n_scans, 1, figsize=(14, total_height))
|
|
47
|
+
|
|
48
|
+
if n_scans == 1:
|
|
49
|
+
axes = [axes]
|
|
50
|
+
|
|
51
|
+
# Overall figure title
|
|
52
|
+
fig.suptitle(f'{label} - Combined: {result["mean_angle"]:.2f}° ± {result["std_angle"]:.2f}° '
|
|
53
|
+
f'(N={result["n_grooves"]} grooves from {n_scans} scans)',
|
|
54
|
+
fontsize=12, fontweight='bold', y=0.995)
|
|
55
|
+
|
|
56
|
+
for i, scan in enumerate(scans):
|
|
57
|
+
ax = axes[i]
|
|
58
|
+
|
|
59
|
+
if 'raw_x' in scan and 'flat_y' in scan:
|
|
60
|
+
# Plot profile
|
|
61
|
+
ax.plot(scan['raw_x'], scan['flat_y'], 'k-', linewidth=0.8, alpha=0.5,
|
|
62
|
+
label='Flattened profile')
|
|
63
|
+
|
|
64
|
+
# Plot groove centers
|
|
65
|
+
if 'groove_centers' in scan:
|
|
66
|
+
ax.plot(scan['raw_x'][scan['groove_centers']],
|
|
67
|
+
scan['flat_y'][scan['groove_centers']],
|
|
68
|
+
'ro', markersize=4, alpha=0.7, label='Groove centers')
|
|
69
|
+
|
|
70
|
+
# Plot analyzed regions and fits
|
|
71
|
+
if 'quality' in scan:
|
|
72
|
+
for j, qual in enumerate(scan['quality']):
|
|
73
|
+
if 'regions' in qual:
|
|
74
|
+
reg = qual['regions']
|
|
75
|
+
blaze_x_um = reg['blaze_x_trim'] / 1000
|
|
76
|
+
blaze_y = reg['blaze_y_trim']
|
|
77
|
+
fit_line = reg['blaze_fit']
|
|
78
|
+
|
|
79
|
+
if j == 0:
|
|
80
|
+
ax.plot(blaze_x_um, blaze_y, 'b-', linewidth=2.5, alpha=0.8,
|
|
81
|
+
label='Analyzed regions', zorder=3)
|
|
82
|
+
ax.plot(blaze_x_um, fit_line, 'cyan', linewidth=1.5,
|
|
83
|
+
linestyle='--', alpha=0.7, label='Linear fits', zorder=4)
|
|
84
|
+
else:
|
|
85
|
+
ax.plot(blaze_x_um, blaze_y, 'b-', linewidth=2.5, alpha=0.8, zorder=3)
|
|
86
|
+
ax.plot(blaze_x_um, fit_line, 'cyan', linewidth=1.5,
|
|
87
|
+
linestyle='--', alpha=0.7, zorder=4)
|
|
88
|
+
|
|
89
|
+
ax.set_ylabel('Height (nm)', fontsize=9)
|
|
90
|
+
|
|
91
|
+
# Scan-specific title
|
|
92
|
+
scan_title = f'Scan {i+1}: {os.path.basename(scan["filename"])}\n'
|
|
93
|
+
scan_title += f'Mean = {scan["mean_angle"]:.2f}° ± {scan["std_angle"]:.2f}° '
|
|
94
|
+
scan_title += f'(N={scan["n_grooves"]} grooves) | Spacing: {scan["period_nm"]:.2f} nm'
|
|
95
|
+
ax.set_title(scan_title, fontsize=9)
|
|
96
|
+
ax.legend(loc='best', fontsize=7)
|
|
97
|
+
ax.grid(True, alpha=0.3)
|
|
98
|
+
|
|
99
|
+
if i == n_scans - 1:
|
|
100
|
+
ax.set_xlabel('Position (µm)', fontsize=9)
|
|
101
|
+
else:
|
|
102
|
+
ax.text(0.5, 0.5, f'Scan {i+1}\nProfile data not available',
|
|
103
|
+
ha='center', va='center', transform=ax.transAxes)
|
|
104
|
+
|
|
105
|
+
plt.tight_layout(rect=[0, 0, 1, 0.99])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _plot_single_scan(result, label):
|
|
109
|
+
"""
|
|
110
|
+
Plot a single AFM scan
|
|
111
|
+
|
|
112
|
+
Parameters:
|
|
113
|
+
result: result dictionary
|
|
114
|
+
label: sample label
|
|
115
|
+
"""
|
|
116
|
+
fig, ax = plt.subplots(1, 1, figsize=(14, 4))
|
|
117
|
+
|
|
118
|
+
if 'raw_x' in result and 'flat_y' in result:
|
|
119
|
+
# Plot profile
|
|
120
|
+
ax.plot(result['raw_x'], result['flat_y'], 'k-', linewidth=0.8, alpha=0.5,
|
|
121
|
+
label='Flattened profile')
|
|
122
|
+
|
|
123
|
+
# Plot groove centers
|
|
124
|
+
if 'groove_centers' in result:
|
|
125
|
+
ax.plot(result['raw_x'][result['groove_centers']],
|
|
126
|
+
result['flat_y'][result['groove_centers']],
|
|
127
|
+
'ro', markersize=4, alpha=0.7, label='Groove centers')
|
|
128
|
+
|
|
129
|
+
# Plot analyzed regions and fits
|
|
130
|
+
if 'quality' in result:
|
|
131
|
+
for j, qual in enumerate(result['quality']):
|
|
132
|
+
if 'regions' in qual:
|
|
133
|
+
reg = qual['regions']
|
|
134
|
+
blaze_x_um = reg['blaze_x_trim'] / 1000
|
|
135
|
+
blaze_y = reg['blaze_y_trim']
|
|
136
|
+
fit_line = reg['blaze_fit']
|
|
137
|
+
|
|
138
|
+
if j == 0:
|
|
139
|
+
ax.plot(blaze_x_um, blaze_y, 'b-', linewidth=2.5, alpha=0.8,
|
|
140
|
+
label='Analyzed regions', zorder=3)
|
|
141
|
+
ax.plot(blaze_x_um, fit_line, 'cyan', linewidth=1.5,
|
|
142
|
+
linestyle='--', alpha=0.7, label='Linear fits', zorder=4)
|
|
143
|
+
else:
|
|
144
|
+
ax.plot(blaze_x_um, blaze_y, 'b-', linewidth=2.5, alpha=0.8, zorder=3)
|
|
145
|
+
ax.plot(blaze_x_um, fit_line, 'cyan', linewidth=1.5,
|
|
146
|
+
linestyle='--', alpha=0.7, zorder=4)
|
|
147
|
+
|
|
148
|
+
ax.set_ylabel('Height (nm)', fontsize=10)
|
|
149
|
+
ax.set_xlabel('Position (µm)', fontsize=10)
|
|
150
|
+
|
|
151
|
+
# Title
|
|
152
|
+
title = f'{label}: Mean = {result["mean_angle"]:.2f}° ± {result["std_angle"]:.2f}° '
|
|
153
|
+
title += f'(N={result["n_grooves"]} grooves) | Spacing: {result["period_nm"]:.2f} nm'
|
|
154
|
+
ax.set_title(title, fontsize=11, fontweight='bold')
|
|
155
|
+
ax.legend(loc='best', fontsize=9)
|
|
156
|
+
ax.grid(True, alpha=0.3)
|
|
157
|
+
else:
|
|
158
|
+
ax.text(0.5, 0.5, f'{label}\nProfile data not available',
|
|
159
|
+
ha='center', va='center', transform=ax.transAxes)
|
|
160
|
+
|
|
161
|
+
plt.tight_layout()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Legacy function for backwards compatibility
|
|
165
|
+
def plot_sample_profiles_comparison(results, labels):
|
|
166
|
+
"""
|
|
167
|
+
DEPRECATED: Use plot_sample_profiles_by_temperature instead
|
|
168
|
+
|
|
169
|
+
Plot AFM profiles for all samples in a single multi-panel figure
|
|
170
|
+
This is kept for backwards compatibility but not recommended for many samples
|
|
171
|
+
"""
|
|
172
|
+
import warnings
|
|
173
|
+
warnings.warn("plot_sample_profiles_comparison is deprecated. "
|
|
174
|
+
"Use plot_sample_profiles_by_temperature instead.",
|
|
175
|
+
DeprecationWarning)
|
|
176
|
+
|
|
177
|
+
n_samples = len(results)
|
|
178
|
+
|
|
179
|
+
if n_samples <= 3:
|
|
180
|
+
subplot_height = 3
|
|
181
|
+
elif n_samples == 4:
|
|
182
|
+
subplot_height = 2.5
|
|
183
|
+
else:
|
|
184
|
+
subplot_height = 2.0
|
|
185
|
+
|
|
186
|
+
total_height = min(subplot_height * n_samples, 11)
|
|
187
|
+
fig, axes = plt.subplots(n_samples, 1, figsize=(14, total_height))
|
|
188
|
+
|
|
189
|
+
if n_samples == 1:
|
|
190
|
+
axes = [axes]
|
|
191
|
+
|
|
192
|
+
for i, (r, label) in enumerate(zip(results, labels)):
|
|
193
|
+
if 'raw_x' in r and 'flat_y' in r:
|
|
194
|
+
axes[i].plot(r['raw_x'], r['flat_y'], 'k-', linewidth=0.8, alpha=0.5,
|
|
195
|
+
label='Flattened profile')
|
|
196
|
+
|
|
197
|
+
if 'groove_centers' in r:
|
|
198
|
+
axes[i].plot(r['raw_x'][r['groove_centers']], r['flat_y'][r['groove_centers']],
|
|
199
|
+
'ro', markersize=4, alpha=0.7, label='Groove centers')
|
|
200
|
+
|
|
201
|
+
if 'quality' in r:
|
|
202
|
+
for j, qual in enumerate(r['quality']):
|
|
203
|
+
if 'regions' in qual:
|
|
204
|
+
reg = qual['regions']
|
|
205
|
+
blaze_x_um = reg['blaze_x_trim'] / 1000
|
|
206
|
+
blaze_y = reg['blaze_y_trim']
|
|
207
|
+
fit_line = reg['blaze_fit']
|
|
208
|
+
|
|
209
|
+
if j == 0:
|
|
210
|
+
axes[i].plot(blaze_x_um, blaze_y, 'b-', linewidth=2.5, alpha=0.8,
|
|
211
|
+
label='Analyzed regions', zorder=3)
|
|
212
|
+
axes[i].plot(blaze_x_um, fit_line, 'cyan', linewidth=1.5,
|
|
213
|
+
linestyle='--', alpha=0.7, label='Linear fits', zorder=4)
|
|
214
|
+
else:
|
|
215
|
+
axes[i].plot(blaze_x_um, blaze_y, 'b-', linewidth=2.5, alpha=0.8, zorder=3)
|
|
216
|
+
axes[i].plot(blaze_x_um, fit_line, 'cyan', linewidth=1.5,
|
|
217
|
+
linestyle='--', alpha=0.7, zorder=4)
|
|
218
|
+
|
|
219
|
+
axes[i].set_ylabel('Height (nm)', fontsize=9)
|
|
220
|
+
|
|
221
|
+
# Include spacing in the title
|
|
222
|
+
spacing_str = f"Spacing: {r['period_nm']:.2f} nm"
|
|
223
|
+
axes[i].set_title(f'{label}: Mean = {r["mean_angle"]:.2f}° ± {r["std_angle"]:.2f}° (N={r["n_grooves"]}) | {spacing_str}',
|
|
224
|
+
fontsize=10)
|
|
225
|
+
axes[i].legend(loc='best', fontsize=7)
|
|
226
|
+
axes[i].grid(True, alpha=0.3)
|
|
227
|
+
|
|
228
|
+
if i == n_samples - 1:
|
|
229
|
+
axes[i].set_xlabel('Position (µm)', fontsize=9)
|
|
230
|
+
else:
|
|
231
|
+
axes[i].text(0.5, 0.5, f'{label}\nProfile data not available',
|
|
232
|
+
ha='center', va='center', transform=axes[i].transAxes)
|
|
233
|
+
|
|
234
|
+
plt.tight_layout()
|