diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1874457f..c3e496ab 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -92,8 +92,8 @@ jobs: echo "Loop on PYBIN: $PYBIN" "${PYBIN}/pip" install maturin "${PYBIN}/maturin" -V - "${PYBIN}/maturin" publish -i "${PYBIN}/python" --no-sdist --skip-existing --compatibility manylinux2014 --username "$MATURIN_USERNAME" --target aarch64-unknown-linux-gnu --config "net.git-fetch-with-cli = true" - "${PYBIN}/maturin" publish -i "${PYBIN}/python" --no-sdist --skip-existing --compatibility musllinux_1_2 --username "$MATURIN_USERNAME" --target aarch64-unknown-linux-gnu --config "net.git-fetch-with-cli = true" + "${PYBIN}/maturin" publish -i "${PYBIN}/python" --skip-existing --compatibility manylinux2014 --username "$MATURIN_USERNAME" --target aarch64-unknown-linux-gnu --config "net.git-fetch-with-cli = true" + "${PYBIN}/maturin" publish -i "${PYBIN}/python" --skip-existing --compatibility musllinux_1_2 --username "$MATURIN_USERNAME" --target aarch64-unknown-linux-gnu --config "net.git-fetch-with-cli = true" done' build-windows-wheels: diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f06878..9e08abda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.12.2] + +### Added + +* `MOC.MAX_ORDER` and `TimeMOC.MAX_ORDER` to replace former `IntervalSet.HPX_MAX_ORDER` and `IntervalSet.TIME_MAX_ORDER` +* `MOC.to_depth29_ranges` (+test) to replace former `IntervalSet.nested` +* `TimeMOC.to_depth61_ranges` +* tests on `MOC.uniq_hpx` + +### Bugfix + +* :bug: return statement was missing in `MOC.uniq_hpx` + + ## [0.12.1] ### Added @@ -53,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * ⚠️ BREAKING: `times_to_microseconds` and `microseconds_to_times` moved from `utils` to `tmoc`. * ⚠️ BREAKING: `uniq` removed from `IntervalSet`, but replacing method `uniq_hpx` added to `MOC` + + ⚠️ BREAKING: the output of `uniq_hpx` is not sorted (but follow the order of the cells in the internal range list) * ⚠️ BREAKING: `STMOC.query_by_time` now takes in input a `TimeMOC` * ⚠️ BREAKING: `STMOC.query_by_space` now returns a `MOC` * ⚠️ BREAKING: `TimeMOC.contains` does not take any longer a time resolution as input parameter diff --git a/Cargo.toml b/Cargo.toml index 4837ac0a..9ce6b693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "MOCPy" -version = "0.12.1" +version = "0.12.2" authors = [ "Matthieu Baumann ", "Thomas Boch ", diff --git a/codemeta.json b/codemeta.json index 1e18aa81..636a0d5c 100644 --- a/codemeta.json +++ b/codemeta.json @@ -9,8 +9,8 @@ "dateModified": "2022-02-04", "issueTracker": "https://github.com/cds-astro/mocpy/issues", "name": "MOCpy", - "version": "0.12.1", - "softwareVersion": "0.12.1", + "version": "0.12.2", + "softwareVersion": "0.12.2", "description": "Python library to easily create and manipulate MOCs (Multi-Order Coverage maps)", "applicationCategory": ["Astronomy", "Science"], "funding": "ESCAPE 824064, ASTERICS 653477", diff --git a/python/mocpy/moc/moc.py b/python/mocpy/moc/moc.py index 271c562f..defb4058 100644 --- a/python/mocpy/moc/moc.py +++ b/python/mocpy/moc/moc.py @@ -70,6 +70,10 @@ class MOC(AbstractMOC): 5. Serialize `~mocpy.moc.MOC` objects to `astropy.io.fits.HDUList` or JSON dictionary and save it to a file. """ + # Maximum order (or depth) of a MOC + # (do not remove since it may be used externally). + MAX_ORDER = np.uint8(29) + __create_key = object() def __init__(self, create_key, store_index): @@ -1053,13 +1057,15 @@ def from_healpix_cells(cls, ipix, depth, max_depth): @classmethod def from_depth29_ranges(cls, max_depth, ranges): """ - Create a MOC from a set of HEALPix ranges at order 29. + Create a MOC from a set of ranges of HEALPix Nested indices at order 29. + For each range, the lower bound is inclusive and the upper bound is exclusive. + The range `[0, 12*4^29[` represents the full-sky. Parameters ---------- max_depth : int, The resolution of the MOC ranges : `~numpy.ndarray`, optional - a N x 2 numpy array representing the set of depth 29 ranges. + a N x 2 numpy array representing the set of depth 29 HEALPix nested ranges. defaults to `np.zeros((0, 2), dtype=np.uint64)` Returns @@ -1432,8 +1438,21 @@ def from_string(cls, value, format="ascii"): # noqa: A002 @property def uniq_hpx(self): - """Return a `np.array` of the uniq HEALPIx indices of the cell in the MOC.""" - mocpy.to_uniq_hpx(self._store_index) + """ + Return a `np.array` of the uniq HEALPIx indices of the cell in the MOC. + + Warning + ------- + The output is not sorted, the order follow the order of HEALPix cells in + the underlying sorted array of depth29 nested ranges, i.e. the natural order + of the cells is the underlying z-order curve. + """ + return mocpy.to_uniq_hpx(self._store_index) + + @property + def to_depth29_ranges(self): + """Return the list of order 29 HEALPix nested ranges this MOC contains.""" + return mocpy.to_ranges(self._store_index) def to_rgba(self, y_size=300): """ diff --git a/python/mocpy/tests/test_moc.py b/python/mocpy/tests/test_moc.py index 27e31ba7..6d9f0836 100644 --- a/python/mocpy/tests/test_moc.py +++ b/python/mocpy/tests/test_moc.py @@ -51,33 +51,71 @@ def isets(): dtype=np.uint64, ), ) - return dict(a=a, b=b) + return {"a": a, "b": b} def test_interval_set_consistency(isets): assert isets["a"] == MOC.from_depth29_ranges( - 29, np.array([[27, 126]], dtype=np.uint64) + 29, + np.array([[27, 126]], dtype=np.uint64), ) assert isets["b"] == MOC.from_depth29_ranges( - 29, np.array([[9, 61], [68, 105]], dtype=np.uint64) + 29, + np.array([[9, 61], [68, 105]], dtype=np.uint64), ) +def test_uniq_hpx(): + moc = MOC.from_depth29_ranges(29, np.array([[0, 1]], dtype=np.uint64)) + uniq_hpx = np.array([4 * 4**29], dtype=np.uint64) + assert moc.uniq_hpx == uniq_hpx + + moc = MOC.from_depth29_ranges(29, np.array([[7, 76]], dtype=np.uint64)) + uniq_hpx = np.array( + [ + 1 + 4 * 4**27, + 2 + 4 * 4**27, + 3 + 4 * 4**27, + 2 + 4 * 4**28, + 3 + 4 * 4**28, + 16 + 4 * 4**28, + 17 + 4 * 4**28, + 18 + 4 * 4**28, + 7 + 4 * 4**29, + ], + dtype=np.uint64, + ) + assert (np.sort(moc.uniq_hpx) == uniq_hpx).all() + + +def test_to_depth29_ranges(isets): + l = isets["a"].to_depth29_ranges + r = np.array([[27, 126]], dtype=np.uint64) + assert np.array_equal(l, r) + l = isets["b"].to_depth29_ranges + r = np.array([[9, 61], [68, 105]], dtype=np.uint64) + assert np.array_equal(l, r) + + def test_interval_set_union(isets): assert isets["a"].union(isets["b"]) == MOC.from_depth29_ranges( - 29, np.array([[9, 126]], dtype=np.uint64) + 29, + np.array([[9, 126]], dtype=np.uint64), ) assert isets["a"].union(MOC.new_empty(29)) == MOC.from_depth29_ranges( - 29, np.array([[27, 126]], dtype=np.uint64) + 29, + np.array([[27, 126]], dtype=np.uint64), ) assert MOC.new_empty(29).union(isets["a"]) == MOC.from_depth29_ranges( - 29, np.array([[27, 126]], dtype=np.uint64) + 29, + np.array([[27, 126]], dtype=np.uint64), ) def test_interval_set_intersection(isets): assert isets["a"].intersection(isets["b"]) == MOC.from_depth29_ranges( - 29, np.array([[27, 61], [68, 105]], dtype=np.uint64) + 29, + np.array([[27, 61], [68, 105]], dtype=np.uint64), ) assert isets["a"].intersection(MOC.new_empty(29)) == MOC.new_empty(29) assert MOC.new_empty(29).intersection(isets["a"]) == MOC.new_empty(29) @@ -85,10 +123,12 @@ def test_interval_set_intersection(isets): def test_interval_set_difference(isets): assert isets["a"].difference(isets["b"]) == MOC.from_depth29_ranges( - 29, np.array([[61, 68], [105, 126]], dtype=np.uint64) + 29, + np.array([[61, 68], [105, 126]], dtype=np.uint64), ) assert isets["b"].difference(isets["a"]) == MOC.from_depth29_ranges( - 29, np.array([[9, 27]], dtype=np.uint64) + 29, + np.array([[9, 27]], dtype=np.uint64), ) assert MOC.new_empty(29).difference(isets["a"]) == MOC.new_empty(29) assert isets["a"].difference(MOC.new_empty(29)) == isets["a"] @@ -125,17 +165,21 @@ def test_interval_min_depth(): def test_complement(): assert MOC.from_depth29_ranges( - max_depth=29, ranges=None + max_depth=29, + ranges=None, ).complement() == MOC.from_depth29_ranges( - max_depth=29, ranges=np.array([[0, 12 * 4**29]], dtype=np.uint64) + max_depth=29, + ranges=np.array([[0, 12 * 4**29]], dtype=np.uint64), ) assert MOC.new_empty( - max_depth=29 + max_depth=29, ).complement().complement() == MOC.from_depth29_ranges(max_depth=29, ranges=None) assert MOC.from_depth29_ranges( - 29, np.array([[1, 2], [6, 8], [5, 6]], dtype=np.uint64) + 29, + np.array([[1, 2], [6, 8], [5, 6]], dtype=np.uint64), ).complement() == MOC.from_depth29_ranges( - 29, np.array([[0, 1], [2, 5], [8, 12 * 4**29]], dtype=np.uint64) + 29, + np.array([[0, 1], [2, 5], [8, 12 * 4**29]], dtype=np.uint64), ) @@ -193,7 +237,7 @@ def test_moc_from_lonlat(lonlat_gen_f, size): def test_from_healpix_cells(): ipix = np.array([40, 87, 65]) depth = np.array([3, 3, 3]) - fully_covered = np.array([True, True, True]) + np.array([True, True, True]) MOC.from_healpix_cells(max_depth=29, ipix=ipix, depth=depth) @@ -213,7 +257,9 @@ def test_moc_consistent_with_aladin(): table = parse_single_table("resources/I_125A_catalog.vot").to_table() moc = MOC.from_lonlat( - table["_RAJ2000"].T * u.deg, table["_DEJ2000"].T * u.deg, max_norder=8 + table["_RAJ2000"].T * u.deg, + table["_DEJ2000"].T * u.deg, + max_norder=8, ) assert moc == truth @@ -264,7 +310,7 @@ def test_moc_serialize_and_from_json(moc_from_json): "6": [4500], "7": [], "8": [45], - } + }, ), "5/8-10 42-46 54\n\r 6/4500 8/45", ), @@ -290,7 +336,7 @@ def test_from_str(expected, moc_str): "6": [4500], "7": [], "8": [45], - } + }, ), "5/8-10 42-46 54\n\r 6/4500 8/45", ), @@ -342,7 +388,7 @@ def test_moc_serialize_to_json(moc_from_fits_image): "6": [4500], "7": [], "8": [45], - } + }, ), "5/8-10 42-46 54 6/4500 8/45 ", ), @@ -435,7 +481,9 @@ def test_moc_contains(order): should_be_outside_arr = moc.contains_lonlat(lon=lon, lat=lat, keep_inside=False) assert not should_be_outside_arr.any() should_be_inside_arr = moc_complement.contains_lonlat( - lon=lon, lat=lat, keep_inside=False + lon=lon, + lat=lat, + keep_inside=False, ) assert should_be_inside_arr.all() @@ -503,12 +551,12 @@ def mocs(): moc2 = {"1": [30]} moc2_increased = {"0": [7], "1": [8, 9, 25, 43, 41]} - return dict( - moc1=MOC.from_json(moc1), - moc1_increased=MOC.from_json(moc1_increased), - moc2=MOC.from_json(moc2), - moc2_increased=MOC.from_json(moc2_increased), - ) + return { + "moc1": MOC.from_json(moc1), + "moc1_increased": MOC.from_json(moc1_increased), + "moc2": MOC.from_json(moc2), + "moc2_increased": MOC.from_json(moc2_increased), + } def test_add_neighbours(mocs): @@ -542,31 +590,31 @@ def test_neighbours(mocs): def mocs_op(): moc1 = MOC.from_json({"0": [0, 2, 3, 4, 5]}) moc2 = MOC.from_json({"0": [0, 1, 7, 4, 3]}) - return dict(first=moc1, second=moc2) + return {"first": moc1, "second": moc2} def test_moc_union(mocs_op): assert mocs_op["first"].union(mocs_op["second"]) == MOC.from_json( - {"0": [0, 1, 2, 3, 4, 5, 7]} + {"0": [0, 1, 2, 3, 4, 5, 7]}, ) assert mocs_op["first"] + mocs_op["second"] == MOC.from_json( - {"0": [0, 1, 2, 3, 4, 5, 7]} + {"0": [0, 1, 2, 3, 4, 5, 7]}, ) assert mocs_op["first"] | mocs_op["second"] == MOC.from_json( - {"0": [0, 1, 2, 3, 4, 5, 7]} + {"0": [0, 1, 2, 3, 4, 5, 7]}, ) def test_moc_intersection(mocs_op): assert mocs_op["first"].intersection(mocs_op["second"]) == MOC.from_json( - {"0": [0, 3, 4]} + {"0": [0, 3, 4]}, ) assert mocs_op["first"] & mocs_op["second"] == MOC.from_json({"0": [0, 3, 4]}) def test_moc_difference(mocs_op): assert mocs_op["first"].difference(mocs_op["second"]) == MOC.from_json( - {"0": [2, 5]} + {"0": [2, 5]}, ) assert mocs_op["first"] - mocs_op["second"] == MOC.from_json({"0": [2, 5]}) @@ -587,7 +635,7 @@ def test_from_fits_old(): ( MOC.from_json({"0": [1, 3]}), MOC.from_json({"0": [0, 2, 4, 5, 6, 7, 8, 9, 10, 11]}), - ) + ), ], ) def test_moc_complement(input, expected): @@ -616,7 +664,8 @@ def test_from_valued_healpix_cells_different_sizes(): values = np.array([]) with pytest.raises( - ValueError, match="`uniq` and values do not have the same size." + ValueError, + match="`uniq` and values do not have the same size.", ): MOC.from_valued_healpix_cells(uniq, values, 12) @@ -638,7 +687,11 @@ def test_from_valued_healpix_cells_weird_values(cumul_from, cumul_to): values = np.array([-1.0]) MOC.from_valued_healpix_cells( - uniq, values, 12, cumul_from=cumul_from, cumul_to=cumul_to + uniq, + values, + 12, + cumul_from=cumul_from, + cumul_to=cumul_to, ) @@ -673,7 +726,9 @@ def test_from_valued_healpix_cells_bayestar(): def test_from_valued_healpix_cells_bayestar_and_split(): fits_mom_filename = "./resources/bayestar.multiorder.fits" moc = MOC.from_multiordermap_fits_file( - fits_mom_filename, cumul_from=0.0, cumul_to=0.9 + fits_mom_filename, + cumul_from=0.0, + cumul_to=0.9, ) count = moc.split_count() assert count == 2 diff --git a/python/mocpy/tests/test_tmoc.py b/python/mocpy/tests/test_tmoc.py index 1a437a6c..f70ae868 100644 --- a/python/mocpy/tests/test_tmoc.py +++ b/python/mocpy/tests/test_tmoc.py @@ -45,6 +45,10 @@ def test_complement(): 61, np.array([[0, 1], [2, 5], [8, 2 * 2**61]], dtype=np.uint64) ) +def test_to_depth61_ranges(): + assert (TimeMOC.from_depth61_ranges( + 61, np.array([[1, 2], [6, 8], [5, 6]], dtype=np.uint64) + ).to_depth61_ranges == np.array([[1, 2], [5, 8]], dtype=np.uint64)).all() def test_empty_tmoc(): times = Time([], format="jd", scale="tdb") diff --git a/python/mocpy/tmoc/tmoc.py b/python/mocpy/tmoc/tmoc.py index ef6bea64..9234b19a 100644 --- a/python/mocpy/tmoc/tmoc.py +++ b/python/mocpy/tmoc/tmoc.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals import warnings import numpy as np @@ -56,7 +54,7 @@ def microseconds_to_times(times_microseconds): """ jd1 = np.asarray(times_microseconds // DAY_MICRO_SEC, dtype=np.float64) jd2 = np.asarray( - (times_microseconds - jd1 * DAY_MICRO_SEC) / DAY_MICRO_SEC, dtype=np.float64 + (times_microseconds - jd1 * DAY_MICRO_SEC) / DAY_MICRO_SEC, dtype=np.float64, ) return Time(val=jd1, val2=jd2, format="jd", scale="tdb") @@ -65,8 +63,12 @@ def microseconds_to_times(times_microseconds): class TimeMOC(AbstractMOC): """Multi-order time coverage class. Experimental.""" + # Maximum order of TimeMOCs + # (do not remove since it may be used externally). + MAX_ORDER = np.uint8(61) + # Number of microseconds in a day DAY_MICRO_SEC = 86400000000.0 - # default observation time : 30 min + # Default observation time : 30 min DEFAULT_OBSERVATION_TIME = TimeDelta(30 * 60, format="sec", scale="tdb") __create_key = object() @@ -78,8 +80,8 @@ def __init__(self, create_key, store_index): create_key: Object ensure __init__ is called by super-class/class-methods only store_index: index of the S-MOC in the rust-side storage """ - super(TimeMOC, self).__init__( - AbstractMOC._create_key, TimeMOC.__create_key, store_index + super().__init__( + AbstractMOC._create_key, TimeMOC.__create_key, store_index, ) assert ( create_key == TimeMOC.__create_key @@ -95,6 +97,11 @@ def to_time_ranges(self): """Returns the time ranges this TimeMOC contains.""" return microseconds_to_times(mocpy.to_ranges(self._store_index)) + @property + def to_depth61_ranges(self): + """Return the list of ranges this TimeMOC contains, in microsec since JD=0.""" + return mocpy.to_ranges(self._store_index) + def degrade_to_order(self, new_order): """ Degrades the MOC instance to a new, less precise, MOC. @@ -216,13 +223,13 @@ def from_time_ranges(cls, min_times, max_times, delta_t=DEFAULT_OBSERVATION_TIME assert min_times.shape == max_times.shape store_index = mocpy.from_time_ranges_in_microsec_since_jd_origin( - depth, min_times, max_times + depth, min_times, max_times, ) return cls(cls.__create_key, store_index) @classmethod def from_time_ranges_approx( - cls, min_times, max_times, delta_t=DEFAULT_OBSERVATION_TIME + cls, min_times, max_times, delta_t=DEFAULT_OBSERVATION_TIME, ): """ Create a TimeMOC from a range defined by two `astropy.time.Time`. @@ -292,7 +299,7 @@ def _process_degradation(self, another_moc, order_op): message = ( "Requested time resolution for the operation cannot be applied.\n" "The TimeMoc object resulting from the operation is of time resolution {0} sec.".format( - TimeMOC.order_to_time_resolution(max_order).sec + TimeMOC.order_to_time_resolution(max_order).sec, ) ) warnings.warn(message, UserWarning) @@ -304,7 +311,7 @@ def _process_degradation(self, another_moc, order_op): return result def intersection_with_timeresolution( - self, another_moc, delta_t=DEFAULT_OBSERVATION_TIME + self, another_moc, delta_t=DEFAULT_OBSERVATION_TIME, ): """ Intersection between self and moc. @@ -360,7 +367,7 @@ def union_with_timeresolution(self, another_moc, delta_t=DEFAULT_OBSERVATION_TIM return super(TimeMOC, self_degraded).union(moc_degraded) def difference_with_timeresolution( - self, another_moc, delta_t=DEFAULT_OBSERVATION_TIME + self, another_moc, delta_t=DEFAULT_OBSERVATION_TIME, ): """ Difference between self and moc. @@ -401,7 +408,7 @@ def total_duration(self): """ duration = TimeDelta( - mocpy.ranges_sum(self._store_index) / 1e6, format="sec", scale="tdb" + mocpy.ranges_sum(self._store_index) / 1e6, format="sec", scale="tdb", ) return duration @@ -478,7 +485,7 @@ def contains(self, times, keep_inside=True): return ~mask def contains_with_timeresolution( - self, times, keep_inside=True, delta_t=DEFAULT_OBSERVATION_TIME + self, times, keep_inside=True, delta_t=DEFAULT_OBSERVATION_TIME, ): """ Get a mask array (e.g. a numpy boolean array) of times being inside (or outside) the TMOC instance. @@ -508,7 +515,7 @@ def contains_with_timeresolution( message = ( "Requested time resolution filtering cannot be applied.\n" "Filtering is applied with a time resolution of {0} sec.".format( - TimeMOC.order_to_time_resolution(current_max_order).sec + TimeMOC.order_to_time_resolution(current_max_order).sec, ) ) warnings.warn(message, UserWarning) @@ -580,19 +587,16 @@ def plot(self, title="TimeMoc", view=(None, None), figsize=(9.5, 5), **kwargs): return plot_order = 30 - if self.max_order > plot_order: - plotted_moc = self.degrade_to_order(plot_order) - else: - plotted_moc = self + plotted_moc = self.degrade_to_order(plot_order) if self.max_order > plot_order else self min_jd = plotted_moc.min_time.jd if not view[0] else view[0].jd max_jd = plotted_moc.max_time.jd if not view[1] else view[1].jd if max_jd < min_jd: raise ValueError( - "Invalid selection: max_jd = {0} must be > to min_jd = {1}".format( - max_jd, min_jd - ) + "Invalid selection: max_jd = {} must be > to min_jd = {}".format( + max_jd, min_jd, + ), ) fig1 = plt.figure(figsize=figsize) @@ -607,11 +611,11 @@ def plot(self, title="TimeMoc", view=(None, None), figsize=(9.5, 5), **kwargs): ax.set_xticks([0, size]) ax.set_xticklabels( - Time([min_jd_time, max_jd], format="jd", scale="tdb").iso, rotation=70 + Time([min_jd_time, max_jd], format="jd", scale="tdb").iso, rotation=70, ) y = np.zeros(size) - for (s_time_us, e_time_us) in plotted_moc.to_time_ranges(): + for s_time_us, e_time_us in plotted_moc.to_time_ranges(): s_index = int((s_time_us.jd - min_jd_time) / delta) e_index = int((e_time_us.jd - min_jd_time) / delta) y[s_index : (e_index + 1)] = 1.0 @@ -638,7 +642,7 @@ def on_mouse_motion(event): time = Time(event.xdata * delta + min_jd_time, format="jd", scale="tdb") - tx = "{0}".format(time.iso) + tx = f"{time.iso}" text.set_position((event.xdata - 50, 700)) text.set_rotation(70) text.set_text(tx) diff --git a/python/mocpy/version.py b/python/mocpy/version.py index def467e0..76da4a98 100644 --- a/python/mocpy/version.py +++ b/python/mocpy/version.py @@ -1 +1 @@ -__version__ = "0.12.1" +__version__ = "0.12.2"