|
| 1 | +"""Tests for boundary data utilities.""" |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +import pytest |
| 5 | + |
| 6 | +from harmonic_measure.boundary_data import ( |
| 7 | + sigmoid, |
| 8 | + smooth_indicator, |
| 9 | + smooth_corner_indicator, |
| 10 | + corner_boundary_data, |
| 11 | +) |
| 12 | + |
| 13 | + |
| 14 | +class TestSigmoid: |
| 15 | + """Tests for sigmoid function.""" |
| 16 | + |
| 17 | + def test_sigmoid_at_zero(self): |
| 18 | + """σ(0) = 0.5""" |
| 19 | + assert sigmoid(0.0) == pytest.approx(0.5) |
| 20 | + |
| 21 | + def test_sigmoid_limits(self): |
| 22 | + """σ(-∞) → 0, σ(∞) → 1""" |
| 23 | + assert sigmoid(-10) < 0.001 |
| 24 | + assert sigmoid(10) > 0.999 |
| 25 | + |
| 26 | + def test_sigmoid_symmetry(self): |
| 27 | + """σ(t) + σ(-t) = 1""" |
| 28 | + for t in [0.5, 1.0, 2.0, 5.0]: |
| 29 | + assert sigmoid(t) + sigmoid(-t) == pytest.approx(1.0) |
| 30 | + |
| 31 | + def test_sigmoid_vectorized(self): |
| 32 | + """Should work on arrays.""" |
| 33 | + t = np.array([-5.0, 0.0, 5.0]) |
| 34 | + result = sigmoid(t) |
| 35 | + |
| 36 | + assert result.shape == (3,) |
| 37 | + assert result[0] < 0.01 |
| 38 | + assert result[1] == pytest.approx(0.5) |
| 39 | + assert result[2] > 0.99 |
| 40 | + |
| 41 | + |
| 42 | +class TestSmoothIndicator: |
| 43 | + """Tests for smooth indicator function.""" |
| 44 | + |
| 45 | + def test_inside_returns_high(self): |
| 46 | + """Inside=True should return value near 1.""" |
| 47 | + val = smooth_indicator(True, delta=0.1) |
| 48 | + assert val > 0.9 |
| 49 | + |
| 50 | + def test_outside_returns_low(self): |
| 51 | + """Inside=False should return value near 0.""" |
| 52 | + val = smooth_indicator(False, delta=0.1) |
| 53 | + assert val < 0.1 |
| 54 | + |
| 55 | + def test_delta_affects_sharpness(self): |
| 56 | + """Smaller delta should give values closer to 0/1.""" |
| 57 | + val_sharp = smooth_indicator(True, delta=0.01) |
| 58 | + val_soft = smooth_indicator(True, delta=1.0) |
| 59 | + |
| 60 | + # Sharp should be closer to 1 |
| 61 | + assert val_sharp > val_soft |
| 62 | + |
| 63 | + |
| 64 | +class TestSmoothCornerIndicator: |
| 65 | + """Tests for smooth corner indicator function.""" |
| 66 | + |
| 67 | + def test_output_shape(self): |
| 68 | + """Output should match number of input points.""" |
| 69 | + pts = np.random.randn(50, 2) |
| 70 | + corners = [(1.0, 0.0), (-1.0, 0.0)] |
| 71 | + |
| 72 | + g = smooth_corner_indicator(pts, corners, radius=0.5) |
| 73 | + |
| 74 | + assert g.shape == (50,) |
| 75 | + |
| 76 | + def test_values_in_range(self): |
| 77 | + """Values should be in [0, 1].""" |
| 78 | + pts = np.random.randn(100, 2) * 2 # Some near corners, some far |
| 79 | + corners = [(1.0, 1.0), (-1.0, -1.0)] |
| 80 | + |
| 81 | + g = smooth_corner_indicator(pts, corners, radius=0.5) |
| 82 | + |
| 83 | + assert np.all(g >= 0) |
| 84 | + assert np.all(g <= 1) |
| 85 | + |
| 86 | + def test_point_at_corner(self): |
| 87 | + """Point exactly at corner should have high value.""" |
| 88 | + corners = [(1.0, 0.0), (-1.0, 0.0)] |
| 89 | + pts = np.array([[1.0, 0.0]]) # Exactly at corner |
| 90 | + |
| 91 | + g = smooth_corner_indicator(pts, corners, radius=0.5) |
| 92 | + |
| 93 | + assert g[0] > 0.9 |
| 94 | + |
| 95 | + def test_point_far_from_corners(self): |
| 96 | + """Point far from corners should have low value.""" |
| 97 | + corners = [(1.0, 0.0), (-1.0, 0.0)] |
| 98 | + pts = np.array([[0.0, 10.0]]) # Far from any corner |
| 99 | + |
| 100 | + g = smooth_corner_indicator(pts, corners, radius=0.5) |
| 101 | + |
| 102 | + assert g[0] < 0.1 |
| 103 | + |
| 104 | + def test_custom_delta(self): |
| 105 | + """Custom delta should be used.""" |
| 106 | + corners = [(1.0, 0.0)] |
| 107 | + pts = np.array([[0.9, 0.0]]) # Just inside radius |
| 108 | + |
| 109 | + g_auto = smooth_corner_indicator(pts, corners, radius=0.5) |
| 110 | + g_custom = smooth_corner_indicator(pts, corners, radius=0.5, delta=0.01) |
| 111 | + |
| 112 | + # Both should give reasonable values |
| 113 | + assert g_auto > 0.5 |
| 114 | + assert g_custom > 0.5 |
| 115 | + |
| 116 | + |
| 117 | +class TestCornerBoundaryData: |
| 118 | + """Tests for standard corner boundary data.""" |
| 119 | + |
| 120 | + def test_output_shape(self): |
| 121 | + """Output should match number of points.""" |
| 122 | + pts = np.random.randn(64, 2) |
| 123 | + |
| 124 | + g = corner_boundary_data(pts, a=1.0, rho=0.5) |
| 125 | + |
| 126 | + assert g.shape == (64,) |
| 127 | + |
| 128 | + def test_values_in_range(self): |
| 129 | + """Values should be in [0, 1].""" |
| 130 | + pts = np.random.randn(100, 2) * 2 |
| 131 | + |
| 132 | + g = corner_boundary_data(pts, a=1.0, rho=0.5) |
| 133 | + |
| 134 | + assert np.all(g >= 0) |
| 135 | + assert np.all(g <= 1) |
| 136 | + |
| 137 | + def test_corners_at_expected_locations(self): |
| 138 | + """Corners should be at (±1, ±a).""" |
| 139 | + a = 2.0 |
| 140 | + corners = [ |
| 141 | + [1.0, a], [1.0, -a], [-1.0, a], [-1.0, -a] |
| 142 | + ] |
| 143 | + pts = np.array(corners) |
| 144 | + |
| 145 | + g = corner_boundary_data(pts, a=a, rho=0.5) |
| 146 | + |
| 147 | + # All corner points should have high indicator |
| 148 | + assert np.all(g > 0.9) |
| 149 | + |
| 150 | + def test_custom_delta(self): |
| 151 | + """Custom delta should work.""" |
| 152 | + pts = np.array([[1.0, 1.0]]) |
| 153 | + |
| 154 | + g = corner_boundary_data(pts, a=1.0, rho=0.5, delta=0.01) |
| 155 | + |
| 156 | + assert g[0] > 0.9 |
| 157 | + |
| 158 | + def test_edge_midpoints_low(self): |
| 159 | + """Points on edges (not corners) should have low indicator.""" |
| 160 | + # Mid-points of edges for a=1 |
| 161 | + edge_pts = np.array([ |
| 162 | + [0.0, 1.0], # Top edge midpoint |
| 163 | + [0.0, -1.0], # Bottom edge midpoint |
| 164 | + [1.0, 0.0], # Right edge midpoint |
| 165 | + [-1.0, 0.0], # Left edge midpoint |
| 166 | + ]) |
| 167 | + |
| 168 | + g = corner_boundary_data(edge_pts, a=1.0, rho=0.3) |
| 169 | + |
| 170 | + # Edge midpoints should be far from corners |
| 171 | + assert np.all(g < 0.1) |
0 commit comments