low pri features
This commit is contained in:
45
tests/node_tests/affine_correction.py
Normal file
45
tests/node_tests/affine_correction.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_identity_transform():
|
||||
"""No shear, unit scale, zero rotation should return near-identical data."""
|
||||
from backend.nodes.affine_correction import AffineCorrection
|
||||
|
||||
node = AffineCorrection()
|
||||
field = make_field(shape=(32, 32))
|
||||
result, = node.process(field, shear_x=0.0, shear_y=0.0, scale_x=1.0, scale_y=1.0, angle=0.0)
|
||||
assert result.data.shape == (32, 32)
|
||||
assert np.allclose(result.data, field.data, atol=1e-10)
|
||||
|
||||
|
||||
def test_scale_changes_values():
|
||||
from backend.nodes.affine_correction import AffineCorrection
|
||||
|
||||
node = AffineCorrection()
|
||||
field = make_field(shape=(32, 32))
|
||||
result, = node.process(field, shear_x=0.0, shear_y=0.0, scale_x=2.0, scale_y=1.0, angle=0.0)
|
||||
assert result.data.shape == (32, 32)
|
||||
# Scaled field should differ from original
|
||||
assert not np.allclose(result.data, field.data)
|
||||
|
||||
|
||||
def test_rotation_preserves_shape():
|
||||
from backend.nodes.affine_correction import AffineCorrection
|
||||
|
||||
node = AffineCorrection()
|
||||
field = make_field(shape=(48, 64))
|
||||
result, = node.process(field, shear_x=0.0, shear_y=0.0, scale_x=1.0, scale_y=1.0, angle=10.0)
|
||||
assert result.data.shape == (48, 64)
|
||||
|
||||
|
||||
def test_shear_changes_values():
|
||||
from backend.nodes.affine_correction import AffineCorrection
|
||||
|
||||
node = AffineCorrection()
|
||||
# Use a non-symmetric field so shear has a visible effect
|
||||
data = np.outer(np.arange(32), np.ones(32))
|
||||
field = make_field(data=data)
|
||||
result, = node.process(field, shear_x=0.3, shear_y=0.0, scale_x=1.0, scale_y=1.0, angle=0.0)
|
||||
assert not np.allclose(result.data, field.data)
|
||||
42
tests/node_tests/deconvolution.py
Normal file
42
tests/node_tests/deconvolution.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_wiener_preserves_shape():
|
||||
from backend.nodes.deconvolution import Deconvolution
|
||||
|
||||
node = Deconvolution()
|
||||
field = make_field(shape=(32, 32))
|
||||
result, = node.process(field, "wiener", 2.0, 0.01, 10)
|
||||
assert result.data.shape == (32, 32)
|
||||
assert np.isfinite(result.data).all()
|
||||
|
||||
|
||||
def test_richardson_lucy_preserves_shape():
|
||||
from backend.nodes.deconvolution import Deconvolution
|
||||
|
||||
node = Deconvolution()
|
||||
field = make_field(data=np.abs(np.random.default_rng(42).standard_normal((32, 32))) + 0.1)
|
||||
result, = node.process(field, "richardson_lucy", 2.0, 0.01, 5)
|
||||
assert result.data.shape == (32, 32)
|
||||
assert np.isfinite(result.data).all()
|
||||
|
||||
|
||||
def test_wiener_flat_field():
|
||||
from backend.nodes.deconvolution import Deconvolution
|
||||
|
||||
node = Deconvolution()
|
||||
field = make_field(data=np.ones((32, 32)))
|
||||
result, = node.process(field, "wiener", 2.0, 0.01, 10)
|
||||
# A flat field convolved with anything is still flat; Wiener should preserve it
|
||||
assert result.data.shape == (32, 32)
|
||||
|
||||
|
||||
def test_unknown_method():
|
||||
from backend.nodes.deconvolution import Deconvolution
|
||||
|
||||
node = Deconvolution()
|
||||
field = make_field(shape=(32, 32))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(field, "unknown", 2.0, 0.01, 10)
|
||||
53
tests/node_tests/drift_correction.py
Normal file
53
tests/node_tests/drift_correction.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_drift_correction_flat():
|
||||
from backend.nodes.drift_correction import DriftCorrection
|
||||
|
||||
node = DriftCorrection()
|
||||
field = make_field(data=np.zeros((32, 32)))
|
||||
result, = node.process(field, "previous_row", "horizontal")
|
||||
assert result.data.shape == (32, 32)
|
||||
assert np.allclose(result.data, 0.0, atol=1e-10)
|
||||
|
||||
|
||||
def test_drift_correction_preserves_shape():
|
||||
from backend.nodes.drift_correction import DriftCorrection
|
||||
|
||||
node = DriftCorrection()
|
||||
field = make_field(shape=(48, 64))
|
||||
for ref in ("previous_row", "mean_row"):
|
||||
for direction in ("horizontal", "vertical"):
|
||||
result, = node.process(field, ref, direction)
|
||||
assert result.data.shape == (48, 64)
|
||||
|
||||
|
||||
def test_drift_correction_reduces_drift():
|
||||
"""A field with artificial row-by-row drift should have less variance after correction."""
|
||||
from backend.nodes.drift_correction import DriftCorrection
|
||||
|
||||
node = DriftCorrection()
|
||||
rng = np.random.default_rng(42)
|
||||
base = rng.standard_normal((32, 64))
|
||||
# Add artificial drift: shift each row by cumulative offset
|
||||
drifted = base.copy()
|
||||
for i in range(1, 32):
|
||||
drifted[i] = np.roll(base[i], i)
|
||||
|
||||
field = make_field(data=drifted)
|
||||
result, = node.process(field, "previous_row", "horizontal")
|
||||
# The corrected field should have lower inter-row variance
|
||||
row_means_before = np.var(np.diff(drifted, axis=0))
|
||||
row_means_after = np.var(np.diff(result.data, axis=0))
|
||||
assert row_means_after <= row_means_before
|
||||
|
||||
|
||||
def test_drift_correction_mean_row_reference():
|
||||
from backend.nodes.drift_correction import DriftCorrection
|
||||
|
||||
node = DriftCorrection()
|
||||
field = make_field(shape=(32, 32))
|
||||
result, = node.process(field, "mean_row", "horizontal")
|
||||
assert result.data.shape == (32, 32)
|
||||
42
tests/node_tests/facet_analysis.py
Normal file
42
tests/node_tests/facet_analysis.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_facet_analysis_basic():
|
||||
from backend.nodes.facet_analysis import FacetAnalysis
|
||||
|
||||
node = FacetAnalysis()
|
||||
field = make_field(shape=(64, 64))
|
||||
result, = node.process(field, 180, 3)
|
||||
assert result.data.ndim == 2
|
||||
assert result.si_unit_xy == "deg"
|
||||
|
||||
|
||||
def test_facet_analysis_flat_field():
|
||||
from backend.nodes.facet_analysis import FacetAnalysis
|
||||
|
||||
node = FacetAnalysis()
|
||||
field = make_field(data=np.zeros((32, 32)))
|
||||
result, = node.process(field, 180, 3)
|
||||
assert result.data.ndim == 2
|
||||
|
||||
|
||||
def test_facet_analysis_density_normalised():
|
||||
from backend.nodes.facet_analysis import FacetAnalysis
|
||||
|
||||
node = FacetAnalysis()
|
||||
field = make_field(shape=(64, 64))
|
||||
result, = node.process(field, 180, 3)
|
||||
# Should be a normalised probability density
|
||||
assert np.isclose(result.data.sum(), 1.0, atol=1e-10)
|
||||
|
||||
|
||||
def test_facet_analysis_bin_count():
|
||||
from backend.nodes.facet_analysis import FacetAnalysis
|
||||
|
||||
node = FacetAnalysis()
|
||||
field = make_field(shape=(64, 64))
|
||||
result, = node.process(field, 360, 3)
|
||||
# phi bins = n_bins, theta bins = n_bins // 4
|
||||
assert result.data.shape == (90, 360)
|
||||
50
tests/node_tests/feature_detection.py
Normal file
50
tests/node_tests/feature_detection.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_canny_edge_detection():
|
||||
from backend.nodes.feature_detection import FeatureDetection
|
||||
|
||||
node = FeatureDetection()
|
||||
# Create a field with a sharp edge
|
||||
data = np.zeros((64, 64))
|
||||
data[:, 32:] = 1.0
|
||||
field = make_field(data=data)
|
||||
result, records = node.process(field, "canny", 1.0)
|
||||
assert result.data.shape == (64, 64)
|
||||
# Should detect some edge pixels
|
||||
assert result.data.sum() > 0
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_harris_corner_detection():
|
||||
from backend.nodes.feature_detection import FeatureDetection
|
||||
|
||||
node = FeatureDetection()
|
||||
# Create a field with a sharp corner (L-shape)
|
||||
data = np.zeros((64, 64))
|
||||
data[16:48, 16:48] = 1.0
|
||||
field = make_field(data=data)
|
||||
result, records = node.process(field, "harris", 1.0)
|
||||
assert result.data.shape == (64, 64)
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_canny_flat_field():
|
||||
from backend.nodes.feature_detection import FeatureDetection
|
||||
|
||||
node = FeatureDetection()
|
||||
field = make_field(data=np.ones((32, 32)))
|
||||
result, records = node.process(field, "canny", 1.0)
|
||||
# Flat field should have no edges
|
||||
assert result.data.sum() == 0
|
||||
|
||||
|
||||
def test_unknown_method():
|
||||
from backend.nodes.feature_detection import FeatureDetection
|
||||
|
||||
node = FeatureDetection()
|
||||
field = make_field(shape=(32, 32))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(field, "unknown", 1.0)
|
||||
50
tests/node_tests/hough_transform.py
Normal file
50
tests/node_tests/hough_transform.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_hough_lines_basic():
|
||||
from backend.nodes.hough_transform import HoughTransform
|
||||
|
||||
node = HoughTransform()
|
||||
# Create a field with a horizontal line
|
||||
data = np.zeros((64, 64))
|
||||
data[32, :] = 1.0
|
||||
field = make_field(data=data)
|
||||
accum, records = node.process(field, "lines", 3, 1.0, 10, 30)
|
||||
assert accum.data.ndim == 2
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_hough_circles_basic():
|
||||
from backend.nodes.hough_transform import HoughTransform
|
||||
|
||||
node = HoughTransform()
|
||||
# Create a field with a circle
|
||||
data = np.zeros((64, 64))
|
||||
yy, xx = np.ogrid[:64, :64]
|
||||
r2 = (yy - 32)**2 + (xx - 32)**2
|
||||
data[(r2 > 144) & (r2 < 196)] = 1.0 # ring at radius ~13
|
||||
field = make_field(data=data)
|
||||
accum, records = node.process(field, "circles", 3, 1.0, 8, 20)
|
||||
assert accum.data.shape == (64, 64)
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_hough_preserves_output_types():
|
||||
from backend.nodes.hough_transform import HoughTransform
|
||||
|
||||
node = HoughTransform()
|
||||
field = make_field(shape=(32, 32))
|
||||
accum, records = node.process(field, "lines", 2, 1.0, 5, 15)
|
||||
assert hasattr(accum, 'data')
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_hough_unknown_mode():
|
||||
from backend.nodes.hough_transform import HoughTransform
|
||||
|
||||
node = HoughTransform()
|
||||
field = make_field(shape=(32, 32))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(field, "unknown", 1, 1.0, 5, 15)
|
||||
45
tests/node_tests/image_stitch.py
Normal file
45
tests/node_tests/image_stitch.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_stitch_right_no_overlap():
|
||||
from backend.nodes.image_stitch import ImageStitch
|
||||
|
||||
node = ImageStitch()
|
||||
a = make_field(data=np.ones((32, 32)))
|
||||
b = make_field(data=np.ones((32, 32)) * 2)
|
||||
result, = node.process(a, b, "right", "none")
|
||||
assert result.data.shape[0] == 32
|
||||
assert result.data.shape[1] >= 32
|
||||
|
||||
|
||||
def test_stitch_below():
|
||||
from backend.nodes.image_stitch import ImageStitch
|
||||
|
||||
node = ImageStitch()
|
||||
a = make_field(data=np.ones((32, 32)))
|
||||
b = make_field(data=np.ones((32, 32)) * 2)
|
||||
result, = node.process(a, b, "below", "none")
|
||||
assert result.data.shape[1] == 32
|
||||
assert result.data.shape[0] >= 32
|
||||
|
||||
|
||||
def test_stitch_auto_direction():
|
||||
from backend.nodes.image_stitch import ImageStitch
|
||||
|
||||
node = ImageStitch()
|
||||
a = make_field(data=np.random.default_rng(0).standard_normal((32, 32)))
|
||||
b = make_field(data=np.random.default_rng(1).standard_normal((32, 32)))
|
||||
result, = node.process(a, b, "auto", "linear")
|
||||
assert result.data.ndim == 2
|
||||
|
||||
|
||||
def test_stitch_unknown_direction():
|
||||
from backend.nodes.image_stitch import ImageStitch
|
||||
|
||||
node = ImageStitch()
|
||||
a = make_field(shape=(16, 16))
|
||||
b = make_field(shape=(16, 16))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(a, b, "unknown", "none")
|
||||
46
tests/node_tests/lattice_measurement.py
Normal file
46
tests/node_tests/lattice_measurement.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_lattice_acf_returns_outputs():
|
||||
from backend.nodes.lattice_measurement import LatticeMeasurement
|
||||
|
||||
node = LatticeMeasurement()
|
||||
field = make_field(shape=(64, 64))
|
||||
corr, records = node.process(field, "acf")
|
||||
assert corr.data.shape == (64, 64)
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_lattice_fft_returns_outputs():
|
||||
from backend.nodes.lattice_measurement import LatticeMeasurement
|
||||
|
||||
node = LatticeMeasurement()
|
||||
field = make_field(shape=(64, 64))
|
||||
corr, records = node.process(field, "fft")
|
||||
assert corr.data.shape == (64, 64)
|
||||
|
||||
|
||||
def test_lattice_detects_periodic_structure():
|
||||
"""A simple cosine grid should produce lattice measurements."""
|
||||
from backend.nodes.lattice_measurement import LatticeMeasurement
|
||||
|
||||
node = LatticeMeasurement()
|
||||
x = np.linspace(0, 4 * np.pi, 64, endpoint=False)
|
||||
X, Y = np.meshgrid(x, x)
|
||||
data = np.cos(X) + np.cos(Y)
|
||||
field = make_field(data=data)
|
||||
|
||||
corr, records = node.process(field, "acf")
|
||||
# Should detect at least one vector
|
||||
assert len(records) >= 3
|
||||
|
||||
|
||||
def test_lattice_unknown_method():
|
||||
from backend.nodes.lattice_measurement import LatticeMeasurement
|
||||
|
||||
node = LatticeMeasurement()
|
||||
field = make_field(shape=(32, 32))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(field, "unknown")
|
||||
47
tests/node_tests/mfm_analysis.py
Normal file
47
tests/node_tests/mfm_analysis.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_mfm_all_operations():
|
||||
from backend.nodes.mfm_analysis import MFMAnalysis
|
||||
|
||||
node = MFMAnalysis()
|
||||
field = make_field(shape=(32, 32))
|
||||
|
||||
for op in ("phase_to_force_gradient", "force_gradient_to_field",
|
||||
"charge_density", "magnetisation"):
|
||||
result, = node.process(field, op, 50e-9)
|
||||
assert result.data.shape == (32, 32)
|
||||
assert np.isfinite(result.data).all()
|
||||
|
||||
|
||||
def test_mfm_flat_field():
|
||||
from backend.nodes.mfm_analysis import MFMAnalysis
|
||||
|
||||
node = MFMAnalysis()
|
||||
field = make_field(data=np.zeros((32, 32)))
|
||||
result, = node.process(field, "phase_to_force_gradient", 50e-9)
|
||||
assert np.allclose(result.data, 0.0, atol=1e-10)
|
||||
|
||||
|
||||
def test_mfm_units():
|
||||
from backend.nodes.mfm_analysis import MFMAnalysis
|
||||
|
||||
node = MFMAnalysis()
|
||||
field = make_field(shape=(32, 32))
|
||||
|
||||
result, = node.process(field, "force_gradient_to_field", 50e-9)
|
||||
assert result.si_unit_z == "A/m"
|
||||
|
||||
result, = node.process(field, "charge_density", 50e-9)
|
||||
assert result.si_unit_z == "A/m²"
|
||||
|
||||
|
||||
def test_mfm_unknown_operation():
|
||||
from backend.nodes.mfm_analysis import MFMAnalysis
|
||||
|
||||
node = MFMAnalysis()
|
||||
field = make_field(shape=(32, 32))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(field, "unknown_op", 50e-9)
|
||||
@@ -8,8 +8,7 @@ def test_scar_removal():
|
||||
|
||||
node = ScarRemoval()
|
||||
info = get_node_info("ScarRemoval")
|
||||
assert info["category"] == "Filter"
|
||||
assert {entry["category"] for entry in info["menu_categories"]} == {"Filter", "Level & Correct"}
|
||||
assert info["category"] == "Level & Correct"
|
||||
|
||||
rows = 96
|
||||
cols = 128
|
||||
|
||||
45
tests/node_tests/shape_fitting.py
Normal file
45
tests/node_tests/shape_fitting.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.node_tests._shared import make_field
|
||||
|
||||
|
||||
def test_fit_sphere_residual():
|
||||
from backend.nodes.shape_fitting import ShapeFitting
|
||||
|
||||
node = ShapeFitting()
|
||||
# Create a spherical surface
|
||||
y, x = np.mgrid[:32, :32]
|
||||
data = 100.0 - np.sqrt(np.maximum(500**2 - (x - 16)**2 - (y - 16)**2, 0))
|
||||
field = make_field(data=data)
|
||||
result, records = node.process(field, "sphere", "residual")
|
||||
assert result.data.shape == (32, 32)
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_fit_paraboloid():
|
||||
from backend.nodes.shape_fitting import ShapeFitting
|
||||
|
||||
node = ShapeFitting()
|
||||
field = make_field(shape=(32, 32))
|
||||
result, records = node.process(field, "paraboloid", "fitted")
|
||||
assert result.data.shape == (32, 32)
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_fit_cylinder():
|
||||
from backend.nodes.shape_fitting import ShapeFitting
|
||||
|
||||
node = ShapeFitting()
|
||||
field = make_field(shape=(32, 32))
|
||||
result, records = node.process(field, "cylinder", "residual")
|
||||
assert result.data.shape == (32, 32)
|
||||
assert isinstance(records, list)
|
||||
|
||||
|
||||
def test_fit_unknown_shape():
|
||||
from backend.nodes.shape_fitting import ShapeFitting
|
||||
|
||||
node = ShapeFitting()
|
||||
field = make_field(shape=(32, 32))
|
||||
with pytest.raises(ValueError):
|
||||
node.process(field, "cone", "residual")
|
||||
57
tests/node_tests/synthetic_surface.py
Normal file
57
tests/node_tests/synthetic_surface.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def test_all_patterns_produce_correct_shape():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
for pattern in ("fbm", "white_noise", "lattice", "steps", "particles", "flat"):
|
||||
result, = node.process(
|
||||
pattern=pattern, xres=64, yres=48, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=42,
|
||||
)
|
||||
assert result.data.shape == (48, 64), f"Failed for {pattern}"
|
||||
|
||||
|
||||
def test_flat_is_zero():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
result, = node.process(
|
||||
pattern="flat", xres=32, yres=32, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=0,
|
||||
)
|
||||
assert np.allclose(result.data, 0.0)
|
||||
|
||||
|
||||
def test_seed_reproducibility():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
kwargs = dict(pattern="fbm", xres=32, yres=32, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=123)
|
||||
r1, = node.process(**kwargs)
|
||||
r2, = node.process(**kwargs)
|
||||
assert np.array_equal(r1.data, r2.data)
|
||||
|
||||
|
||||
def test_amplitude_scaling():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
result, = node.process(
|
||||
pattern="white_noise", xres=64, yres=64, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=5e-9, seed=42,
|
||||
)
|
||||
assert result.data.max() <= 5e-9 + 1e-15
|
||||
assert result.data.min() >= -1e-15
|
||||
|
||||
|
||||
def test_unknown_pattern():
|
||||
from backend.nodes.synthetic_surface import SyntheticSurface
|
||||
|
||||
node = SyntheticSurface()
|
||||
with pytest.raises(ValueError):
|
||||
node.process(pattern="unknown", xres=32, yres=32, xreal=1e-6, yreal=1e-6,
|
||||
amplitude=1e-9, seed=0)
|
||||
Reference in New Issue
Block a user