Files
tono/backend/nodes/lateral_force_sim.py

89 lines
2.9 KiB
Python

"""Lateral Force Simulation — topography artifacts in friction channels."""
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="Lateral Force Simulation")
class LateralForceSim:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"field": ("DATA_FIELD",),
"direction": (["forward", "reverse", "both"],),
"friction_coefficient": ("FLOAT", {
"default": 0.3, "min": 0.0, "max": 10.0, "step": 0.01,
}),
"adhesion": ("FLOAT", {
"default": 1e-9, "min": 0.0, "max": 1e-6, "step": 1e-12,
}),
"load": ("FLOAT", {
"default": 10e-9, "min": 1e-12, "max": 1e-6, "step": 1e-12,
}),
}
}
OUTPUTS = (
('DATA_FIELD', 'forward'),
('DATA_FIELD', 'reverse'),
)
FUNCTION = "process"
DESCRIPTION = (
"Simulates topography-induced artifacts in lateral force (friction) "
"microscopy channels. Computes the lateral force signal from surface "
"slope, friction coefficient, adhesion, and normal load for forward "
"and/or reverse scan directions. Based on a contact-mechanics model "
"where the measured lateral force depends on the local tilt angle of "
"the sample surface."
)
KEYWORDS = ("lfm", "friction", "ffm", "tribology")
def process(
self,
field: DataField,
direction: str,
friction_coefficient: float,
adhesion: float,
load: float,
) -> tuple:
data = np.asarray(field.data, dtype=np.float64)
# Surface slope angle from x-gradient of topography
dz_dx = np.gradient(data, axis=1)
theta = np.arctan2(dz_dx, field.dx)
sin_theta = np.sin(theta)
cos_theta = np.cos(theta)
va = load * sin_theta
vc = cos_theta
vb = friction_coefficient * (load * vc + adhesion)
vd = friction_coefficient * sin_theta
# Forward scan (tip moves in +x)
denom_fwd = vc - vd
safe_fwd = np.abs(denom_fwd) >= 1e-30
lateral_forward = np.where(safe_fwd, (va + vb) / np.where(safe_fwd, denom_fwd, 1.0), 0.0)
# Reverse scan (tip moves in -x)
denom_rev = vc + vd
safe_rev = np.abs(denom_rev) >= 1e-30
lateral_reverse = np.where(safe_rev, -(va - vb) / np.where(safe_rev, denom_rev, 1.0), 0.0)
if direction == "forward":
lateral_reverse = lateral_forward.copy()
elif direction == "reverse":
lateral_forward = lateral_reverse.copy()
out_fwd = field.replace(data=lateral_forward, si_unit_z="N")
out_rev = field.replace(data=lateral_reverse, si_unit_z="N")
return (out_fwd, out_rev)