Skip to content

Commit c030c48

Browse files
feat: add Sobol distribution (#529)
1 parent 304b6fd commit c030c48

4 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎optiland/_types.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"hexapolar",
5555
"cross",
5656
"ring",
57+
"sobol",
5758
]
5859
ApertureType = Literal["EPD", "imageFNO", "objectNA", "float_by_stop_size"]
5960
Fields = Literal["all"] | Sequence[tuple[float, float]]

‎optiland/distribution.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import matplotlib.pyplot as plt
1414
import numpy as np
15+
from scipy.stats import qmc
1516

1617
import optiland.backend as be
1718

@@ -375,6 +376,42 @@ def generate_points(self, num_points: int):
375376
self.y = be.sin(theta)
376377

377378

379+
class SobolDistribution(BaseDistribution):
380+
"""A class representing a Sobol distribution.
381+
382+
Generates `num_points` points using a Sobol low-discrepancy sequence
383+
within the unit disk.
384+
385+
Attributes:
386+
seed (int | None): Seed for the Sobol sequence generator.
387+
x: The x-coordinates of the generated points.
388+
y: The y-coordinates of the generated points.
389+
"""
390+
391+
def __init__(self, seed: int | None = None):
392+
super().__init__()
393+
self.seed = seed
394+
395+
def generate_points(self, num_points: int):
396+
"""Generates Sobol points.
397+
398+
Args:
399+
num_points (int): The number of points to generate.
400+
401+
"""
402+
sampler = qmc.Sobol(d=2, scramble=True, seed=self.seed)
403+
sample = sampler.random(num_points)
404+
405+
u1 = be.array(sample[:, 0])
406+
u2 = be.array(sample[:, 1])
407+
408+
r = be.sqrt(u1)
409+
theta = 2 * be.pi * u2
410+
411+
self.x = r * be.cos(theta)
412+
self.y = r * be.sin(theta)
413+
414+
378415
def create_distribution(distribution_type: DistributionType) -> BaseDistribution:
379416
"""Create a distribution based on the given distribution type.
380417
@@ -400,6 +437,7 @@ def create_distribution(distribution_type: DistributionType) -> BaseDistribution
400437
"hexapolar": HexagonalDistribution,
401438
"cross": CrossDistribution,
402439
"ring": RingDistribution,
440+
"sobol": SobolDistribution,
403441
}
404442

405443
if distribution_type not in distribution_classes:

‎tests/test_distribution.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55
from matplotlib.axes import Axes
66
from matplotlib.figure import Figure
7+
from scipy.stats import qmc
78

89
import optiland.backend as be
910
from optiland import distribution
@@ -474,3 +475,23 @@ def test_gaussian_quad_weights(set_test_backend):
474475
weights / scale[k],
475476
be.array([0.04283, 0.09019, 0.11698, 0.11698, 0.09019, 0.04283]),
476477
)
478+
479+
480+
@pytest.mark.parametrize("num_points", [16, 64, 256, 1024])
481+
def test_sobol_distribution(set_test_backend, num_points):
482+
seed = 42
483+
d = distribution.SobolDistribution(seed=seed)
484+
d.generate_points(num_points=num_points)
485+
486+
sampler = qmc.Sobol(d=2, scramble=True, seed=seed)
487+
sample = sampler.random(num_points)
488+
u1 = be.array(sample[:, 0])
489+
u2 = be.array(sample[:, 1])
490+
r = be.sqrt(u1)
491+
theta = 2 * be.pi * u2
492+
x = r * be.cos(theta)
493+
y = r * be.sin(theta)
494+
495+
assert_allclose(d.x, x)
496+
assert_allclose(d.y, y)
497+
assert be.all(d.x**2 + d.y**2 <= 1.0 + 1e-7)

‎tests/test_distribution_points.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
LineYDistribution,
1313
RandomDistribution,
1414
RingDistribution,
15+
SobolDistribution,
1516
UniformDistribution,
1617
)
1718

@@ -219,3 +220,14 @@ def test_ring_distribution_zero_points(self, backend):
219220
)
220221
assert len(dist.x) == 0
221222
assert len(dist.y) == 0
223+
224+
def test_sobol_distribution(self, backend):
225+
be.set_backend(backend)
226+
num_points = 100
227+
dist = SobolDistribution(seed=42)
228+
dist.generate_points(num_points=num_points)
229+
# For Sobol, all points should be unique
230+
unique_count = count_unique_points(dist.x, dist.y)
231+
assert unique_count == num_points, (
232+
f"SobolDistribution expected {num_points} unique points, got {unique_count}"
233+
)

0 commit comments

Comments
 (0)