Files
tono/backend/nodes/smm_analysis.py

137 lines
4.9 KiB
Python

"""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)