diff --git a/backend/nodes/radial_profile.py b/backend/nodes/radial_profile.py index 527cd25..a765346 100644 --- a/backend/nodes/radial_profile.py +++ b/backend/nodes/radial_profile.py @@ -33,10 +33,13 @@ class RadialProfile: FUNCTION = "process" DESCRIPTION = ( - "Compute the azimuthally averaged radial profile from a centre point. " + "Compute an azimuthally averaged profile around a centre point. " + "At each radius, every pixel in the full 360° ring is averaged together, " + "so the profile is direction-independent — there is no clockwise/counter-clockwise " + "traversal and no start/end point along the ring. " "Drag the centre marker on the preview to reposition the profile, " - "drag either end marker to change the radius. " - "Output x-axis is radius in physical xy units. " + "or drag either end marker (both just set the outer radius) to change the extent. " + "Output x-axis is radius in physical xy units." ) KEYWORDS = ("azimuthal average", "ring average", "circular", "isotropic") diff --git a/docs/nodes/Radial Profile.md b/docs/nodes/Radial Profile.md index 8af77da..a592948 100644 --- a/docs/nodes/Radial Profile.md +++ b/docs/nodes/Radial Profile.md @@ -1,6 +1,6 @@ # Radial Profile -Compute the azimuthally averaged radial profile from a centre point. The output x-axis is radius in physical xy units. Equivalent to gwy_data_field_angular_average used by Gwyddion's Radial Profile tool. +Compute an **azimuthally averaged** profile around a centre point on a DATA_FIELD. At each radius, every pixel in the full 360° ring around the centre is averaged together, so the profile is direction-independent — there is no clockwise/counter-clockwise traversal and no start or end point along the ring. The output is a single 1-D profile: value vs. radius. ## Inputs @@ -18,11 +18,13 @@ Compute the azimuthally averaged radial profile from a centre point. The output | Name | Type | Default | Description | |------|------|---------|-------------| -| cx | FLOAT | 0.5 | Centre x position as a fraction of field width (0 = left, 1 = right) | -| cy | FLOAT | 0.5 | Centre y position as a fraction of field height (0 = top, 1 = bottom) | | n_bins | INT | 128 | Number of radial bins (4-4096) | +## Interactive preview + +The dashed circle around the centre shows the outer radius used by the profile. Pixels beyond it are not included in the averaging. + ## Notes -- Pixels are assigned to radial bins by Euclidean distance; bins near the centre contain fewer pixels and may be noisier. +- Pixels are assigned to radial bins by Euclidean distance from the centre; inner bins contain fewer pixels and may be noisier. - Physical x-axis units come from the field's si_unit_xy; uncalibrated fields produce pixel-unit radii. diff --git a/tests/node_tests/radial_profile.py b/tests/node_tests/radial_profile.py index 0ab151f..fb0e80a 100644 --- a/tests/node_tests/radial_profile.py +++ b/tests/node_tests/radial_profile.py @@ -11,7 +11,7 @@ def test_radial_profile_constant_field(): node = RadialProfile() field = make_field(data=np.full((64, 64), 2.5)) - result, = node.process(field, cx=0.5, cy=0.5, n_bins=32) + result, = node.process(field, n_bins=32, cx=0.5, cy=0.5, ex=1.0, ey=0.5) assert isinstance(result, LineData) assert len(result.data) == 32 @@ -26,7 +26,7 @@ def test_radial_profile_units(): node = RadialProfile() field = make_field() - result, = node.process(field, cx=0.5, cy=0.5, n_bins=32) + result, = node.process(field, n_bins=32, cx=0.5, cy=0.5, ex=1.0, ey=0.5) assert result.x_unit == field.si_unit_xy assert result.y_unit == field.si_unit_z @@ -38,7 +38,7 @@ def test_radial_profile_x_axis_monotone(): node = RadialProfile() field = make_field() - result, = node.process(field, cx=0.5, cy=0.5, n_bins=64) + result, = node.process(field, n_bins=64, cx=0.5, cy=0.5, ex=1.0, ey=0.5) assert result.x_axis[0] >= 0.0 assert np.all(np.diff(result.x_axis) > 0) @@ -50,7 +50,7 @@ def test_radial_profile_off_centre(): node = RadialProfile() field = make_field(data=np.ones((64, 64))) - result, = node.process(field, cx=0.0, cy=0.0, n_bins=32) + result, = node.process(field, n_bins=32, cx=0.0, cy=0.0, ex=1.0, ey=1.0) assert len(result.data) == 32 finite = result.data[np.isfinite(result.data)] assert np.allclose(finite, 1.0, atol=1e-10) @@ -67,7 +67,7 @@ def test_radial_profile_radial_symmetry(): data = np.cos(r * np.pi / (xres / 2.0)) field = make_field(data=data) - result, = node.process(field, cx=0.5, cy=0.5, n_bins=32) + result, = node.process(field, n_bins=32, cx=0.5, cy=0.5, ex=1.0, ey=0.5) finite = result.data[np.isfinite(result.data)] # The profile should vary (not constant) assert np.std(finite) > 0.01 @@ -80,6 +80,25 @@ def test_radial_profile_n_bins(): field = make_field() for n in (16, 64, 256): - result, = node.process(field, cx=0.5, cy=0.5, n_bins=n) + result, = node.process(field, n_bins=n, cx=0.5, cy=0.5, ex=1.0, ey=0.5) assert len(result.data) == n assert len(result.x_axis) == n + + +def test_radial_profile_radius_controlled_by_endpoint(): + """The outer radius is set by the distance from (cx,cy) to (ex,ey).""" + from backend.nodes.radial_profile import RadialProfile + + node = RadialProfile() + field = make_field() + + # End at (1.0, 0.5): radius = 0.5 * xreal + short, = node.process(field, n_bins=32, cx=0.5, cy=0.5, ex=1.0, ey=0.5) + expected_r_short = 0.5 * field.xreal + assert np.isclose(short.x_axis[-1], expected_r_short, rtol=0.05) + + # End at corner: radius = sqrt(xreal^2 + yreal^2) * 0.5 (half-diagonal) + diag, = node.process(field, n_bins=32, cx=0.5, cy=0.5, ex=1.0, ey=1.0) + expected_r_diag = 0.5 * np.hypot(field.xreal, field.yreal) + assert np.isclose(diag.x_axis[-1], expected_r_diag, rtol=0.05) + assert diag.x_axis[-1] > short.x_axis[-1]