work on igor note

This commit is contained in:
2026-03-28 18:48:25 -07:00
parent ce74cf0a3e
commit 559b1ae09a
26 changed files with 166 additions and 109 deletions

View File

@@ -31,11 +31,11 @@ CUSTOM_FILE_FONT = "Custom File"
PREVIEW_MARKUP_REFERENCE_DIM = 512
class RecordTable(list):
class DataTable(list):
"""Tabular rows with a shared schema, e.g. grain statistics."""
class MeasureTable(list):
class RecordTable(list):
"""Named scalar measurements, typically rows of quantity/value/unit."""

View File

@@ -318,7 +318,7 @@ class ExecutionEngine:
def _fingerprint_bytes(self, value: Any) -> bytes:
import numpy as np
from backend.data_types import DataField, ImageData, LineData, MeasureTable, MeshModel, RecordTable
from backend.data_types import DataField, ImageData, LineData, RecordTable, MeshModel, DataTable
if value is None:
return b"null"
@@ -381,7 +381,7 @@ class ExecutionEngine:
).encode()
return b"|".join([b"ndarray", header, memoryview(array).tobytes()])
if isinstance(value, (MeasureTable, RecordTable, list)):
if isinstance(value, (RecordTable, DataTable, list)):
return b"[" + b",".join(self._fingerprint_bytes(item) for item in value) + b"]"
if isinstance(value, tuple):
@@ -494,7 +494,7 @@ class ExecutionEngine:
on_preview(node_id, preview)
return
if type_name in ("TABLE", "MEASURE_TABLE", "RECORD_TABLE") and isinstance(value, list) and on_table:
if type_name in ("TABLE", "RECORD_TABLE", "DATA_TABLE") and isinstance(value, list) and on_table:
on_table(node_id, value)
return

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.data_types import LineData, MeasureTable
from backend.data_types import LineData, RecordTable
from backend.nodes.spectral_common import acf_line_from_data
@@ -34,7 +34,7 @@ class ACF1D:
OUTPUTS = (
('LINE', 'acf'),
('MEASURE_TABLE', 'measurement'),
('RECORD_TABLE', 'measurement'),
)
FUNCTION = "process"
@@ -58,4 +58,4 @@ class ACF1D:
if peak_lag is not None:
rows.append({"quantity": "Peak period", "value": peak_lag, "unit": x_unit})
return (acf_line, MeasureTable(rows))
return (acf_line, RecordTable(rows))

View File

@@ -5,7 +5,7 @@ import numpy as np
from backend.data_types import (
DataField,
ImageData,
MeasureTable,
RecordTable,
_apply_angle_measure_overlay,
encode_preview,
image_metadata,
@@ -114,7 +114,7 @@ class AngleMeasure:
OUTPUTS = (
('ANNOTATION_SOURCE', 'output'),
('MEASURE_TABLE', 'measurements'),
('RECORD_TABLE', 'measurements'),
)
FUNCTION = "process"
@@ -189,7 +189,7 @@ class AngleMeasure:
stroke_width=resolved_stroke_width,
color=resolved_color,
)
table = MeasureTable([
table = RecordTable([
{"quantity": "Angle", "value": angle_deg, "unit": "deg"},
{"quantity": "Arm A length", "value": length_a, "unit": length_unit},
{"quantity": "Arm B length", "value": length_b, "unit": length_unit},

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, LineData, MeasureTable, encode_preview, render_datafield_preview
from backend.data_types import DataField, LineData, RecordTable, encode_preview, render_datafield_preview
@register_node(display_name="Cursors")
@@ -28,7 +28,7 @@ class Cursors:
}
OUTPUTS = (
('MEASURE_TABLE', 'measurement'),
('RECORD_TABLE', 'measurement'),
('COORDPAIR', 'coord_pair'),
)
FUNCTION = "process"
@@ -107,7 +107,7 @@ class Cursors:
})
length = float(np.hypot(xb - xa, yb - ya))
table = MeasureTable([
table = RecordTable([
{"quantity": "Length", "value": length, "unit": x_unit},
{"quantity": "dx", "value": xb - xa, "unit": x_unit},
{"quantity": "dy", "value": yb - ya, "unit": y_unit},
@@ -159,7 +159,7 @@ class Cursors:
"b_locked": locked,
})
table = MeasureTable([
table = RecordTable([
{"quantity": "A x", "value": ax, "unit": field.si_unit_xy},
{"quantity": "A y", "value": ay, "unit": field.si_unit_xy},
{"quantity": "A z", "value": z1, "unit": field.si_unit_z},

View File

@@ -8,7 +8,7 @@ from scipy.ndimage import map_coordinates
from backend.data_types import (
DataField,
LineData,
MeasureTable,
RecordTable,
_apply_markup_overlay,
encode_preview,
render_datafield_preview,
@@ -289,7 +289,7 @@ class Curvature:
OUTPUTS = (
('ANNOTATION_SOURCE', 'output'),
('MEASURE_TABLE', 'measurements'),
('RECORD_TABLE', 'measurements'),
('LINE', 'profile_1'),
('LINE', 'profile_2'),
)
@@ -313,7 +313,7 @@ class Curvature:
if results is None:
emit_warning("Curvature requires at least six usable pixels for the quadratic fit.")
table = MeasureTable([])
table = RecordTable([])
emit_table(table)
emit_preview(encode_preview(render_datafield_preview(field, field.colormap)))
empty = _empty_profile(field.si_unit_xy, field.si_unit_z)
@@ -345,7 +345,7 @@ class Curvature:
markup_spec = _curvature_markup(field, results["x0"], results["y0"], intersections)
output = field.replace(overlays=[*field.overlays, markup_spec])
table = MeasureTable([
table = RecordTable([
{"quantity": "Center x position", "value": float(results["x0"]), "unit": field.si_unit_xy},
{"quantity": "Center y position", "value": float(results["y0"]), "unit": field.si_unit_xy},
{"quantity": "Center value", "value": float(results["z0"]), "unit": field.si_unit_z},

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.data_types import LineData, MeasureTable
from backend.data_types import LineData, RecordTable
@register_node(display_name="FFT 1D")
@@ -21,7 +21,7 @@ class FFT1D:
OUTPUTS = (
("LINE", "frequency_plot"),
('MEASURE_TABLE', 'measurement'),
('RECORD_TABLE', 'measurement'),
)
FUNCTION = "process"
@@ -55,7 +55,7 @@ class FFT1D:
peak_period = float(period_axis[np.argmax(spectrum)])
table = MeasureTable([
table = RecordTable([
{"quantity": "Peak period", "value": peak_period, "unit": spatial_unit},
])

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass
import numpy as np
from scipy.ndimage import map_coordinates
from backend.data_types import LineData, MeasureTable
from backend.data_types import LineData, RecordTable
from backend.execution_context import emit_overlay, emit_table, emit_warning
from backend.node_registry import register_node
@@ -307,7 +307,7 @@ class FractalDimension:
OUTPUTS = (
('FLOAT', 'dimension'),
('LINE', 'curve'),
('MEASURE_TABLE', 'measurements'),
('RECORD_TABLE', 'measurements'),
)
FUNCTION = "process"
@@ -348,7 +348,7 @@ class FractalDimension:
dimension = float("nan")
emit_warning("Fractal fit range contains fewer than two usable points.")
table = MeasureTable([
table = RecordTable([
{"quantity": "Dimension", "value": float(dimension), "unit": ""},
{"quantity": "Fit slope", "value": float(slope), "unit": ""},
{"quantity": "Fit intercept", "value": float(intercept), "unit": ""},

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.data_types import DataField, RecordTable
from backend.data_types import DataField, DataTable
from backend.nodes.helpers import _square_unit
@@ -18,7 +18,7 @@ class GrainAnalysis:
}
OUTPUTS = (
('RECORD_TABLE', 'grain_stats'),
('DATA_TABLE', 'grain_stats'),
)
FUNCTION = "process"
@@ -38,7 +38,7 @@ class GrainAnalysis:
xy_unit = str(field.si_unit_xy or "").strip()
z_unit = str(field.si_unit_z or "").strip()
rows = RecordTable()
rows = DataTable()
for gid in range(1, n_grains + 1):
grain_pixels = labeled == gid
area_px = int(grain_pixels.sum())

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, MeasureTable
from backend.data_types import DataField, RecordTable
@register_node(display_name="Histogram")
@@ -22,7 +22,7 @@ class Histogram:
}
OUTPUTS = (
('MEASURE_TABLE', 'measurements'),
('RECORD_TABLE', 'measurements'),
('COORDPAIR', 'marker_pair'),
)
FUNCTION = "process"
@@ -88,7 +88,7 @@ class Histogram:
"b_locked": False,
})
table = MeasureTable([
table = RecordTable([
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
{"quantity": "delta X", "value": xb - xa, "unit": field.si_unit_z},
{"quantity": "A position", "value": xa, "unit": field.si_unit_z},

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import re
from backend.node_registry import register_node
from backend.data_types import MeasureTable
from backend.data_types import DataTable
from backend.nodes.helpers import _resolve_path, _import_ibw_loader
@@ -22,14 +22,10 @@ def _parse_ibw_note(note_bytes: bytes) -> list[dict]:
if not match:
continue
key = match.group(1).strip()
raw_val = match.group(2).strip()
value = match.group(2).strip()
if not key:
continue
try:
value = float(raw_val)
except (ValueError, TypeError):
continue
rows.append({"quantity": key, "value": value, "unit": ""})
rows.append({"key": key, "value": value})
return rows
@@ -48,13 +44,13 @@ class IBWNote:
}
OUTPUTS = (
('MEASURE_TABLE', 'note'),
('DATA_TABLE', 'note'),
)
FUNCTION = "load"
DESCRIPTION = (
"Read the Note metadata from an .ibw file and display numeric entries "
"as a measurement table. Non-numeric note entries are skipped."
"Read the Note metadata from an .ibw file and display all entries "
"as a table of key/value pairs."
)
def load(self, filename: str = "", path: str | None = None) -> tuple:
@@ -73,6 +69,6 @@ class IBWNote:
rows = _parse_ibw_note(note_bytes)
if not rows:
raise ValueError("No numeric metadata found in the .ibw note.")
raise ValueError("No metadata found in the .ibw note.")
return (MeasureTable(rows),)
return (DataTable(rows),)

View File

@@ -2,6 +2,8 @@ from __future__ import annotations
from functools import lru_cache
import numpy as np
from pathlib import Path
import nanonispy as nap
import gwyfile
from backend.node_registry import register_node
from backend.execution_context import emit_warning
@@ -25,6 +27,7 @@ class Image:
}
OUTPUTS = (
("FILE_PATH", 'path'),
('DATA_FIELD', 'field'),
)
FUNCTION = "load"
@@ -65,7 +68,7 @@ class Image:
if ext not in _SPM_EXTENSIONS:
self._send_warning("Uncalibrated data — no physical dimensions.")
return fields
return (str(path_obj.resolve()),) + fields
def _send_warning(self, message: str):
emit_warning(message)
@@ -92,11 +95,6 @@ class Image:
@staticmethod
def _load_gwy_all(path: Path) -> list[DataField]:
try:
import gwyfile
except ImportError:
raise ImportError("Install 'gwyfile' package to load .gwy files: pip install gwyfile")
obj = gwyfile.load(str(path))
channels = gwyfile.util.get_datafields(obj)
if not channels:
@@ -118,11 +116,6 @@ class Image:
@staticmethod
def _load_sxm_all(path: Path) -> list[DataField]:
try:
import nanonispy as nap
except ImportError:
raise ImportError("Install 'nanonispy' package to load .sxm files: pip install nanonispy")
sxm = nap.read.Scan(str(path))
signals = sxm.signals
if not signals:

View File

@@ -45,8 +45,20 @@ class PreviewImage:
input=None,
colormap_map=None,
) -> tuple:
field = input if isinstance(input, DataField) else None
image = None if field is not None else input
if isinstance(input, DataField):
field = input
image = None
elif isinstance(input, np.ndarray):
field = None
image = input
elif input is not None:
raise TypeError(
f"Preview expects an IMAGE or DATA_FIELD — got {type(input).__name__}. "
"Check that you are connected to the DATA_FIELD output, not the path socket."
)
else:
field = None
image = None
resolved_colormap = resolve_colormap_input(
colormap,

View File

@@ -9,8 +9,8 @@ class PrintTable:
def INPUT_TYPES(cls):
return {
"required": {
"table": ("MEASURE_TABLE", {
"accepted_types": ["RECORD_TABLE"],
"table": ("RECORD_TABLE", {
"accepted_types": ["DATA_TABLE"],
}),
}
}

View File

@@ -35,8 +35,8 @@ class Save:
"IMAGE",
"ANNOTATION_SOURCE",
"LINE",
"MEASURE_TABLE",
"RECORD_TABLE",
"DATA_TABLE",
"MESH_MODEL",
"FLOAT",
],
@@ -48,8 +48,8 @@ class Save:
"IMAGE": ["PNG", "TIFF", "NPZ"],
"ANNOTATION_SOURCE": ["PNG", "TIFF", "NPZ"],
"LINE": ["CSV", "NPZ", "JSON"],
"MEASURE_TABLE": ["CSV", "JSON"],
"RECORD_TABLE": ["CSV", "JSON"],
"DATA_TABLE": ["CSV", "JSON"],
"FLOAT": ["TXT", "JSON"],
"MESH_MODEL": ["OBJ", "STL"],
},

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.data_types import DataField, MeasureTable
from backend.data_types import DataField, RecordTable
@register_node(display_name="Statistics")
@@ -15,7 +15,7 @@ class Statistics:
}
OUTPUTS = (
('MEASURE_TABLE', 'stats'),
('RECORD_TABLE', 'stats'),
)
FUNCTION = "process"
@@ -31,7 +31,7 @@ class Statistics:
skewness = float(np.mean(((d - mean) / rms) ** 3)) if rms > 0 else 0.0
kurtosis = float(np.mean(((d - mean) / rms) ** 4)) if rms > 0 else 0.0
table = MeasureTable([
table = RecordTable([
{"quantity": "min", "value": float(d.min()), "unit": field.si_unit_z},
{"quantity": "max", "value": float(d.max()), "unit": field.si_unit_z},
{"quantity": "mean", "value": mean, "unit": field.si_unit_z},

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_value
from backend.data_types import DataField, LineData, MeasureTable
from backend.data_types import DataField, LineData, RecordTable
from backend.nodes.helpers import (
LINE_OPS,
TABLE_OPS,
@@ -17,7 +17,7 @@ from backend.nodes.helpers import (
@register_node(display_name="Stats")
class Stats:
"""Polymorphic scalar stats node for LINE, RECORD_TABLE, DATA_FIELD, or IMAGE inputs."""
"""Polymorphic scalar stats node for LINE, DATA_TABLE, DATA_FIELD, or IMAGE inputs."""
_broadcast_value_fn = None
_current_node_id: str = ""
@@ -27,20 +27,20 @@ class Stats:
return {
"required": {
"input": ("DATA_FIELD", {
"accepted_types": ["IMAGE", "LINE", "RECORD_TABLE"],
"accepted_types": ["IMAGE", "LINE", "DATA_TABLE"],
}),
"column": ("STRING", {
"default": "value",
"choices_from_table_input": "input",
"show_when_source_type": {
"input": ["RECORD_TABLE"],
"input": ["DATA_TABLE"],
},
}),
"operation": ("STRING", {
"default": "mean",
"choices_by_source_type": {
"LINE": list(LINE_OPS.keys()),
"RECORD_TABLE": list(TABLE_OPS.keys()),
"DATA_TABLE": list(TABLE_OPS.keys()),
"DATA_FIELD": list(ARRAY_OPS.keys()),
"IMAGE": list(ARRAY_OPS.keys()),
},
@@ -62,7 +62,7 @@ class Stats:
def process(self, input, operation: str, column: str = "value") -> tuple:
source_type, values, resolved_column = self._resolve_input_values(input, column)
if source_type == "RECORD_TABLE":
if source_type == "DATA_TABLE":
ops = TABLE_OPS
elif source_type == "LINE":
ops = LINE_OPS
@@ -93,7 +93,7 @@ class Stats:
return _apply_scalar_unit(input_value.y_unit, operation)
return ""
if source_type == "RECORD_TABLE" and isinstance(input_value, list) and column:
if source_type == "DATA_TABLE" and isinstance(input_value, list) and column:
return _apply_scalar_unit(_common_table_unit(input_value, column), operation)
return ""
@@ -103,7 +103,7 @@ class Stats:
values = np.asarray(input_value.data, dtype=np.float64)
return ("DATA_FIELD", values.ravel(), None)
if isinstance(input_value, MeasureTable):
if isinstance(input_value, RecordTable):
raise ValueError("Stats only accepts record tables, not measurement tables.")
if isinstance(input_value, list):
@@ -113,7 +113,7 @@ class Stats:
values = extract_numeric_table_values(input_value, column_name)
if not values:
raise ValueError(f"Column '{column_name}' has no numeric values.")
return ("RECORD_TABLE", np.asarray(values, dtype=np.float64), column_name)
return ("DATA_TABLE", np.asarray(values, dtype=np.float64), column_name)
if isinstance(input_value, LineData):
values = np.asarray(input_value.data, dtype=np.float64)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from backend.node_registry import register_node
from backend.execution_context import emit_value
from backend.data_types import MeasureTable
from backend.data_types import RecordTable
from backend.nodes.helpers import _measurement_entry, _measurement_value, _scalar_payload
@@ -12,13 +12,13 @@ class ValueDisplay:
return {
"required": {
"value": ("FLOAT", {
"accepted_types": ["MEASURE_TABLE"],
"accepted_types": ["RECORD_TABLE"],
}),
"measurement": ("STRING", {
"default": "",
"choices_from_measure_input": "value",
"show_when_source_type": {
"value": ["MEASURE_TABLE"],
"value": ["RECORD_TABLE"],
},
}),
}
@@ -36,7 +36,7 @@ class ValueDisplay:
def display_value(self, value, measurement: str = "") -> tuple:
unit = ""
if isinstance(value, MeasureTable):
if isinstance(value, RecordTable):
row = _measurement_entry(value, measurement)
numeric = _measurement_value(value, measurement)
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""