Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add union_strategy in from_cones and from_boxes #174

Merged
merged 3 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Add support of `regions.Regions` [#163]
* Creation of a MOC from a zone (defined by min/max ra and dec)`MOC.from_zone`
* Creation of a single MOC from a lot of small cones is faster with the new option in
`MOC.from_cones`: the keyword 'union_strategy' can now take the value 'small_cones'.
* Creation of a single MOC from a lot of cones/boxes is faster with the new option in
`MOC.from_cones`/`MOC.from_boxes`: the keyword 'union_strategy' can now take the value
'small_cones'/'small_boxes' or 'large_cones'/'large_boxes'.
Small cones/boxes is faster for non-overlapping cones/boxes.

## [0.16.2]

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ crate-type = ["cdylib"]

[dependencies]
# moc = { version = "0.15", features = ["storage"] }
moc = { git = 'https://github.com/cds-astro/cds-moc-rust', rev = '5032e6163fd88cb57ba535eca9bff744931cfbb2', features = ["storage"] }
moc = { git = 'https://github.com/cds-astro/cds-moc-rust', rev = '361eb278fe782bfc053433495c33e3f16e20cdbd', features = ["storage"] }
healpix = { package = "cdshealpix", version = "0.6" }
# healpix = { package = "cdshealpix", git = 'https://github.com/cds-astro/cds-healpix-rust', branch = 'master' }
rayon = "1.10"
Expand Down
104 changes: 89 additions & 15 deletions notebooks/01-Creating_MOCs_from_shapes.ipynb

Large diffs are not rendered by default.

98 changes: 75 additions & 23 deletions python/mocpy/moc/moc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,9 +1310,9 @@ def from_cones(
max_depth : int
Maximum HEALPix cell resolution.
union_strategy : str, optional
Return the union of all the cones instead of the list of MOCs. For now, the
"small_cones" strategy is implemented. It works better if the cones don't overlap
a lot.
Return the union of all the cones instead of the list of MOCs. Can be either
"small_cones" or "large_cones". The "small_cone" strategy will be faster for
non-overlapping cones and the "large_cones" for the other case.
delta_depth : int, optional
To control the approximation, you can choose to perform the computations at a deeper
depth using the `delta_depth` parameter.
Expand Down Expand Up @@ -1355,8 +1355,25 @@ def from_cones(
)
return cls(index)

if union_strategy == "large_cones":
if radius.isscalar:
radii = np.full(len(lon), Angle(radius).to_value(u.deg))
else:
radii = Angle(radius).to_value(u.deg)
index = mocpy.from_large_cones(
lon,
lat,
radii,
np.uint8(max_depth),
np.uint8(delta_depth),
n_threads,
)
return cls(index)

if union_strategy is not None:
raise ValueError("'union_strategy' can only be None or 'small_cones'.")
raise ValueError(
"'union_strategy' can only be None, 'large_cones', or 'small_cones'."
)

if radius.isscalar:
indices = mocpy.from_same_cones(
Expand Down Expand Up @@ -1474,7 +1491,9 @@ def from_box(cls, lon, lat, a, b, angle, max_depth):

@classmethod
@validate_lonlat
def from_boxes(cls, lon, lat, a, b, angle, max_depth, *, n_threads=None):
def from_boxes(
cls, lon, lat, a, b, angle, max_depth, *, n_threads=None, union_strategy=None
):
"""
Create a MOC from a box/rectangle.

Expand All @@ -1499,11 +1518,16 @@ def from_boxes(cls, lon, lat, a, b, angle, max_depth, *, n_threads=None):
n_threads : int, optional
The number of threads to be used. If this is set to None (default value),
all available threads will be used.
union_strategy : str, optional
Return the union of all the boxes instead of the list of MOCs. Can be either
"small_boxes" or "large_boxes". The "small_boxes" strategy will be faster for
non-overlapping boxes and the "large_boxes" for the other case.

Returns
-------
result : list[`~mocpy.moc.MOC`]
The resulting list of MOCs.
result : list[`~mocpy.MOC`] or `~mocpy.MOC`
The resulting list of MOCs. If 'union_strategy' is not None, returns the MOC
of the union of all boxes instead.

Examples
--------
Expand All @@ -1525,35 +1549,63 @@ def from_boxes(cls, lon, lat, a, b, angle, max_depth, *, n_threads=None):
... a=[10, 20]*u.deg,
... b=[5, 10]*u.deg,
... angle=[30, 10]*u.deg,
... max_depth=10
... max_depth=10,
... union_strategy="small_boxes"
... )
"""
params = [a, b, angle]
max_depth = np.uint8(max_depth)
if any(isinstance(param, u.Quantity) and param.isscalar for param in params):
if not all(isinstance(param, u.Quantity) for param in params):
raise ValueError(
"'a', 'b' and 'angle' should either be all astropy angle-equivalent"
"scalar values or they should all be iterable angle-equivalent. "
" scalar values or they should all be iterable angle-equivalent. "
"They cannot be a mix of both.",
)
indices = mocpy.from_same_boxes(
lon,
lat,
np.float64(a.to_value(u.deg)),
np.float64(b.to_value(u.deg)),
np.float64(angle.to_value(u.deg)),
np.uint8(max_depth),
n_threads=n_threads,
)
return [cls(index) for index in indices]
if union_strategy is None:
indices = mocpy.from_same_boxes(
lon,
lat,
np.float64(a.to_value(u.deg)),
np.float64(b.to_value(u.deg)),
np.float64(angle.to_value(u.deg)),
max_depth,
n_threads=n_threads,
)
return [cls(index) for index in indices]
# no exception for same boxes in the union case
a = np.full(len(lon), Angle(a).to_value(u.deg))
b = np.full(len(lon), Angle(b).to_value(u.deg))
angle = np.full(len(lon), Angle(angle).to_value(u.deg))
else:
a = Angle(a).to_value(u.deg)
b = Angle(b).to_value(u.deg)
angle = Angle(angle).to_value(u.deg)
# different boxes
if union_strategy == "small_boxes":
return cls(
mocpy.from_small_boxes(
lon, lat, a, b, angle, max_depth, n_threads=n_threads
)
)
if union_strategy == "large_boxes":
return cls(
mocpy.from_large_boxes(
lon, lat, a, b, angle, max_depth, n_threads=n_threads
)
)
if union_strategy is not None:
raise ValueError(
"'union_strategy' can only be None, 'large_boxes', or 'small_boxes'."
)

indices = mocpy.from_boxes(
lon,
lat,
np.array(Angle(a).to_value(u.deg), dtype=np.float64),
np.array(Angle(b).to_value(u.deg), dtype=np.float64),
np.array(Angle(angle).to_value(u.deg), dtype=np.float64),
np.uint8(max_depth),
a,
b,
angle,
max_depth,
n_threads=n_threads,
)
return [cls(index) for index in indices]
Expand Down
38 changes: 37 additions & 1 deletion python/mocpy/tests/test_moc.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,21 @@ def test_from_box():
)
assert len(list_boxes_same) == 2
assert list_boxes_same[0] == moc
# union strategies
union_boxes_same = MOC.from_boxes(
lon=[0, 0] * u.deg,
lat=[0, 0] * u.deg,
a=a,
b=b,
angle=30 * u.deg,
max_depth=10,
union_strategy="large_boxes",
)
assert union_boxes_same == MOC.from_box(
lon=0 * u.deg, lat=0 * u.deg, a=a, b=b, angle=30 * u.deg, max_depth=10
)

# not same boxes
a = [Angle("10d"), Angle("20d")]
b = [Angle("2d"), Angle("4d")]
list_boxes_different = MOC.from_boxes(
Expand All @@ -644,7 +659,6 @@ def test_from_box():
)
assert len(list_boxes_different) == 2
assert list_boxes_different[0] == moc

# mixed iterables and scalars raise an error
with pytest.raises(ValueError, match="'a', 'b' and 'angle' should*"):
MOC.from_boxes(
Expand All @@ -655,6 +669,20 @@ def test_from_box():
angle=30 * u.deg,
max_depth=10,
)
# union strategy possible choices
with pytest.raises(
ValueError,
match="'union_strategy' can only be None, 'large_boxes', or 'small_boxes'.",
):
MOC.from_boxes(
lon=[0, 0] * u.deg,
lat=[0, 0] * u.deg,
a=a,
b=b,
angle=[30, 30] * u.deg,
max_depth=10,
union_strategy="large_cones", # voluntary confusion between cones and boxes
)


def test_from_astropy_regions():
Expand Down Expand Up @@ -754,6 +782,14 @@ def test_from_cones():
union_strategy="small_cones",
)
assert isinstance(moc, MOC)
moc2 = MOC.from_cones(
lon,
lat,
radius=radius,
max_depth=14,
union_strategy="large_cones",
)
assert moc == moc2
# different radii
radii = [5, 6] * u.arcmin
cones = MOC.from_cones(lon, lat, radius=radii, max_depth=14)
Expand Down
Loading
Loading