work on igor note
This commit is contained in:
@@ -31,11 +31,11 @@ CUSTOM_FILE_FONT = "Custom File"
|
|||||||
PREVIEW_MARKUP_REFERENCE_DIM = 512
|
PREVIEW_MARKUP_REFERENCE_DIM = 512
|
||||||
|
|
||||||
|
|
||||||
class RecordTable(list):
|
class DataTable(list):
|
||||||
"""Tabular rows with a shared schema, e.g. grain statistics."""
|
"""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."""
|
"""Named scalar measurements, typically rows of quantity/value/unit."""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
def _fingerprint_bytes(self, value: Any) -> bytes:
|
def _fingerprint_bytes(self, value: Any) -> bytes:
|
||||||
import numpy as np
|
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:
|
if value is None:
|
||||||
return b"null"
|
return b"null"
|
||||||
@@ -381,7 +381,7 @@ class ExecutionEngine:
|
|||||||
).encode()
|
).encode()
|
||||||
return b"|".join([b"ndarray", header, memoryview(array).tobytes()])
|
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"]"
|
return b"[" + b",".join(self._fingerprint_bytes(item) for item in value) + b"]"
|
||||||
|
|
||||||
if isinstance(value, tuple):
|
if isinstance(value, tuple):
|
||||||
@@ -494,7 +494,7 @@ class ExecutionEngine:
|
|||||||
on_preview(node_id, preview)
|
on_preview(node_id, preview)
|
||||||
return
|
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)
|
on_table(node_id, value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from backend.node_registry import register_node
|
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
|
from backend.nodes.spectral_common import acf_line_from_data
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class ACF1D:
|
|||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('LINE', 'acf'),
|
('LINE', 'acf'),
|
||||||
('MEASURE_TABLE', 'measurement'),
|
('RECORD_TABLE', 'measurement'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|
||||||
@@ -58,4 +58,4 @@ class ACF1D:
|
|||||||
if peak_lag is not None:
|
if peak_lag is not None:
|
||||||
rows.append({"quantity": "Peak period", "value": peak_lag, "unit": x_unit})
|
rows.append({"quantity": "Peak period", "value": peak_lag, "unit": x_unit})
|
||||||
|
|
||||||
return (acf_line, MeasureTable(rows))
|
return (acf_line, RecordTable(rows))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import numpy as np
|
|||||||
from backend.data_types import (
|
from backend.data_types import (
|
||||||
DataField,
|
DataField,
|
||||||
ImageData,
|
ImageData,
|
||||||
MeasureTable,
|
RecordTable,
|
||||||
_apply_angle_measure_overlay,
|
_apply_angle_measure_overlay,
|
||||||
encode_preview,
|
encode_preview,
|
||||||
image_metadata,
|
image_metadata,
|
||||||
@@ -114,7 +114,7 @@ class AngleMeasure:
|
|||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('ANNOTATION_SOURCE', 'output'),
|
('ANNOTATION_SOURCE', 'output'),
|
||||||
('MEASURE_TABLE', 'measurements'),
|
('RECORD_TABLE', 'measurements'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class AngleMeasure:
|
|||||||
stroke_width=resolved_stroke_width,
|
stroke_width=resolved_stroke_width,
|
||||||
color=resolved_color,
|
color=resolved_color,
|
||||||
)
|
)
|
||||||
table = MeasureTable([
|
table = RecordTable([
|
||||||
{"quantity": "Angle", "value": angle_deg, "unit": "deg"},
|
{"quantity": "Angle", "value": angle_deg, "unit": "deg"},
|
||||||
{"quantity": "Arm A length", "value": length_a, "unit": length_unit},
|
{"quantity": "Arm A length", "value": length_a, "unit": length_unit},
|
||||||
{"quantity": "Arm B length", "value": length_b, "unit": length_unit},
|
{"quantity": "Arm B length", "value": length_b, "unit": length_unit},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.execution_context import emit_overlay
|
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")
|
@register_node(display_name="Cursors")
|
||||||
@@ -28,7 +28,7 @@ class Cursors:
|
|||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('MEASURE_TABLE', 'measurement'),
|
('RECORD_TABLE', 'measurement'),
|
||||||
('COORDPAIR', 'coord_pair'),
|
('COORDPAIR', 'coord_pair'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
@@ -107,7 +107,7 @@ class Cursors:
|
|||||||
})
|
})
|
||||||
|
|
||||||
length = float(np.hypot(xb - xa, yb - ya))
|
length = float(np.hypot(xb - xa, yb - ya))
|
||||||
table = MeasureTable([
|
table = RecordTable([
|
||||||
{"quantity": "Length", "value": length, "unit": x_unit},
|
{"quantity": "Length", "value": length, "unit": x_unit},
|
||||||
{"quantity": "dx", "value": xb - xa, "unit": x_unit},
|
{"quantity": "dx", "value": xb - xa, "unit": x_unit},
|
||||||
{"quantity": "dy", "value": yb - ya, "unit": y_unit},
|
{"quantity": "dy", "value": yb - ya, "unit": y_unit},
|
||||||
@@ -159,7 +159,7 @@ class Cursors:
|
|||||||
"b_locked": locked,
|
"b_locked": locked,
|
||||||
})
|
})
|
||||||
|
|
||||||
table = MeasureTable([
|
table = RecordTable([
|
||||||
{"quantity": "A x", "value": ax, "unit": field.si_unit_xy},
|
{"quantity": "A x", "value": ax, "unit": field.si_unit_xy},
|
||||||
{"quantity": "A y", "value": ay, "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},
|
{"quantity": "A z", "value": z1, "unit": field.si_unit_z},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from scipy.ndimage import map_coordinates
|
|||||||
from backend.data_types import (
|
from backend.data_types import (
|
||||||
DataField,
|
DataField,
|
||||||
LineData,
|
LineData,
|
||||||
MeasureTable,
|
RecordTable,
|
||||||
_apply_markup_overlay,
|
_apply_markup_overlay,
|
||||||
encode_preview,
|
encode_preview,
|
||||||
render_datafield_preview,
|
render_datafield_preview,
|
||||||
@@ -289,7 +289,7 @@ class Curvature:
|
|||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('ANNOTATION_SOURCE', 'output'),
|
('ANNOTATION_SOURCE', 'output'),
|
||||||
('MEASURE_TABLE', 'measurements'),
|
('RECORD_TABLE', 'measurements'),
|
||||||
('LINE', 'profile_1'),
|
('LINE', 'profile_1'),
|
||||||
('LINE', 'profile_2'),
|
('LINE', 'profile_2'),
|
||||||
)
|
)
|
||||||
@@ -313,7 +313,7 @@ class Curvature:
|
|||||||
|
|
||||||
if results is None:
|
if results is None:
|
||||||
emit_warning("Curvature requires at least six usable pixels for the quadratic fit.")
|
emit_warning("Curvature requires at least six usable pixels for the quadratic fit.")
|
||||||
table = MeasureTable([])
|
table = RecordTable([])
|
||||||
emit_table(table)
|
emit_table(table)
|
||||||
emit_preview(encode_preview(render_datafield_preview(field, field.colormap)))
|
emit_preview(encode_preview(render_datafield_preview(field, field.colormap)))
|
||||||
empty = _empty_profile(field.si_unit_xy, field.si_unit_z)
|
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)
|
markup_spec = _curvature_markup(field, results["x0"], results["y0"], intersections)
|
||||||
output = field.replace(overlays=[*field.overlays, markup_spec])
|
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 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 y position", "value": float(results["y0"]), "unit": field.si_unit_xy},
|
||||||
{"quantity": "Center value", "value": float(results["z0"]), "unit": field.si_unit_z},
|
{"quantity": "Center value", "value": float(results["z0"]), "unit": field.si_unit_z},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from backend.node_registry import register_node
|
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")
|
@register_node(display_name="FFT 1D")
|
||||||
@@ -21,7 +21,7 @@ class FFT1D:
|
|||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
("LINE", "frequency_plot"),
|
("LINE", "frequency_plot"),
|
||||||
('MEASURE_TABLE', 'measurement'),
|
('RECORD_TABLE', 'measurement'),
|
||||||
)
|
)
|
||||||
|
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
@@ -55,7 +55,7 @@ class FFT1D:
|
|||||||
|
|
||||||
peak_period = float(period_axis[np.argmax(spectrum)])
|
peak_period = float(period_axis[np.argmax(spectrum)])
|
||||||
|
|
||||||
table = MeasureTable([
|
table = RecordTable([
|
||||||
{"quantity": "Peak period", "value": peak_period, "unit": spatial_unit},
|
{"quantity": "Peak period", "value": peak_period, "unit": spatial_unit},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.ndimage import map_coordinates
|
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.execution_context import emit_overlay, emit_table, emit_warning
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ class FractalDimension:
|
|||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('FLOAT', 'dimension'),
|
('FLOAT', 'dimension'),
|
||||||
('LINE', 'curve'),
|
('LINE', 'curve'),
|
||||||
('MEASURE_TABLE', 'measurements'),
|
('RECORD_TABLE', 'measurements'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ class FractalDimension:
|
|||||||
dimension = float("nan")
|
dimension = float("nan")
|
||||||
emit_warning("Fractal fit range contains fewer than two usable points.")
|
emit_warning("Fractal fit range contains fewer than two usable points.")
|
||||||
|
|
||||||
table = MeasureTable([
|
table = RecordTable([
|
||||||
{"quantity": "Dimension", "value": float(dimension), "unit": ""},
|
{"quantity": "Dimension", "value": float(dimension), "unit": ""},
|
||||||
{"quantity": "Fit slope", "value": float(slope), "unit": ""},
|
{"quantity": "Fit slope", "value": float(slope), "unit": ""},
|
||||||
{"quantity": "Fit intercept", "value": float(intercept), "unit": ""},
|
{"quantity": "Fit intercept", "value": float(intercept), "unit": ""},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
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
|
from backend.nodes.helpers import _square_unit
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class GrainAnalysis:
|
|||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('RECORD_TABLE', 'grain_stats'),
|
('DATA_TABLE', 'grain_stats'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class GrainAnalysis:
|
|||||||
xy_unit = str(field.si_unit_xy or "").strip()
|
xy_unit = str(field.si_unit_xy or "").strip()
|
||||||
z_unit = str(field.si_unit_z or "").strip()
|
z_unit = str(field.si_unit_z or "").strip()
|
||||||
|
|
||||||
rows = RecordTable()
|
rows = DataTable()
|
||||||
for gid in range(1, n_grains + 1):
|
for gid in range(1, n_grains + 1):
|
||||||
grain_pixels = labeled == gid
|
grain_pixels = labeled == gid
|
||||||
area_px = int(grain_pixels.sum())
|
area_px = int(grain_pixels.sum())
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.execution_context import emit_overlay
|
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")
|
@register_node(display_name="Histogram")
|
||||||
@@ -22,7 +22,7 @@ class Histogram:
|
|||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('MEASURE_TABLE', 'measurements'),
|
('RECORD_TABLE', 'measurements'),
|
||||||
('COORDPAIR', 'marker_pair'),
|
('COORDPAIR', 'marker_pair'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
@@ -88,7 +88,7 @@ class Histogram:
|
|||||||
"b_locked": False,
|
"b_locked": False,
|
||||||
})
|
})
|
||||||
|
|
||||||
table = MeasureTable([
|
table = RecordTable([
|
||||||
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
|
{"quantity": "delta Y", "value": yb - ya, "unit": count_unit},
|
||||||
{"quantity": "delta X", "value": xb - xa, "unit": field.si_unit_z},
|
{"quantity": "delta X", "value": xb - xa, "unit": field.si_unit_z},
|
||||||
{"quantity": "A position", "value": xa, "unit": field.si_unit_z},
|
{"quantity": "A position", "value": xa, "unit": field.si_unit_z},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.node_registry import register_node
|
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
|
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:
|
if not match:
|
||||||
continue
|
continue
|
||||||
key = match.group(1).strip()
|
key = match.group(1).strip()
|
||||||
raw_val = match.group(2).strip()
|
value = match.group(2).strip()
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
try:
|
rows.append({"key": key, "value": value})
|
||||||
value = float(raw_val)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
continue
|
|
||||||
rows.append({"quantity": key, "value": value, "unit": ""})
|
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -48,13 +44,13 @@ class IBWNote:
|
|||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('MEASURE_TABLE', 'note'),
|
('DATA_TABLE', 'note'),
|
||||||
)
|
)
|
||||||
FUNCTION = "load"
|
FUNCTION = "load"
|
||||||
|
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
"Read the Note metadata from an .ibw file and display numeric entries "
|
"Read the Note metadata from an .ibw file and display all entries "
|
||||||
"as a measurement table. Non-numeric note entries are skipped."
|
"as a table of key/value pairs."
|
||||||
)
|
)
|
||||||
|
|
||||||
def load(self, filename: str = "", path: str | None = None) -> tuple:
|
def load(self, filename: str = "", path: str | None = None) -> tuple:
|
||||||
@@ -73,6 +69,6 @@ class IBWNote:
|
|||||||
|
|
||||||
rows = _parse_ibw_note(note_bytes)
|
rows = _parse_ibw_note(note_bytes)
|
||||||
if not rows:
|
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),)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import nanonispy as nap
|
||||||
|
import gwyfile
|
||||||
|
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.execution_context import emit_warning
|
from backend.execution_context import emit_warning
|
||||||
@@ -25,6 +27,7 @@ class Image:
|
|||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
|
("FILE_PATH", 'path'),
|
||||||
('DATA_FIELD', 'field'),
|
('DATA_FIELD', 'field'),
|
||||||
)
|
)
|
||||||
FUNCTION = "load"
|
FUNCTION = "load"
|
||||||
@@ -65,7 +68,7 @@ class Image:
|
|||||||
if ext not in _SPM_EXTENSIONS:
|
if ext not in _SPM_EXTENSIONS:
|
||||||
self._send_warning("Uncalibrated data — no physical dimensions.")
|
self._send_warning("Uncalibrated data — no physical dimensions.")
|
||||||
|
|
||||||
return fields
|
return (str(path_obj.resolve()),) + fields
|
||||||
|
|
||||||
def _send_warning(self, message: str):
|
def _send_warning(self, message: str):
|
||||||
emit_warning(message)
|
emit_warning(message)
|
||||||
@@ -92,11 +95,6 @@ class Image:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_gwy_all(path: Path) -> list[DataField]:
|
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))
|
obj = gwyfile.load(str(path))
|
||||||
channels = gwyfile.util.get_datafields(obj)
|
channels = gwyfile.util.get_datafields(obj)
|
||||||
if not channels:
|
if not channels:
|
||||||
@@ -118,11 +116,6 @@ class Image:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_sxm_all(path: Path) -> list[DataField]:
|
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))
|
sxm = nap.read.Scan(str(path))
|
||||||
signals = sxm.signals
|
signals = sxm.signals
|
||||||
if not signals:
|
if not signals:
|
||||||
|
|||||||
@@ -45,8 +45,20 @@ class PreviewImage:
|
|||||||
input=None,
|
input=None,
|
||||||
colormap_map=None,
|
colormap_map=None,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
field = input if isinstance(input, DataField) else None
|
if isinstance(input, DataField):
|
||||||
image = None if field is not None else input
|
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(
|
resolved_colormap = resolve_colormap_input(
|
||||||
colormap,
|
colormap,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class PrintTable:
|
|||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"table": ("MEASURE_TABLE", {
|
"table": ("RECORD_TABLE", {
|
||||||
"accepted_types": ["RECORD_TABLE"],
|
"accepted_types": ["DATA_TABLE"],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ class Save:
|
|||||||
"IMAGE",
|
"IMAGE",
|
||||||
"ANNOTATION_SOURCE",
|
"ANNOTATION_SOURCE",
|
||||||
"LINE",
|
"LINE",
|
||||||
"MEASURE_TABLE",
|
|
||||||
"RECORD_TABLE",
|
"RECORD_TABLE",
|
||||||
|
"DATA_TABLE",
|
||||||
"MESH_MODEL",
|
"MESH_MODEL",
|
||||||
"FLOAT",
|
"FLOAT",
|
||||||
],
|
],
|
||||||
@@ -48,8 +48,8 @@ class Save:
|
|||||||
"IMAGE": ["PNG", "TIFF", "NPZ"],
|
"IMAGE": ["PNG", "TIFF", "NPZ"],
|
||||||
"ANNOTATION_SOURCE": ["PNG", "TIFF", "NPZ"],
|
"ANNOTATION_SOURCE": ["PNG", "TIFF", "NPZ"],
|
||||||
"LINE": ["CSV", "NPZ", "JSON"],
|
"LINE": ["CSV", "NPZ", "JSON"],
|
||||||
"MEASURE_TABLE": ["CSV", "JSON"],
|
|
||||||
"RECORD_TABLE": ["CSV", "JSON"],
|
"RECORD_TABLE": ["CSV", "JSON"],
|
||||||
|
"DATA_TABLE": ["CSV", "JSON"],
|
||||||
"FLOAT": ["TXT", "JSON"],
|
"FLOAT": ["TXT", "JSON"],
|
||||||
"MESH_MODEL": ["OBJ", "STL"],
|
"MESH_MODEL": ["OBJ", "STL"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
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")
|
@register_node(display_name="Statistics")
|
||||||
@@ -15,7 +15,7 @@ class Statistics:
|
|||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('MEASURE_TABLE', 'stats'),
|
('RECORD_TABLE', 'stats'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class Statistics:
|
|||||||
skewness = float(np.mean(((d - mean) / rms) ** 3)) if rms > 0 else 0.0
|
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
|
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": "min", "value": float(d.min()), "unit": field.si_unit_z},
|
||||||
{"quantity": "max", "value": float(d.max()), "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},
|
{"quantity": "mean", "value": mean, "unit": field.si_unit_z},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.execution_context import emit_value
|
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 (
|
from backend.nodes.helpers import (
|
||||||
LINE_OPS,
|
LINE_OPS,
|
||||||
TABLE_OPS,
|
TABLE_OPS,
|
||||||
@@ -17,7 +17,7 @@ from backend.nodes.helpers import (
|
|||||||
|
|
||||||
@register_node(display_name="Stats")
|
@register_node(display_name="Stats")
|
||||||
class 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
|
_broadcast_value_fn = None
|
||||||
_current_node_id: str = ""
|
_current_node_id: str = ""
|
||||||
@@ -27,20 +27,20 @@ class Stats:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"input": ("DATA_FIELD", {
|
"input": ("DATA_FIELD", {
|
||||||
"accepted_types": ["IMAGE", "LINE", "RECORD_TABLE"],
|
"accepted_types": ["IMAGE", "LINE", "DATA_TABLE"],
|
||||||
}),
|
}),
|
||||||
"column": ("STRING", {
|
"column": ("STRING", {
|
||||||
"default": "value",
|
"default": "value",
|
||||||
"choices_from_table_input": "input",
|
"choices_from_table_input": "input",
|
||||||
"show_when_source_type": {
|
"show_when_source_type": {
|
||||||
"input": ["RECORD_TABLE"],
|
"input": ["DATA_TABLE"],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
"operation": ("STRING", {
|
"operation": ("STRING", {
|
||||||
"default": "mean",
|
"default": "mean",
|
||||||
"choices_by_source_type": {
|
"choices_by_source_type": {
|
||||||
"LINE": list(LINE_OPS.keys()),
|
"LINE": list(LINE_OPS.keys()),
|
||||||
"RECORD_TABLE": list(TABLE_OPS.keys()),
|
"DATA_TABLE": list(TABLE_OPS.keys()),
|
||||||
"DATA_FIELD": list(ARRAY_OPS.keys()),
|
"DATA_FIELD": list(ARRAY_OPS.keys()),
|
||||||
"IMAGE": 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:
|
def process(self, input, operation: str, column: str = "value") -> tuple:
|
||||||
source_type, values, resolved_column = self._resolve_input_values(input, column)
|
source_type, values, resolved_column = self._resolve_input_values(input, column)
|
||||||
|
|
||||||
if source_type == "RECORD_TABLE":
|
if source_type == "DATA_TABLE":
|
||||||
ops = TABLE_OPS
|
ops = TABLE_OPS
|
||||||
elif source_type == "LINE":
|
elif source_type == "LINE":
|
||||||
ops = LINE_OPS
|
ops = LINE_OPS
|
||||||
@@ -93,7 +93,7 @@ class Stats:
|
|||||||
return _apply_scalar_unit(input_value.y_unit, operation)
|
return _apply_scalar_unit(input_value.y_unit, operation)
|
||||||
return ""
|
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 _apply_scalar_unit(_common_table_unit(input_value, column), operation)
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
@@ -103,7 +103,7 @@ class Stats:
|
|||||||
values = np.asarray(input_value.data, dtype=np.float64)
|
values = np.asarray(input_value.data, dtype=np.float64)
|
||||||
return ("DATA_FIELD", values.ravel(), None)
|
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.")
|
raise ValueError("Stats only accepts record tables, not measurement tables.")
|
||||||
|
|
||||||
if isinstance(input_value, list):
|
if isinstance(input_value, list):
|
||||||
@@ -113,7 +113,7 @@ class Stats:
|
|||||||
values = extract_numeric_table_values(input_value, column_name)
|
values = extract_numeric_table_values(input_value, column_name)
|
||||||
if not values:
|
if not values:
|
||||||
raise ValueError(f"Column '{column_name}' has no numeric 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):
|
if isinstance(input_value, LineData):
|
||||||
values = np.asarray(input_value.data, dtype=np.float64)
|
values = np.asarray(input_value.data, dtype=np.float64)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.execution_context import emit_value
|
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
|
from backend.nodes.helpers import _measurement_entry, _measurement_value, _scalar_payload
|
||||||
|
|
||||||
|
|
||||||
@@ -12,13 +12,13 @@ class ValueDisplay:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"value": ("FLOAT", {
|
"value": ("FLOAT", {
|
||||||
"accepted_types": ["MEASURE_TABLE"],
|
"accepted_types": ["RECORD_TABLE"],
|
||||||
}),
|
}),
|
||||||
"measurement": ("STRING", {
|
"measurement": ("STRING", {
|
||||||
"default": "",
|
"default": "",
|
||||||
"choices_from_measure_input": "value",
|
"choices_from_measure_input": "value",
|
||||||
"show_when_source_type": {
|
"show_when_source_type": {
|
||||||
"value": ["MEASURE_TABLE"],
|
"value": ["RECORD_TABLE"],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ class ValueDisplay:
|
|||||||
|
|
||||||
def display_value(self, value, measurement: str = "") -> tuple:
|
def display_value(self, value, measurement: str = "") -> tuple:
|
||||||
unit = ""
|
unit = ""
|
||||||
if isinstance(value, MeasureTable):
|
if isinstance(value, RecordTable):
|
||||||
row = _measurement_entry(value, measurement)
|
row = _measurement_entry(value, measurement)
|
||||||
numeric = _measurement_value(value, measurement)
|
numeric = _measurement_value(value, measurement)
|
||||||
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""
|
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""
|
||||||
|
|||||||
@@ -1271,7 +1271,7 @@ function Flow() {
|
|||||||
if (!isTrackedNodeRequestCurrent(loadNodeOutputRequestVersionsRef.current, nodeId, requestVersion)) {
|
if (!isTrackedNodeRequestCurrent(loadNodeOutputRequestVersionsRef.current, nodeId, requestVersion)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setNodeOutputs(nodeId, ['DATA_FIELD'], ['field'], { output_paths: [] });
|
setNodeOutputs(nodeId, ['FILE_PATH', 'DATA_FIELD'], ['path', 'field'], { output_paths: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1281,8 +1281,8 @@ function Flow() {
|
|||||||
}
|
}
|
||||||
setNodeOutputs(
|
setNodeOutputs(
|
||||||
nodeId,
|
nodeId,
|
||||||
channels.map((channel) => channel.type),
|
['FILE_PATH', ...channels.map((channel) => channel.type)],
|
||||||
channels.map((channel) => channel.name),
|
['path', ...channels.map((channel) => channel.name)],
|
||||||
{ output_paths: [] },
|
{ output_paths: [] },
|
||||||
);
|
);
|
||||||
}, [getResolvedPathInput, reactFlow, setNodeOutputs]);
|
}, [getResolvedPathInput, reactFlow, setNodeOutputs]);
|
||||||
|
|||||||
@@ -786,6 +786,17 @@ function ColorMapStopsEditor({ nodeId, name, value, onChange }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function NodeTable({ rows }) {
|
function NodeTable({ rows }) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const handler = (e) => e.stopPropagation();
|
||||||
|
el.addEventListener('wheel', handler, { passive: false });
|
||||||
|
return () => el.removeEventListener('wheel', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const columns = getTableColumns(rows);
|
const columns = getTableColumns(rows);
|
||||||
if (columns.length === 0) return null;
|
if (columns.length === 0) return null;
|
||||||
const lowerColumns = columns.map((column) => String(column).toLowerCase());
|
const lowerColumns = columns.map((column) => String(column).toLowerCase());
|
||||||
@@ -804,9 +815,32 @@ function NodeTable({ rows }) {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredRows = query.trim()
|
||||||
|
? rows.filter((row) =>
|
||||||
|
columns.some((col) => {
|
||||||
|
const cell = formatTableRowCell(row, col);
|
||||||
|
return String(cell).toLowerCase().includes(query.toLowerCase());
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: rows;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="node-table-wrap">
|
<div className="node-table-wrap">
|
||||||
<div className="node-table-scroll">
|
{rows.length > 5 && (
|
||||||
|
<div
|
||||||
|
className="node-table-search"
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="node-table-search-input nodrag"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search…"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="node-table-scroll" ref={scrollRef}>
|
||||||
<table className="node-table-grid">
|
<table className="node-table-grid">
|
||||||
{hasMeasurementLayout && (
|
{hasMeasurementLayout && (
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -823,7 +857,7 @@ function NodeTable({ rows }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, rowIndex) => (
|
{filteredRows.map((row, rowIndex) => (
|
||||||
<tr key={row.id ?? row.quantity ?? rowIndex}>
|
<tr key={row.id ?? row.quantity ?? rowIndex}>
|
||||||
{columns.map((column) => {
|
{columns.map((column) => {
|
||||||
const value = row?.[column];
|
const value = row?.[column];
|
||||||
@@ -1379,7 +1413,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
|||||||
const tableInputName = opts?.choices_from_table_input;
|
const tableInputName = opts?.choices_from_table_input;
|
||||||
if (!tableInputName) return [];
|
if (!tableInputName) return [];
|
||||||
const sourceType = getSourceTypeForInput(s, nodeId, tableInputName);
|
const sourceType = getSourceTypeForInput(s, nodeId, tableInputName);
|
||||||
if (sourceType !== 'RECORD_TABLE') return [];
|
if (sourceType !== 'DATA_TABLE') return [];
|
||||||
const sourceNode = getSourceNodeForInput(s, nodeId, tableInputName);
|
const sourceNode = getSourceNodeForInput(s, nodeId, tableInputName);
|
||||||
const rows = sourceNode?.data?.tableRows;
|
const rows = sourceNode?.data?.tableRows;
|
||||||
return Array.isArray(rows) ? getTableColumns(rows) : [];
|
return Array.isArray(rows) ? getTableColumns(rows) : [];
|
||||||
@@ -1393,7 +1427,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
|||||||
const measurementInputName = opts?.choices_from_measure_input;
|
const measurementInputName = opts?.choices_from_measure_input;
|
||||||
if (!measurementInputName) return [];
|
if (!measurementInputName) return [];
|
||||||
const sourceType = getSourceTypeForInput(s, nodeId, measurementInputName);
|
const sourceType = getSourceTypeForInput(s, nodeId, measurementInputName);
|
||||||
if (sourceType !== 'MEASURE_TABLE') return [];
|
if (sourceType !== 'RECORD_TABLE') return [];
|
||||||
const sourceNode = getSourceNodeForInput(s, nodeId, measurementInputName);
|
const sourceNode = getSourceNodeForInput(s, nodeId, measurementInputName);
|
||||||
const rows = sourceNode?.data?.tableRows;
|
const rows = sourceNode?.data?.tableRows;
|
||||||
return Array.isArray(rows) ? getMeasurementChoices(rows) : [];
|
return Array.isArray(rows) ? getMeasurementChoices(rows) : [];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ── Shared type & color constants ─────────────────────────────────────
|
// ── Shared type & color constants ─────────────────────────────────────
|
||||||
|
|
||||||
export const DATA_TYPES = new Set([
|
export const DATA_TYPES = new Set([
|
||||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE',
|
'DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE', 'DATA_TABLE',
|
||||||
'COORD', 'ANNOTATION_SOURCE', 'COLORMAP',
|
'COORD', 'ANNOTATION_SOURCE', 'COLORMAP',
|
||||||
'MESH_MODEL', 'FONT', 'FILE_PATH', 'DIRECTORY', 'COORDPAIR',
|
'MESH_MODEL', 'FONT', 'FILE_PATH', 'DIRECTORY', 'COORDPAIR',
|
||||||
]);
|
]);
|
||||||
@@ -12,8 +12,8 @@ export const TYPE_COLORS = {
|
|||||||
DATA_FIELD: '#3a7abf',
|
DATA_FIELD: '#3a7abf',
|
||||||
IMAGE: '#00ff08a0',
|
IMAGE: '#00ff08a0',
|
||||||
LINE: '#ffbe5c',
|
LINE: '#ffbe5c',
|
||||||
MEASURE_TABLE: '#35e2fd',
|
RECORD_TABLE: '#35e2fd',
|
||||||
RECORD_TABLE: '#ff7474',
|
DATA_TABLE: '#ff7474',
|
||||||
COORD: '#e91ed1',
|
COORD: '#e91ed1',
|
||||||
COORDPAIR: '#5cb861',
|
COORDPAIR: '#5cb861',
|
||||||
FLOAT: '#ab3197',
|
FLOAT: '#ab3197',
|
||||||
|
|||||||
@@ -1296,6 +1296,26 @@ html, body, #root {
|
|||||||
padding: 4px 10px 8px;
|
padding: 4px 10px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-table-search {
|
||||||
|
padding: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-table-search-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--bg-deep);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-table-search-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.node-table-scroll {
|
.node-table-scroll {
|
||||||
max-height: 220px;
|
max-height: 220px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -1310,7 +1330,7 @@ html, body, #root {
|
|||||||
font-family: "SF Mono", "Fira Code", monospace;
|
font-family: "SF Mono", "Fira Code", monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-table);
|
color: var(--text-table);
|
||||||
table-layout: auto;
|
table-layout: fixed;
|
||||||
font-variant-numeric: tabular-nums lining-nums;
|
font-variant-numeric: tabular-nums lining-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1319,6 +1339,8 @@ html, body, #root {
|
|||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-bottom: 1px solid var(--border-table);
|
border-bottom: 1px solid var(--border-table);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ test('retired save alias types are no longer first-class socket types', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('accepted_types extend canonical socket compatibility without reintroducing alias types', () => {
|
test('accepted_types extend canonical socket compatibility without reintroducing alias types', () => {
|
||||||
const spec = ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }];
|
const spec = ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }];
|
||||||
|
|
||||||
assert.equal(isDataSocketSpec(spec), true);
|
assert.equal(isDataSocketSpec(spec), true);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
Array.from(getAcceptedSocketTypes(spec)).sort(),
|
Array.from(getAcceptedSocketTypes(spec)).sort(),
|
||||||
['MEASURE_TABLE', 'RECORD_TABLE'],
|
['RECORD_TABLE', 'DATA_TABLE'],
|
||||||
);
|
);
|
||||||
assert.equal(socketSpecAcceptsType('RECORD_TABLE', spec), true);
|
assert.equal(socketSpecAcceptsType('DATA_TABLE', spec), true);
|
||||||
assert.equal(socketSpecAcceptsType('LINE', spec), false);
|
assert.equal(socketSpecAcceptsType('LINE', spec), false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ test('serializeExecutionGraph treats accepted_types inputs as sockets, not widge
|
|||||||
className: 'TableSource',
|
className: 'TableSource',
|
||||||
definition: {
|
definition: {
|
||||||
input: { required: {}, optional: {} },
|
input: { required: {}, optional: {} },
|
||||||
output: ['RECORD_TABLE'],
|
output: ['DATA_TABLE'],
|
||||||
output_name: ['rows'],
|
output_name: ['rows'],
|
||||||
manual_trigger: false,
|
manual_trigger: false,
|
||||||
},
|
},
|
||||||
@@ -501,7 +501,7 @@ test('serializeExecutionGraph treats accepted_types inputs as sockets, not widge
|
|||||||
definition: {
|
definition: {
|
||||||
input: {
|
input: {
|
||||||
required: {
|
required: {
|
||||||
table: ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }],
|
table: ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }],
|
||||||
},
|
},
|
||||||
optional: {},
|
optional: {},
|
||||||
},
|
},
|
||||||
@@ -514,9 +514,9 @@ test('serializeExecutionGraph treats accepted_types inputs as sockets, not widge
|
|||||||
const edges = [
|
const edges = [
|
||||||
{
|
{
|
||||||
source: '1',
|
source: '1',
|
||||||
sourceHandle: 'output::0::RECORD_TABLE',
|
sourceHandle: 'output::0::DATA_TABLE',
|
||||||
target: '2',
|
target: '2',
|
||||||
targetHandle: 'input::table::MEASURE_TABLE',
|
targetHandle: 'input::table::RECORD_TABLE',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -542,7 +542,7 @@ test('hasBlockingAutoRunInput still blocks unconnected accepted_types sockets',
|
|||||||
manual_trigger: false,
|
manual_trigger: false,
|
||||||
input: {
|
input: {
|
||||||
required: {
|
required: {
|
||||||
input: ['DATA_FIELD', { accepted_types: ['IMAGE', 'LINE', 'RECORD_TABLE'] }],
|
input: ['DATA_FIELD', { accepted_types: ['IMAGE', 'LINE', 'DATA_TABLE'] }],
|
||||||
},
|
},
|
||||||
optional: {},
|
optional: {},
|
||||||
},
|
},
|
||||||
@@ -556,7 +556,7 @@ test('hasBlockingAutoRunInput still blocks unconnected accepted_types sockets',
|
|||||||
hasBlockingAutoRunInput(node, [
|
hasBlockingAutoRunInput(node, [
|
||||||
{
|
{
|
||||||
source: '1',
|
source: '1',
|
||||||
sourceHandle: 'output::0::RECORD_TABLE',
|
sourceHandle: 'output::0::DATA_TABLE',
|
||||||
target: '2',
|
target: '2',
|
||||||
targetHandle: 'input::input::DATA_FIELD',
|
targetHandle: 'input::input::DATA_FIELD',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ test('buildDefaultWidgetValues keeps non-data required widget defaults', () => {
|
|||||||
input: {
|
input: {
|
||||||
required: {
|
required: {
|
||||||
input: ['ANNOTATION_SOURCE', { label: 'Input' }],
|
input: ['ANNOTATION_SOURCE', { label: 'Input' }],
|
||||||
table: ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }],
|
table: ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }],
|
||||||
shape: [['line', 'rectangle', 'circle', 'arrow'], { default: 'arrow' }],
|
shape: [['line', 'rectangle', 'circle', 'arrow'], { default: 'arrow' }],
|
||||||
stroke_color: ['STRING', { default: '#ff0000', color_picker: true }],
|
stroke_color: ['STRING', { default: '#ff0000', color_picker: true }],
|
||||||
stroke_width: ['INT', { default: 3 }],
|
stroke_width: ['INT', { default: 3 }],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
sys.path.insert(0, ".")
|
sys.path.insert(0, ".")
|
||||||
from backend.data_types import DataField, LineData, MeasureTable, RecordTable, datafield_to_uint8, render_datafield_preview
|
from backend.data_types import DataField, LineData, RecordTable, DataTable, datafield_to_uint8, render_datafield_preview
|
||||||
|
|
||||||
|
|
||||||
def make_field(data=None, shape=(64, 64), xreal=1e-6, yreal=1e-6):
|
def make_field(data=None, shape=(64, 64), xreal=1e-6, yreal=1e-6):
|
||||||
@@ -2000,8 +2000,8 @@ def test_print_table():
|
|||||||
node = PrintTable()
|
node = PrintTable()
|
||||||
|
|
||||||
table_spec = PrintTable.INPUT_TYPES()["required"]["table"]
|
table_spec = PrintTable.INPUT_TYPES()["required"]["table"]
|
||||||
assert table_spec[0] == "MEASURE_TABLE"
|
assert table_spec[0] == "RECORD_TABLE"
|
||||||
assert table_spec[1]["accepted_types"] == ["RECORD_TABLE"]
|
assert table_spec[1]["accepted_types"] == ["DATA_TABLE"]
|
||||||
|
|
||||||
captured = []
|
captured = []
|
||||||
PrintTable._broadcast_table_fn = lambda node_id, rows: captured.append(rows)
|
PrintTable._broadcast_table_fn = lambda node_id, rows: captured.append(rows)
|
||||||
@@ -2023,7 +2023,7 @@ def test_value_display():
|
|||||||
node = ValueDisplay()
|
node = ValueDisplay()
|
||||||
value_spec = ValueDisplay.INPUT_TYPES()["required"]["value"]
|
value_spec = ValueDisplay.INPUT_TYPES()["required"]["value"]
|
||||||
assert value_spec[0] == "FLOAT"
|
assert value_spec[0] == "FLOAT"
|
||||||
assert value_spec[1]["accepted_types"] == ["MEASURE_TABLE"]
|
assert value_spec[1]["accepted_types"] == ["RECORD_TABLE"]
|
||||||
|
|
||||||
captured = []
|
captured = []
|
||||||
ValueDisplay._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
ValueDisplay._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
||||||
@@ -2033,7 +2033,7 @@ def test_value_display():
|
|||||||
assert result == (3.25,)
|
assert result == (3.25,)
|
||||||
assert captured == [("test", {"value": 3.25})]
|
assert captured == [("test", {"value": 3.25})]
|
||||||
|
|
||||||
measurements = MeasureTable([
|
measurements = RecordTable([
|
||||||
{"quantity": "delta X", "value": 1.7e-7, "unit": "m"},
|
{"quantity": "delta X", "value": 1.7e-7, "unit": "m"},
|
||||||
{"quantity": "delta Y", "value": 463, "unit": "count"},
|
{"quantity": "delta Y", "value": 463, "unit": "count"},
|
||||||
])
|
])
|
||||||
@@ -2845,7 +2845,7 @@ def test_stats():
|
|||||||
node = Stats()
|
node = Stats()
|
||||||
input_spec = Stats.INPUT_TYPES()["required"]["input"]
|
input_spec = Stats.INPUT_TYPES()["required"]["input"]
|
||||||
assert input_spec[0] == "DATA_FIELD"
|
assert input_spec[0] == "DATA_FIELD"
|
||||||
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "RECORD_TABLE"]
|
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "DATA_TABLE"]
|
||||||
|
|
||||||
captured = []
|
captured = []
|
||||||
Stats._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
Stats._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
|
||||||
@@ -2858,7 +2858,7 @@ def test_stats():
|
|||||||
roughness, = node.process(line, operation="Rq", column="value")
|
roughness, = node.process(line, operation="Rq", column="value")
|
||||||
assert np.isclose(roughness, np.sqrt(np.mean((line - line.mean()) ** 2)))
|
assert np.isclose(roughness, np.sqrt(np.mean((line - line.mean()) ** 2)))
|
||||||
|
|
||||||
table = RecordTable([
|
table = DataTable([
|
||||||
{"name": "a", "value": 3.0, "unit": "m", "other": 10.0},
|
{"name": "a", "value": 3.0, "unit": "m", "other": 10.0},
|
||||||
{"name": "b", "value": 7.0, "unit": "m", "other": 20.0},
|
{"name": "b", "value": 7.0, "unit": "m", "other": 20.0},
|
||||||
])
|
])
|
||||||
@@ -2894,7 +2894,7 @@ def test_stats():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
node.process(
|
node.process(
|
||||||
MeasureTable([{"quantity": "min", "value": 1.0, "unit": "m"}]),
|
RecordTable([{"quantity": "min", "value": 1.0, "unit": "m"}]),
|
||||||
operation="max",
|
operation="max",
|
||||||
column="value",
|
column="value",
|
||||||
)
|
)
|
||||||
@@ -3026,7 +3026,7 @@ def test_view3d():
|
|||||||
def test_save_generic():
|
def test_save_generic():
|
||||||
print("=== Test: Save ===")
|
print("=== Test: Save ===")
|
||||||
from backend.nodes.save import Save
|
from backend.nodes.save import Save
|
||||||
from backend.data_types import DataField, ImageData, LineData, MeasureTable, MeshModel, RecordTable
|
from backend.data_types import DataField, ImageData, LineData, RecordTable, MeshModel, DataTable
|
||||||
import tifffile
|
import tifffile
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
@@ -3037,8 +3037,8 @@ def test_save_generic():
|
|||||||
"IMAGE",
|
"IMAGE",
|
||||||
"ANNOTATION_SOURCE",
|
"ANNOTATION_SOURCE",
|
||||||
"LINE",
|
"LINE",
|
||||||
"MEASURE_TABLE",
|
|
||||||
"RECORD_TABLE",
|
"RECORD_TABLE",
|
||||||
|
"DATA_TABLE",
|
||||||
"MESH_MODEL",
|
"MESH_MODEL",
|
||||||
"FLOAT",
|
"FLOAT",
|
||||||
]
|
]
|
||||||
@@ -3137,7 +3137,7 @@ def test_save_generic():
|
|||||||
assert np.array_equal(annotation_npz["image"], image)
|
assert np.array_equal(annotation_npz["image"], image)
|
||||||
|
|
||||||
# Save tables as CSV and JSON
|
# Save tables as CSV and JSON
|
||||||
measure_table = MeasureTable([
|
measure_table = RecordTable([
|
||||||
{"quantity": "Rq", "value": 1.23, "unit": "nm"},
|
{"quantity": "Rq", "value": 1.23, "unit": "nm"},
|
||||||
{"quantity": "Ra", "value": 0.98, "unit": "nm"},
|
{"quantity": "Ra", "value": 0.98, "unit": "nm"},
|
||||||
])
|
])
|
||||||
@@ -3148,7 +3148,7 @@ def test_save_generic():
|
|||||||
node.save(filename="measurements_json", directory_path=tmpdir, format="JSON", value=measure_table)
|
node.save(filename="measurements_json", directory_path=tmpdir, format="JSON", value=measure_table)
|
||||||
assert json.loads(Path(tmpdir, "measurements_json.json").read_text(encoding="utf-8")) == list(measure_table)
|
assert json.loads(Path(tmpdir, "measurements_json.json").read_text(encoding="utf-8")) == list(measure_table)
|
||||||
|
|
||||||
record_table = RecordTable([
|
record_table = DataTable([
|
||||||
{"label": "particle-1", "height": 12.0, "area": 44.0},
|
{"label": "particle-1", "height": 12.0, "area": 44.0},
|
||||||
{"label": "particle-2", "height": 8.0, "area": 21.0},
|
{"label": "particle-2", "height": 8.0, "area": 21.0},
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user