Files
tono/backend/nodes/calibration.py
2026-04-04 00:25:53 -07:00

144 lines
4.6 KiB
Python

"""Calibration — apply lateral and height calibration corrections."""
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="Calibration")
class Calibration:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"field": ("DATA_FIELD",),
"xy_mode": (["keep", "set_size", "scale"], {"default": "keep"}),
"z_mode": (["keep", "set_range", "scale", "offset"], {"default": "keep"}),
"xreal_new": ("FLOAT", {
"default": 1e-6,
"min": 1e-12,
"max": 1.0,
"step": 1e-9,
"show_when_widget_value": {"xy_mode": ["set_size"]},
}),
"yreal_new": ("FLOAT", {
"default": 1e-6,
"min": 1e-12,
"max": 1.0,
"step": 1e-9,
"show_when_widget_value": {"xy_mode": ["set_size"]},
}),
"xy_scale": ("FLOAT", {
"default": 1.0,
"min": 0.001,
"max": 1000.0,
"step": 0.001,
"show_when_widget_value": {"xy_mode": ["scale"]},
}),
"z_min": ("FLOAT", {
"default": 0.0,
"min": -1e-3,
"max": 1e-3,
"step": 1e-12,
"show_when_widget_value": {"z_mode": ["set_range"]},
}),
"z_max": ("FLOAT", {
"default": 1e-9,
"min": -1e-3,
"max": 1e-3,
"step": 1e-12,
"show_when_widget_value": {"z_mode": ["set_range"]},
}),
"z_scale": ("FLOAT", {
"default": 1.0,
"min": 0.001,
"max": 1000.0,
"step": 0.001,
"show_when_widget_value": {"z_mode": ["scale"]},
}),
"z_offset": ("FLOAT", {
"default": 0.0,
"min": -1e-3,
"max": 1e-3,
"step": 1e-12,
"show_when_widget_value": {"z_mode": ["offset"]},
}),
"xy_unit": ("STRING", {"default": ""}),
"z_unit": ("STRING", {"default": ""}),
}
}
OUTPUTS = (
('DATA_FIELD', 'result'),
)
FUNCTION = "process"
DESCRIPTION = (
"Apply lateral and height calibration corrections to a DATA_FIELD. "
"Lateral mode can set explicit physical size or scale uniformly. "
"Height mode can rescale to a target range, multiply by a factor, "
"or add a constant offset. Optionally override the XY or Z unit strings. "
"Equivalent to Gwyddion's calibrate functionality."
)
def process(
self,
field: DataField,
xy_mode: str,
z_mode: str,
xreal_new: float,
yreal_new: float,
xy_scale: float,
z_min: float,
z_max: float,
z_scale: float,
z_offset: float,
xy_unit: str,
z_unit: str,
) -> tuple:
data = np.asarray(field.data, dtype=np.float64).copy()
xreal = float(field.xreal)
yreal = float(field.yreal)
si_unit_xy = field.si_unit_xy
si_unit_z = field.si_unit_z
# --- lateral calibration ---
if xy_mode == "set_size":
xreal = float(xreal_new)
yreal = float(yreal_new)
elif xy_mode == "scale":
xreal *= float(xy_scale)
yreal *= float(xy_scale)
# "keep" → no change
# --- height calibration ---
if z_mode == "set_range":
cur_min = float(data.min())
cur_max = float(data.max())
if cur_max > cur_min:
data = float(z_min) + (data - cur_min) * (float(z_max) - float(z_min)) / (cur_max - cur_min)
else:
data[:] = float(z_min)
elif z_mode == "scale":
data *= float(z_scale)
elif z_mode == "offset":
data += float(z_offset)
# "keep" → no change
# --- unit overrides ---
if xy_unit:
si_unit_xy = xy_unit
if z_unit:
si_unit_z = z_unit
return (field.replace(
data=data,
xreal=xreal,
yreal=yreal,
si_unit_xy=si_unit_xy,
si_unit_z=si_unit_z,
),)