synthetic surface and spm specific features
This commit is contained in:
136
backend/nodes/smm_analysis.py
Normal file
136
backend/nodes/smm_analysis.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""SMM analysis — scanning microwave microscopy 3-point calibration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import DataField
|
||||
|
||||
|
||||
@register_node(display_name="SMM Analysis")
|
||||
class SMMAnalysis:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"s11_amplitude": ("DATA_FIELD",),
|
||||
"s11_phase": ("DATA_FIELD",),
|
||||
"frequency": ("FLOAT", {
|
||||
"default": 1e9, "min": 1e6, "max": 100e9, "step": 1e6,
|
||||
}),
|
||||
"ref_impedance": ("FLOAT", {
|
||||
"default": 50.0, "min": 1.0, "max": 1000.0,
|
||||
}),
|
||||
"cal_c1": ("FLOAT", {
|
||||
"default": 1e-15, "min": 1e-18, "max": 1e-9, "step": 1e-18,
|
||||
}),
|
||||
"cal_c2": ("FLOAT", {
|
||||
"default": 10e-15, "min": 1e-18, "max": 1e-9, "step": 1e-18,
|
||||
}),
|
||||
"cal_c3": ("FLOAT", {
|
||||
"default": 100e-15, "min": 1e-18, "max": 1e-9, "step": 1e-18,
|
||||
}),
|
||||
"cal_s11_1": ("FLOAT", {
|
||||
"default": 0.9, "min": -1.0, "max": 1.0, "step": 0.001,
|
||||
}),
|
||||
"cal_s11_2": ("FLOAT", {
|
||||
"default": 0.5, "min": -1.0, "max": 1.0, "step": 0.001,
|
||||
}),
|
||||
"cal_s11_3": ("FLOAT", {
|
||||
"default": 0.1, "min": -1.0, "max": 1.0, "step": 0.001,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('DATA_FIELD', 'capacitance'),
|
||||
('DATA_FIELD', 'impedance_real'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Scanning microwave microscopy analysis using 3-point calibration. "
|
||||
"Corrects measured S11 reflection data using three known calibration "
|
||||
"capacitances to solve for the VNA error coefficients (e00, e01, e11), "
|
||||
"then extracts tip-sample capacitance and real impedance maps."
|
||||
)
|
||||
|
||||
def process(
|
||||
self,
|
||||
s11_amplitude: DataField,
|
||||
s11_phase: DataField,
|
||||
frequency: float,
|
||||
ref_impedance: float,
|
||||
cal_c1: float,
|
||||
cal_c2: float,
|
||||
cal_c3: float,
|
||||
cal_s11_1: float,
|
||||
cal_s11_2: float,
|
||||
cal_s11_3: float,
|
||||
) -> tuple:
|
||||
omega = 2.0 * np.pi * frequency
|
||||
|
||||
# --- Step 1: Compute ideal S11 for each calibration capacitance ---
|
||||
cal_caps = [cal_c1, cal_c2, cal_c3]
|
||||
cal_s11_meas = [cal_s11_1, cal_s11_2, cal_s11_3]
|
||||
s11_ideal = []
|
||||
for c in cal_caps:
|
||||
z_load = 1.0 / (1j * omega * c)
|
||||
s11_ideal.append(
|
||||
(z_load - ref_impedance) / (z_load + ref_impedance)
|
||||
)
|
||||
|
||||
# --- Step 2: Solve for error coefficients (e00, e01, e11) ---
|
||||
# Model: S11m_i = e00 + e01 * S11a_i / (1 - e11 * S11a_i)
|
||||
# Rearranged: S11m_i * (1 - e11 * S11a_i) = e00 * (1 - e11 * S11a_i) + e01 * S11a_i
|
||||
# S11m_i = e00 + S11a_i * (e01 + e11 * S11m_i - e11 * e00)
|
||||
#
|
||||
# Linear form with unknowns [e00, e01, e11]:
|
||||
# S11m_i = e00 + e01 * S11a_i / (1 - e11 * S11a_i)
|
||||
# Multiply through by (1 - e11 * S11a_i):
|
||||
# S11m_i - S11m_i * e11 * S11a_i = e00 - e00 * e11 * S11a_i + e01 * S11a_i
|
||||
# Rearrange to a linear system in (e00, e01, e11):
|
||||
# S11m_i = e00 + e01 * S11a_i + e11 * S11a_i * S11m_i
|
||||
# This follows because the product e00*e11 can be absorbed by defining
|
||||
# the system as: S11m = e00 + e01*Ga + e11*Ga*S11m
|
||||
# where Ga = S11_ideal.
|
||||
#
|
||||
# Matrix row: [1, S11a_i, S11a_i * S11m_i] * [e00, e01, e11]^T = S11m_i
|
||||
|
||||
A = np.zeros((3, 3), dtype=complex)
|
||||
b = np.zeros(3, dtype=complex)
|
||||
for i in range(3):
|
||||
ga = s11_ideal[i]
|
||||
sm = cal_s11_meas[i]
|
||||
A[i, 0] = 1.0
|
||||
A[i, 1] = ga
|
||||
A[i, 2] = ga * sm
|
||||
b[i] = sm
|
||||
|
||||
coeffs = np.linalg.solve(A, b)
|
||||
e00 = coeffs[0]
|
||||
e01 = coeffs[1]
|
||||
e11 = coeffs[2]
|
||||
|
||||
# --- Step 3: Apply calibration to measured S11 data ---
|
||||
amp = np.asarray(s11_amplitude.data, dtype=np.float64)
|
||||
phase = np.asarray(s11_phase.data, dtype=np.float64)
|
||||
s11m_complex = amp * np.exp(1j * phase)
|
||||
|
||||
# Invert the error model: Ga = (S11m - e00) / (e01 + e11*(S11m - e00))
|
||||
s11_corrected = (s11m_complex - e00) / (e01 + e11 * (s11m_complex - e00))
|
||||
|
||||
# --- Step 4: Convert corrected S11 to impedance ---
|
||||
z_tip = ref_impedance * (1.0 + s11_corrected) / (1.0 - s11_corrected)
|
||||
|
||||
# --- Step 5: Extract capacitance from admittance ---
|
||||
y_tip = 1.0 / z_tip
|
||||
capacitance = np.imag(y_tip) / omega
|
||||
|
||||
impedance_real = np.real(z_tip)
|
||||
|
||||
cap_field = s11_amplitude.replace(data=capacitance, si_unit_z="F")
|
||||
imp_field = s11_amplitude.replace(data=impedance_real, si_unit_z="Ohm")
|
||||
|
||||
return (cap_field, imp_field)
|
||||
Reference in New Issue
Block a user