Skip to content

Commit dcabdf9

Browse files
authored
Added unifrom sampling option for sphere and torus (#28)
Added uniform sampling option for sphere and torus Added uniform sampling option for sphere and torus added uniform sampling option Added uniform option for sphere and torus Added uniform option for sphere and Torus added example usage with uniform option added uniformity test ran ruff format added scikit-learn dependency to pyproject.toml added example usage with uniform option added uniformity test ran ruff format corrected the test_shapes.py corrected the dependency ran ruff format and updated version number removed the genus g surface test
1 parent 914c9ec commit dcabdf9

File tree

5 files changed

+167
-19
lines changed

5 files changed

+167
-19
lines changed

docs/notebooks/Examples.ipynb

Lines changed: 79 additions & 3 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ keywords = [
4242
]
4343

4444
[project.optional-dependencies]
45-
testing = ["pytest", "pytest-cov", "scipy"]
45+
testing = ["pytest", "pytest-cov", "scikit-learn", "scipy"]
4646

4747
docs = ["sktda_docs_config"]
4848

tadasets/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.1"
1+
__version__ = "0.2.2"

tadasets/shapes.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def sphere(
6666
noise: Optional[float] = None,
6767
ambient: Optional[int] = None,
6868
seed: Optional[int] = None,
69+
uniform: bool = False,
6970
) -> np.ndarray:
7071
"""
7172
Sample ``n`` data points on a sphere.
@@ -82,6 +83,8 @@ def sphere(
8283
Embed the sphere into a space with ambient dimension equal to `ambient`. The sphere is randomly rotated in this high dimensional space.
8384
seed : int, optional
8485
Seed for random state.
86+
uniform : bool, default=False
87+
If True, sample points uniformly on the sphere. If False, sample points by choosing spherical coordinates uniformly.
8588
8689
Returns
8790
-------
@@ -90,15 +93,21 @@ def sphere(
9093
"""
9194

9295
rng = np.random.default_rng(seed)
93-
theta = rng.random(n) * 2.0 * np.pi
94-
phi = rng.random(n) * np.pi
95-
rad = np.ones((n,)) * r
9696

97-
data = np.zeros((n, 3))
97+
if uniform:
98+
data = rng.standard_normal((n, 3))
99+
data = r * data / np.sqrt(np.sum(data**2, 1)[:, None])
100+
101+
else:
102+
theta = rng.random(n) * 2.0 * np.pi
103+
phi = rng.random(n) * np.pi
104+
rad = np.ones((n,)) * r
105+
106+
data = np.zeros((n, 3))
98107

99-
data[:, 0] = rad * np.cos(theta) * np.cos(phi)
100-
data[:, 1] = rad * np.cos(theta) * np.sin(phi)
101-
data[:, 2] = rad * np.sin(theta)
108+
data[:, 0] = rad * np.cos(theta) * np.cos(phi)
109+
data[:, 1] = rad * np.cos(theta) * np.sin(phi)
110+
data[:, 2] = rad * np.sin(theta)
102111

103112
if noise:
104113
data += noise * rng.standard_normal(data.shape)
@@ -116,6 +125,7 @@ def torus(
116125
noise: Optional[float] = None,
117126
ambient: Optional[int] = None,
118127
seed: Optional[int] = None,
128+
uniform: bool = False,
119129
) -> np.ndarray:
120130
"""
121131
Sample ``n`` data points on a torus.
@@ -134,6 +144,8 @@ def torus(
134144
Embed the torus into a space with ambient dimension equal to `ambient`. The torus is randomly rotated in this high dimensional space.
135145
seed : int, optional
136146
Seed for random state.
147+
uniform : bool, default=False
148+
If True, sample points uniformly on the torus. If False, sample points by choosing angles uniformly.
137149
138150
Returns
139151
-------
@@ -144,13 +156,31 @@ def torus(
144156
assert a <= c, "That's not a torus"
145157

146158
rng = np.random.default_rng(seed)
147-
theta = rng.random(n) * 2.0 * np.pi
148-
phi = rng.random(n) * 2.0 * np.pi
149-
150-
data = np.zeros((n, 3))
151-
data[:, 0] = (c + a * np.cos(theta)) * np.cos(phi)
152-
data[:, 1] = (c + a * np.cos(theta)) * np.sin(phi)
153-
data[:, 2] = a * np.sin(theta)
159+
if uniform:
160+
samples = []
161+
while len(samples) < n:
162+
u = np.random.uniform(0, 2 * np.pi)
163+
v = np.random.uniform(0, 2 * np.pi)
164+
x = (c + a * np.cos(v)) * np.cos(u)
165+
y = (c + a * np.cos(v)) * np.sin(u)
166+
z = a * np.sin(v)
167+
168+
# REJECTION SAMPLING TO ENSURE UNIFORMITY
169+
# The map from u,v to x,y,z is not area-preserving, so we use rejection sampling to ensure uniformity
170+
jacobian = a * (c + a * np.cos(v))
171+
acceptance_prob = jacobian / (a * (c + a))
172+
if np.random.uniform(0, 1) < acceptance_prob:
173+
samples.append((x, y, z))
174+
data = np.array(samples)
175+
176+
else:
177+
theta = rng.random(n) * 2.0 * np.pi
178+
phi = rng.random(n) * 2.0 * np.pi
179+
180+
data = np.zeros((n, 3))
181+
data[:, 0] = (c + a * np.cos(theta)) * np.cos(phi)
182+
data[:, 1] = (c + a * np.cos(theta)) * np.sin(phi)
183+
data[:, 2] = a * np.sin(theta)
154184

155185
if noise:
156186
data += noise * rng.standard_normal(data.shape)

test/test_shapes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
import tadasets
44
from scipy.spatial.distance import pdist
5+
from sklearn.neighbors import NearestNeighbors
56

67

78
def norm(p):
@@ -53,6 +54,26 @@ def test_noise(self):
5354
rs = np.fromiter((norm(p) for p in s), np.float64)
5455
assert np.any(rs != 1.0)
5556

57+
def test_uniform(self):
58+
s = tadasets.sphere(n=6400, r=1, uniform=True)
59+
epsilon = 0.5
60+
61+
nbrs = NearestNeighbors(radius=epsilon).fit(s)
62+
63+
# Compute the number of points within the epsilon-ball around each point
64+
distances, indices = nbrs.radius_neighbors(s)
65+
num_points_in_ball = [len(ind) for ind in indices]
66+
67+
# Caculate the mean and standard deviation of the counts
68+
# For a uniform sampling on the sphere, this ditribution should be roughly normal
69+
# The expected mean is n/16 = 400
70+
# The standard deviation should be approximately np.sqrt(400) = 20
71+
mean_count = np.mean(num_points_in_ball)
72+
std_count = np.std(num_points_in_ball)
73+
74+
assert mean_count > 395 and mean_count < 405
75+
assert std_count < 45
76+
5677

5778
class TestDsphere:
5879
def test_d(self):
@@ -100,6 +121,27 @@ def test_ambient(self):
100121
s = tadasets.torus(n=200, c=3, ambient=15)
101122
assert s.shape == (200, 15)
102123

124+
def test_uniform(self):
125+
c, a = 3.183, 1
126+
s = tadasets.torus(n=16000, c=c, a=a, uniform=True)
127+
epsilon = 0.5
128+
129+
nbrs = NearestNeighbors(radius=epsilon).fit(s)
130+
131+
# Compute the number of points within the epsilon-ball around each point
132+
distances, indices = nbrs.radius_neighbors(s)
133+
num_points_in_ball = [len(ind) for ind in indices]
134+
135+
# Caculate the mean and standard deviation of the counts
136+
# For a uniform sampling on the sphere, this ditribution should be roughly normal
137+
# The expected mean is n/160 = 100
138+
# The standard deviation should be approximately np.sqrt(100) = 10
139+
mean_count = np.mean(num_points_in_ball)
140+
std_count = np.std(num_points_in_ball)
141+
142+
assert mean_count > 95 and mean_count < 105
143+
assert std_count < 25
144+
103145

104146
class TestSwissRoll:
105147
def test_n(self):

0 commit comments

Comments
 (0)