add remaining high value features

This commit is contained in:
2026-03-27 23:53:49 -07:00
parent 61d7b0fdcc
commit 240a2529eb
10 changed files with 1648 additions and 6 deletions

View File

@@ -526,6 +526,81 @@ def test_plane_level():
print(" PASS\n")
def test_facet_level():
print("=== Test: FacetLevelField ===")
from backend.node_registry import get_node_info
from backend.nodes.facet_level_field import FacetLevelField
from backend.nodes.plane_level_field import PlaneLevelField
def fit_pixel_plane(data: np.ndarray, region: np.ndarray) -> tuple[float, float, float]:
yy, xx = np.mgrid[0:data.shape[0], 0:data.shape[1]]
A = np.column_stack([
np.ones(int(np.count_nonzero(region)), dtype=np.float64),
xx[region].astype(np.float64),
yy[region].astype(np.float64),
])
coeffs, _, _, _ = np.linalg.lstsq(A, data[region].ravel().astype(np.float64), rcond=None)
return float(coeffs[0]), float(coeffs[1]), float(coeffs[2])
node = FacetLevelField()
plane_node = PlaneLevelField()
assert get_node_info("FacetLevelField")["category"] == "Flatten"
N = 96
yy, xx = np.mgrid[0:N, 0:N]
base = 0.055 * xx + 0.028 * yy
terraces = np.zeros((N, N), dtype=np.float64)
terraces[:, 54:] += 6.0
terraces[18:70, 68:88] += 3.5
field = make_field(data=base + terraces)
plane_leveled, = plane_node.process(field)
facet_leveled, = node.process(field, masking="ignore")
left_region = xx < 48
right_region = (xx > 60) & ~((yy >= 18) & (yy < 70) & (xx >= 68) & (xx < 88))
_, plane_left_bx, plane_left_by = fit_pixel_plane(plane_leveled.data, left_region)
_, plane_right_bx, plane_right_by = fit_pixel_plane(plane_leveled.data, right_region)
_, facet_left_bx, facet_left_by = fit_pixel_plane(facet_leveled.data, left_region)
_, facet_right_bx, facet_right_by = fit_pixel_plane(facet_leveled.data, right_region)
plane_slope = float(max(np.hypot(plane_left_bx, plane_left_by), np.hypot(plane_right_bx, plane_right_by)))
facet_slope = float(max(np.hypot(facet_left_bx, facet_left_by), np.hypot(facet_right_bx, facet_right_by)))
assert facet_slope < plane_slope * 1e-6
mask = np.zeros((N, N), dtype=np.uint8)
mask[24:72, 24:72] = 255
base_only = 0.035 * xx + 0.014 * yy
masked_facet = 5.0 - 0.065 * xx + 0.045 * yy
competing = np.where(mask > 0, masked_facet, base_only)
competing_field = make_field(data=competing)
excluded, = node.process(competing_field, masking="exclude", mask=mask)
included, = node.process(competing_field, masking="include", mask=mask)
outer_region = (mask == 0) & (xx > 4) & (xx < N - 4) & (yy > 4) & (yy < N - 4)
inner_region = (mask > 0) & (xx > 28) & (xx < 68) & (yy > 28) & (yy < 68)
_, excl_outer_bx, excl_outer_by = fit_pixel_plane(excluded.data, outer_region)
_, excl_inner_bx, excl_inner_by = fit_pixel_plane(excluded.data, inner_region)
_, incl_outer_bx, incl_outer_by = fit_pixel_plane(included.data, outer_region)
_, incl_inner_bx, incl_inner_by = fit_pixel_plane(included.data, inner_region)
excl_outer_slope = float(np.hypot(excl_outer_bx, excl_outer_by))
excl_inner_slope = float(np.hypot(excl_inner_bx, excl_inner_by))
incl_outer_slope = float(np.hypot(incl_outer_bx, incl_outer_by))
incl_inner_slope = float(np.hypot(incl_inner_bx, incl_inner_by))
assert excl_outer_slope < incl_outer_slope * 0.2
assert incl_inner_slope < excl_inner_slope * 0.2
bad_units = DataField(data=np.zeros((16, 16), dtype=np.float64), xreal=1e-6, yreal=1e-6, si_unit_xy="nm", si_unit_z="V")
try:
node.process(bad_units, masking="ignore")
except ValueError as exc:
assert "compatible XY and Z units" in str(exc)
else:
assert False, "Facet level should reject incompatible XY/Z units."
print(" PASS\n")
def test_poly_level():
print("=== Test: PolyLevelField ===")
from backend.nodes.poly_level_field import PolyLevelField
@@ -571,6 +646,78 @@ def test_fix_zero():
print(" PASS\n")
def test_curvature():
print("=== Test: Curvature ===")
from backend.node_registry import get_node_info
from backend.execution_context import active_node, execution_callbacks
from backend.nodes.curvature import Curvature
node = Curvature()
assert get_node_info("Curvature")["category"] == "Measure"
xres, yres = 121, 101
xreal, yreal = 8.0e-6, 6.0e-6
xoff, yoff = 1.0e-6, -0.5e-6
x = np.linspace(xoff, xoff + xreal, xres, dtype=np.float64)
y = np.linspace(yoff, yoff + yreal, yres, dtype=np.float64)
yy, xx = np.meshgrid(y, x, indexing="ij")
x0 = xoff + 0.45 * xreal
y0 = yoff + 0.60 * yreal
rx = 1.2e-6
ry = 2.4e-6
z0 = 3.0e-9
data = z0 + (xx - x0) ** 2 / (2.0 * rx) + (yy - y0) ** 2 / (2.0 * ry)
field = DataField(data=data, xreal=xreal, yreal=yreal, xoff=xoff, yoff=yoff, si_unit_xy="m", si_unit_z="m")
previews = []
tables = []
with execution_callbacks(preview=lambda nid, uri: previews.append(uri), table=lambda nid, rows: tables.append(rows)), active_node("test"):
output, table, profile1, profile2 = node.process(field, masking="ignore")
rows = {row["quantity"]: row for row in table}
recovered_radii = sorted([rows["Curvature radius 1"]["value"], rows["Curvature radius 2"]["value"]])
expected_radii = sorted([rx, ry])
assert len(previews) == 1
assert previews[0].startswith("data:image/png;base64,")
assert len(tables) == 1
assert abs(rows["Center x position"]["value"] - x0) < xreal * 0.02
assert abs(rows["Center y position"]["value"] - y0) < yreal * 0.02
assert abs(rows["Center value"]["value"] - z0) < 5e-11
assert np.allclose(recovered_radii, expected_radii, rtol=0.08, atol=5e-8)
assert output.overlays[-1]["kind"] == "markup"
assert len(output.overlays[-1]["shapes"]) == 3
assert isinstance(profile1, LineData)
assert isinstance(profile2, LineData)
assert profile1.x_unit == field.si_unit_xy
assert profile1.y_unit == field.si_unit_z
assert profile2.x_unit == field.si_unit_xy
assert profile2.y_unit == field.si_unit_z
assert len(profile1) > 10
assert len(profile2) > 10
mask = np.zeros((yres, xres), dtype=np.uint8)
mask[:, :xres // 2] = 255
left = 1.0e-9 + (xx - (xoff + 0.25 * xreal)) ** 2 / (2.0 * 0.9e-6) + (yy - (yoff + 0.5 * yreal)) ** 2 / (2.0 * 1.8e-6)
right = 2.0e-9 + (xx - (xoff + 0.75 * xreal)) ** 2 / (2.0 * 1.6e-6) + (yy - (yoff + 0.5 * yreal)) ** 2 / (2.0 * 3.2e-6)
split_field = DataField(data=np.where(mask > 0, left, right), xreal=xreal, yreal=yreal, xoff=xoff, yoff=yoff, si_unit_xy="m", si_unit_z="m")
_, include_table, _, _ = node.process(split_field, masking="include", mask=mask)
_, exclude_table, _, _ = node.process(split_field, masking="exclude", mask=mask)
include_radii = sorted([row["value"] for row in include_table if row["quantity"].startswith("Curvature radius")])
exclude_radii = sorted([row["value"] for row in exclude_table if row["quantity"].startswith("Curvature radius")])
assert np.allclose(include_radii, [0.9e-6, 1.8e-6], rtol=0.12, atol=5e-8)
assert np.allclose(exclude_radii, [1.6e-6, 3.2e-6], rtol=0.12, atol=5e-8)
bad_units = DataField(data=np.zeros((16, 16), dtype=np.float64), xreal=1e-6, yreal=1e-6, si_unit_xy="nm", si_unit_z="V")
try:
node.process(bad_units, masking="ignore")
except ValueError as exc:
assert "compatible XY and Z units" in str(exc)
else:
assert False, "Curvature should reject incompatible XY/Z units."
print(" PASS\n")
def test_line_correction():
print("=== Test: LineCorrection ===")
from backend.node_registry import get_node_info
@@ -866,6 +1013,80 @@ def test_height_histogram():
print(" PASS\n")
def test_fractal_dimension():
print("=== Test: FractalDimension ===")
from backend.node_registry import get_node_info
from backend.execution_context import active_node, execution_callbacks
from backend.nodes.fractal_dimension import FractalDimension
node = FractalDimension()
assert get_node_info("FractalDimension")["category"] == "Measure"
N = 129
yy, xx = np.mgrid[0:N, 0:N] / (N - 1)
data = 0.25 * xx + 0.12 * yy + 0.03 * np.sin(6.0 * np.pi * xx) + 0.02 * np.cos(4.0 * np.pi * yy)
field = make_field(data=data, xreal=4.0e-6, yreal=4.0e-6)
overlays = []
tables = []
with execution_callbacks(overlay=lambda nid, payload: overlays.append(payload), table=lambda nid, rows: tables.append(rows)), active_node("test"):
dimension, curve, table = node.process(
field,
method="partitioning",
interpolation="linear",
x1=0.0,
y1=0.5,
x2=1.0,
y2=0.5,
)
assert np.isfinite(dimension)
assert 1.5 < dimension < 2.5
assert isinstance(curve, LineData)
assert len(curve) > 3
assert curve.x_axis is not None
assert np.all(np.diff(curve.x_axis) > 0.0)
assert len(overlays) == 1
assert overlays[0]["kind"] == "line_plot"
assert len(tables) == 1
assert table[0]["quantity"] == "Dimension"
methods = ["partitioning", "cube_counting", "triangulation", "psdf", "hhcf"]
for method in methods:
dim, line, measurements = node.process(
field,
method=method,
interpolation="linear",
x1=0.0,
y1=0.5,
x2=1.0,
y2=0.5,
)
assert np.isfinite(dim), f"{method} should produce a finite fractal dimension"
if method == "psdf":
assert -1.0 < dim < 3.2
else:
assert 1.2 < dim < 3.2
assert isinstance(line, LineData)
assert len(line) >= 2
assert measurements[0]["quantity"] == "Dimension"
narrowed_dim, _, narrowed_table = node.process(
field,
method="partitioning",
interpolation="linear",
x1=0.15,
y1=0.5,
x2=0.55,
y2=0.5,
)
assert np.isfinite(narrowed_dim)
fit_from = next(row["value"] for row in narrowed_table if row["quantity"] == "Fit from")
fit_to = next(row["value"] for row in narrowed_table if row["quantity"] == "Fit to")
assert fit_to > fit_from
print(" PASS\n")
def test_cross_section():
print("=== Test: CrossSection ===")
from backend.nodes.cross_section import CrossSection
@@ -1167,6 +1388,93 @@ def test_particle_analysis():
print(" PASS\n")
def test_grain_distance_transform():
print("=== Test: GrainDistanceTransform ===")
from backend.nodes.grain_distance_transform import GrainDistanceTransform
node = GrainDistanceTransform()
field = make_field(data=np.zeros((7, 7), dtype=np.float64), xreal=7.0, yreal=7.0)
mask = np.zeros((7, 7), dtype=np.uint8)
mask[2:5, 2:5] = 255
interior, = node.process(field, mask, distance_type="euclidean", output_type="interior", from_border=True)
assert interior.data.shape == field.data.shape
assert interior.si_unit_z == field.si_unit_xy
assert np.isclose(interior.data[3, 3], 2.0)
assert np.isclose(interior.data[2, 2], 1.0)
assert np.isclose(interior.data[0, 0], 0.0)
exterior, = node.process(field, mask, distance_type="cityblock", output_type="exterior", from_border=True)
assert np.isclose(exterior.data[1, 1], 2.0)
assert np.isclose(exterior.data[2, 1], 1.0)
assert np.isclose(exterior.data[3, 3], 0.0)
signed, = node.process(field, mask, distance_type="chess", output_type="signed", from_border=True)
assert signed.data[3, 3] > 0.0
assert signed.data[0, 0] < 0.0
edge_field = make_field(data=np.zeros((5, 5), dtype=np.float64), xreal=5.0, yreal=5.0)
edge_mask = np.zeros((5, 5), dtype=np.uint8)
edge_mask[:, :2] = 255
from_edge, = node.process(edge_field, edge_mask, distance_type="euclidean", output_type="interior", from_border=True)
not_from_edge, = node.process(edge_field, edge_mask, distance_type="euclidean", output_type="interior", from_border=False)
assert not_from_edge.data[2, 0] > from_edge.data[2, 0]
print(" PASS\n")
def test_watershed_segmentation():
print("=== Test: WatershedSegmentation ===")
from scipy.ndimage import label
from backend.execution_context import active_node, execution_callbacks
from backend.nodes.watershed_segmentation import WatershedSegmentation
node = WatershedSegmentation()
y, x = np.mgrid[-1:1:64j, -1:1:64j]
data = (
2.0 * np.exp(-((x + 0.45) ** 2 + y**2) / 0.05)
+ 2.0 * np.exp(-((x - 0.45) ** 2 + y**2) / 0.05)
- 0.3 * np.exp(-(x**2 + y**2) / 0.12)
)
field = make_field(data=data, xreal=2.0e-6, yreal=2.0e-6)
previews = []
with execution_callbacks(preview=lambda nid, uri: previews.append(uri)), active_node("test"):
mask, = node.process(
field,
invert_height=False,
locate_steps=10,
locate_threshold=8,
locate_drop_size=0.1,
watershed_steps=20,
watershed_drop_size=0.1,
combine_mode="replace",
)
assert mask.dtype == np.uint8
assert mask.shape == field.data.shape
assert len(previews) == 1
assert previews[0].startswith("data:image/png;base64,")
_, ngrains = label(mask > 127)
assert ngrains >= 2
seed_mask = np.zeros_like(mask)
seed_mask[:, :32] = 255
intersected, = node.process(
field,
invert_height=False,
locate_steps=10,
locate_threshold=8,
locate_drop_size=0.1,
watershed_steps=20,
watershed_drop_size=0.1,
combine_mode="intersection",
mask=seed_mask,
)
assert np.count_nonzero(intersected) < np.count_nonzero(mask)
assert np.all(intersected[:, 40:] == 0)
print(" PASS\n")
# =========================================================================
# I/O
# =========================================================================
@@ -2814,6 +3122,7 @@ if __name__ == "__main__":
test_plane_level()
test_poly_level()
test_fix_zero()
test_curvature()
test_line_correction()
test_scar_removal()
test_angle_measure()
@@ -2821,6 +3130,7 @@ if __name__ == "__main__":
# Analysis
test_statistics()
test_height_histogram()
test_fractal_dimension()
test_cross_section()
test_line_cursors()
test_fft2d()