|
10 | 10 | from numpy import linalg as LA |
11 | 11 | from scipy.special import erf |
12 | 12 |
|
13 | | -from pycs.sparsity.sparse2d.starlet import * |
14 | | -from pycs.misc.cosmostat_init import * |
15 | | -from pycs.misc.mr_prog import * |
16 | | -from pycs.misc.utilHSS import * |
17 | | -from pycs.misc.im1d_tend import * |
18 | | -from pycs.misc.stats import * |
19 | | -from pycs.sparsity.sparse2d.dct import dct2d, idct2d |
20 | | -from pycs.sparsity.sparse2d.dct_inpainting import dct_inpainting |
21 | | -from pycs.misc.im_isospec import * |
22 | | -from pycs.astro.wl.mass_mapping import * |
| 13 | +# Import spherical functionality - this is needed for the new functions |
23 | 14 | from pycs.sparsity.mrs.mrs_starlet import mrs_uwttrans, CMRStarlet |
| 15 | + |
| 16 | +# Conditional imports to avoid dependency issues with 2D starlet |
| 17 | +try: |
| 18 | + from pycs.misc.cosmostat_init import * |
| 19 | + from pycs.misc.mr_prog import * |
| 20 | + from pycs.misc.utilHSS import * |
| 21 | + from pycs.misc.im1d_tend import * |
| 22 | + from pycs.misc.stats import * |
| 23 | +except ImportError as e: |
| 24 | + print(f"Warning: Some utility functionality may not be available: {e}") |
| 25 | + |
| 26 | +try: |
| 27 | + from pycs.sparsity.sparse2d.starlet import * |
| 28 | + from pycs.sparsity.sparse2d.dct import dct2d, idct2d |
| 29 | + from pycs.sparsity.sparse2d.dct_inpainting import dct_inpainting |
| 30 | + from pycs.misc.im_isospec import * |
| 31 | + from pycs.astro.wl.mass_mapping import * |
| 32 | +except ImportError as e: |
| 33 | + print(f"Warning: Some 2D starlet functionality may not be available: {e}") |
| 34 | + # Define minimal replacement functions for compatibility |
| 35 | + def conv(a, b): |
| 36 | + return a # Placeholder |
24 | 37 | import healpy as hp # Added for Nside calculation |
25 | 38 |
|
26 | 39 |
|
@@ -683,6 +696,283 @@ def get_wtl1_sphere( |
683 | 696 | return np.array(bins_coll), np.array(l1norm_coll) |
684 | 697 |
|
685 | 698 |
|
| 699 | +def get_peaks_sphere(healpix_map, threshold=None, ordered=True, mask=None, nside=None): |
| 700 | + """Identify peaks in a HEALPix map above a given threshold. |
| 701 | +
|
| 702 | + A peak, or local maximum, is defined as a pixel with a value larger than |
| 703 | + all of its neighbors on the sphere. A mask may be provided to exclude |
| 704 | + certain regions from the search. |
| 705 | +
|
| 706 | + Parameters |
| 707 | + ---------- |
| 708 | + healpix_map : array_like |
| 709 | + One-dimensional HEALPix map. |
| 710 | + threshold : float, optional |
| 711 | + Minimum pixel amplitude to be considered as a peak. If not provided, |
| 712 | + the default value is set to the minimum of `healpix_map`. |
| 713 | + ordered : bool, optional |
| 714 | + If True, return peaks in decreasing order according to height. |
| 715 | + mask : array_like (same shape as `healpix_map`), optional |
| 716 | + Boolean array identifying which pixels of `healpix_map` to consider/exclude |
| 717 | + in finding peaks. A numerical array will be converted to binary, where |
| 718 | + only zero values are considered masked. |
| 719 | + nside : int, optional |
| 720 | + HEALPix nside parameter. If not provided, it will be inferred from map size. |
| 721 | +
|
| 722 | + Returns |
| 723 | + ------- |
| 724 | + pixel_indices, heights : tuple of 1D numpy arrays |
| 725 | + Pixel indices of peak positions and their associated heights. |
| 726 | +
|
| 727 | + Notes |
| 728 | + ----- |
| 729 | + This is the spherical version of get_peaks, designed for HEALPix maps. |
| 730 | + It uses healpy.get_all_neighbours to find the neighbors of each pixel. |
| 731 | + """ |
| 732 | + healpix_map = np.atleast_1d(healpix_map) |
| 733 | + |
| 734 | + # Determine nside if not provided |
| 735 | + if nside is None: |
| 736 | + nside = hp.npix2nside(len(healpix_map)) |
| 737 | + |
| 738 | + npix = hp.nside2npix(nside) |
| 739 | + if len(healpix_map) != npix: |
| 740 | + raise ValueError(f"Map size ({len(healpix_map)}) doesn't match nside={nside} (npix={npix})") |
| 741 | + |
| 742 | + # Deal with the mask first |
| 743 | + if mask is not None: |
| 744 | + mask = np.atleast_1d(mask) |
| 745 | + if mask.shape != healpix_map.shape: |
| 746 | + print("Warning: mask not compatible with map -> ignoring.") |
| 747 | + mask = np.ones(healpix_map.shape) |
| 748 | + else: |
| 749 | + # Make sure mask is binary, i.e. turn nonzero values into ones |
| 750 | + mask = mask.astype(bool).astype(float) |
| 751 | + else: |
| 752 | + mask = np.ones(healpix_map.shape) |
| 753 | + |
| 754 | + # Determine threshold level |
| 755 | + if threshold is None: |
| 756 | + threshold = healpix_map[mask.astype('bool')].min() |
| 757 | + else: |
| 758 | + threshold = max(threshold, healpix_map.min()) |
| 759 | + |
| 760 | + # Find peaks by checking each pixel against its neighbors |
| 761 | + peak_pixels = [] |
| 762 | + peak_heights = [] |
| 763 | + |
| 764 | + for ipix in range(npix): |
| 765 | + # Skip if pixel is masked |
| 766 | + if mask[ipix] == 0: |
| 767 | + continue |
| 768 | + |
| 769 | + pixel_value = healpix_map[ipix] |
| 770 | + |
| 771 | + # Skip if below threshold |
| 772 | + if pixel_value < threshold: |
| 773 | + continue |
| 774 | + |
| 775 | + # Get neighbors of this pixel |
| 776 | + neighbors = hp.get_all_neighbours(nside, ipix) |
| 777 | + |
| 778 | + # Remove invalid neighbors (-1 values) |
| 779 | + valid_neighbors = neighbors[neighbors >= 0] |
| 780 | + |
| 781 | + # Check if this pixel is higher than all its valid neighbors |
| 782 | + is_peak = True |
| 783 | + for neighbor_idx in valid_neighbors: |
| 784 | + # Skip masked neighbors |
| 785 | + if mask[neighbor_idx] == 0: |
| 786 | + continue |
| 787 | + if healpix_map[neighbor_idx] >= pixel_value: |
| 788 | + is_peak = False |
| 789 | + break |
| 790 | + |
| 791 | + if is_peak: |
| 792 | + peak_pixels.append(ipix) |
| 793 | + peak_heights.append(pixel_value) |
| 794 | + |
| 795 | + peak_pixels = np.array(peak_pixels) |
| 796 | + peak_heights = np.array(peak_heights) |
| 797 | + |
| 798 | + # Sort by height if requested |
| 799 | + if ordered and len(peak_heights) > 0: |
| 800 | + sort_indices = np.argsort(peak_heights)[::-1] # Descending order |
| 801 | + peak_pixels = peak_pixels[sort_indices] |
| 802 | + peak_heights = peak_heights[sort_indices] |
| 803 | + |
| 804 | + return peak_pixels, peak_heights |
| 805 | + |
| 806 | + |
| 807 | +def get_wtpeaks_sphere( |
| 808 | + Map, |
| 809 | + nscales, |
| 810 | + nbins=None, |
| 811 | + Mask=None, |
| 812 | + min_snr=None, |
| 813 | + max_snr=None, |
| 814 | + noise_std=None, |
| 815 | + peak_threshold=None, |
| 816 | +): |
| 817 | + """ |
| 818 | + Calculate multi-scale peak counts for a HEALPix map using spherical wavelet transform. |
| 819 | + |
| 820 | + This function performs a spherical wavelet transform using CMRStarlet, then identifies |
| 821 | + peaks at each scale and returns histograms of peak counts binned by peak height/SNR. |
| 822 | + This is the spherical analog of the get_wtpeaks method. |
| 823 | +
|
| 824 | + Parameters |
| 825 | + ---------- |
| 826 | + Map : array_like |
| 827 | + HEALPix map to analyze. |
| 828 | + nscales : int |
| 829 | + Number of wavelet scales to use. |
| 830 | + nbins : int, optional |
| 831 | + Number of bins for the histogram. Default is 40. |
| 832 | + Mask : array_like, optional |
| 833 | + Mask indicating where we have observations. Only pixels where Mask != 0 are considered. |
| 834 | + min_snr : float, optional |
| 835 | + Minimum value for binning the normalized coefficients. |
| 836 | + If None, uses the minimum value in the coefficients for the current scale. |
| 837 | + max_snr : float, optional |
| 838 | + Maximum value for binning the normalized coefficients. |
| 839 | + If None, uses the maximum value in the coefficients for the current scale. |
| 840 | + noise_std : float, optional |
| 841 | + Noise standard deviation. If provided, coefficients are divided by this value |
| 842 | + to compute an SNR before binning. Default is None. |
| 843 | + peak_threshold : float, optional |
| 844 | + Minimum peak height threshold for peak detection. If None, uses minimum value |
| 845 | + of each scale. |
| 846 | +
|
| 847 | + Returns |
| 848 | + ------- |
| 849 | + tuple of arrays |
| 850 | + (bins_coll, peaks_count_coll, peaks_pixels_coll, peaks_heights_coll) where: |
| 851 | + - bins_coll[i] are the bin centers for scale i |
| 852 | + - peaks_count_coll[i] are the peak counts for each bin at scale i |
| 853 | + - peaks_pixels_coll[i] are the pixel indices of peaks at scale i |
| 854 | + - peaks_heights_coll[i] are the peak heights at scale i |
| 855 | + """ |
| 856 | + |
| 857 | + # Set default for nbins if not provided |
| 858 | + if nbins is None: |
| 859 | + nbins = 40 |
| 860 | + |
| 861 | + # Determine Nside from the input map |
| 862 | + Nside = hp.npix2nside(Map.shape[0]) |
| 863 | + |
| 864 | + # Initialize and perform CMRStarlet transform |
| 865 | + C = CMRStarlet() |
| 866 | + C.init_starlet(Nside, nscale=nscales) |
| 867 | + C.transform(Map) |
| 868 | + |
| 869 | + bins_coll = [] |
| 870 | + peaks_count_coll = [] |
| 871 | + peaks_pixels_coll = [] |
| 872 | + peaks_heights_coll = [] |
| 873 | + |
| 874 | + # Loop through each scale of the wavelet transform |
| 875 | + for i in range(nscales): |
| 876 | + # Get normalized wavelet coefficients for the i-th scale |
| 877 | + if C.TabNorm[i] == 0: # Avoid division by zero if TabNorm is zero |
| 878 | + ScaleCoeffs = C.coef[i].copy() |
| 879 | + else: |
| 880 | + ScaleCoeffs = C.coef[i] / C.TabNorm[i] |
| 881 | + |
| 882 | + # If noise_std is provided, convert to SNR |
| 883 | + if noise_std is not None: |
| 884 | + ScaleCoeffs = ScaleCoeffs / noise_std |
| 885 | + |
| 886 | + # Find peaks in the current scale |
| 887 | + peak_pixels, peak_heights = get_peaks_sphere( |
| 888 | + ScaleCoeffs, |
| 889 | + threshold=peak_threshold, |
| 890 | + ordered=True, |
| 891 | + mask=Mask, |
| 892 | + nside=Nside |
| 893 | + ) |
| 894 | + |
| 895 | + # Store peak information |
| 896 | + peaks_pixels_coll.append(peak_pixels) |
| 897 | + peaks_heights_coll.append(peak_heights) |
| 898 | + |
| 899 | + # Create histogram of peak heights if we have peaks |
| 900 | + if len(peak_heights) > 0: |
| 901 | + # Determine binning range |
| 902 | + if min_snr is not None: |
| 903 | + current_min_val = min_snr |
| 904 | + else: |
| 905 | + current_min_val = np.min(peak_heights) if len(peak_heights) > 0 else 0 |
| 906 | + |
| 907 | + if max_snr is not None: |
| 908 | + current_max_val = max_snr |
| 909 | + else: |
| 910 | + current_max_val = np.max(peak_heights) if len(peak_heights) > 0 else 1 |
| 911 | + |
| 912 | + # Define thresholds and bins |
| 913 | + thresholds = np.linspace(current_min_val, current_max_val, nbins + 1) |
| 914 | + bins = 0.5 * (thresholds[:-1] + thresholds[1:]) |
| 915 | + |
| 916 | + # Create histogram of peak heights |
| 917 | + counts, _ = np.histogram(peak_heights, bins=thresholds) |
| 918 | + else: |
| 919 | + # No peaks found, create empty bins |
| 920 | + if min_snr is not None and max_snr is not None: |
| 921 | + thresholds = np.linspace(min_snr, max_snr, nbins + 1) |
| 922 | + else: |
| 923 | + thresholds = np.linspace(0, 1, nbins + 1) |
| 924 | + bins = 0.5 * (thresholds[:-1] + thresholds[1:]) |
| 925 | + counts = np.zeros(nbins, dtype=int) |
| 926 | + |
| 927 | + # Store the bins and counts for this scale |
| 928 | + bins_coll.append(bins) |
| 929 | + peaks_count_coll.append(counts) |
| 930 | + |
| 931 | + return ( |
| 932 | + np.array(bins_coll), |
| 933 | + np.array(peaks_count_coll), |
| 934 | + peaks_pixels_coll, |
| 935 | + peaks_heights_coll |
| 936 | + ) |
| 937 | + |
| 938 | + |
| 939 | +def test_spherical_peaks(): |
| 940 | + """Test function for the new spherical peak counting functionality.""" |
| 941 | + import numpy as np |
| 942 | + import healpy as hp |
| 943 | + |
| 944 | + print("Testing spherical peak counting functions...") |
| 945 | + |
| 946 | + # Create test data |
| 947 | + nside = 8 |
| 948 | + npix = hp.nside2npix(nside) |
| 949 | + test_map = np.random.normal(0, 0.3, npix) |
| 950 | + |
| 951 | + # Add some peaks |
| 952 | + test_map[100] = 2.0 |
| 953 | + test_map[200] = 1.5 |
| 954 | + test_map[300] = 1.0 |
| 955 | + |
| 956 | + # Test get_peaks_sphere |
| 957 | + peak_pixels, peak_heights = get_peaks_sphere(test_map, threshold=0.5) |
| 958 | + print(f"get_peaks_sphere: Found {len(peak_pixels)} peaks") |
| 959 | + |
| 960 | + # Test get_wtpeaks_sphere |
| 961 | + bins, counts, pixels_list, heights_list = get_wtpeaks_sphere( |
| 962 | + test_map, nscales=3, nbins=10 |
| 963 | + ) |
| 964 | + print(f"get_wtpeaks_sphere: Analysis complete!") |
| 965 | + print(f"Peaks per scale: {[len(p) for p in pixels_list]}") |
| 966 | + |
| 967 | + # Test with SNR |
| 968 | + bins_snr, counts_snr, _, _ = get_wtpeaks_sphere( |
| 969 | + test_map, nscales=3, nbins=10, noise_std=0.3 |
| 970 | + ) |
| 971 | + print("SNR analysis successful!") |
| 972 | + |
| 973 | + return True |
| 974 | + |
| 975 | + |
686 | 976 | ############# TESTS routine ############ |
687 | 977 |
|
688 | 978 |
|
|
0 commit comments