139 lines
5.0 KiB
Python
139 lines
5.0 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."
|
|
)
|
|
|
|
KEYWORDS = ("microwave", "s11", "capacitance", "impedance", "vna", "calibration")
|
|
|
|
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)
|