diff --git a/pyproject.toml b/pyproject.toml index 4837e72..454b748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,5 +45,63 @@ exclude = ["smart._dev*"] [tool.setuptools_scm] write_to = "smart/_version.py" +[ tool.gilesbot ] + + [ tool.gilesbot.pull_requests ] + enabled = true + + [ tool.gilesbot.towncrier_changelog ] + enabled = true + verify_pr_number = true + changelog_skip_label = "No Changelog Entry Needed" + help_url = "https://github.com/TCDSolar/SMARTpy/blob/main/changelog/README.rst" + + changelog_missing_long = "There isn't a changelog file in this pull request. Please add a changelog file to the `changelog/` directory following the instructions in the changelog [README](https://github.com/TCDSolar/SMARTpy/blob/main/changelog/README.rst)." + + type_incorrect_long = "The changelog file you added is not one of the allowed types. Please use one of the types described in the changelog [README](https://github.com/TCDSolar/SMARTpy/blob/main/changelog/README.rst)" + + number_incorrect_long = "The number in the changelog file you added does not match the number of this pull request. Please rename the file." + +[tool.towncrier] + package = "CHIMERApy" + filename = "CHANGELOG.rst" + directory = "changelog/" + issue_format = "`#{issue} `__" + + [[tool.towncrier.type]] + directory = "breaking" + name = "Backwards Incompatible Changes" + showcontent = true + + [[tool.towncrier.type]] + directory = "api" + name = "API Changes" + showcontent = true + + [[tool.towncrier.type]] + directory = "removal" + name = "Deprecations and Removals" + showcontent = true + + [[tool.towncrier.type]] + directory = "feature" + name = "Features" + showcontent = true + + [[tool.towncrier.type]] + directory = "bugfix" + name = "Bug Fixes" + showcontent = true + + [[tool.towncrier.type]] + directory = "doc" + name = "Improved Documentation" + showcontent = true + + [[tool.towncrier.type]] + directory = "trivial" + name = "Trivial/Internal Changes" + showcontent = true + [tool.mypy] python_version = "3.9" diff --git a/smart/characterization/dB_dt.py b/smart/characterization/dB_dt.py deleted file mode 100644 index 1072910..0000000 --- a/smart/characterization/dB_dt.py +++ /dev/null @@ -1,160 +0,0 @@ -import numpy as np - -import astropy.units as u - -from sunpy.map import Map, all_coordinates_from_map, coordinate_is_on_solar_disk - -from smart.differential_rotation import diff_rotation -from smart.map_processing import calculate_cosine_correction - -__all__ = ["cosine_weighted_area_map", "extract_features", "dB_dt", "get_properties"] - - -def cosine_weighted_area_map(im_map: Map): - """ - Calculate the cosine-weighted area map for a feature and determine the feature's total area. - - Parameters - ---------- - im_map : Map - Processed SunPy magnetogram map. - feature_mask : numpy.ndarray - Binary mask where feature pixels = 1 and background pixels = 0. - - Returns - ------- - total_area : astropy.units.quantity.Quantity - The total area of the feature in square metres. - area_map : astropy.units.quantity.Quantity - Area map corrected for cosine projection. - """ - cos_cor = calculate_cosine_correction(im_map) - - m_per_arcsec = im_map.rsun_meters / im_map.rsun_obs - pixel_area = (im_map.scale[0] * m_per_arcsec) * (im_map.scale[1] * m_per_arcsec) - pixel_area = pixel_area * u.pix**2 - - area_map = pixel_area * cos_cor - - return area_map - - -def extract_features(sorted_labels): - """ - Extract binary masks for each feature found in index_and_grow_mask's sorted_labels. - - Parameters - ---------- - sorted_labels : numpy.ndarray - An array where each unique label corresponds to a different feature on the solar disk. - - Returns - ------- - feature_masks : list - A list containing a numpy.ndarray binary mask for each identified feature. - """ - unique_labels = np.unique(sorted_labels) - unique_labels = unique_labels[unique_labels != 0] - - feature_masks = [] - for label_value in unique_labels: - feature_mask = (sorted_labels == label_value).astype(int) - feature_masks.append(feature_mask) - - return feature_masks - - -def dB_dt(current_map: Map, previous_map: Map): - """ - A magnetogram differentially rotated to time 't' is subtracted from a processed magnetogram from time 't', and the resultant map is divided - by their time separation, yielding a map of the temporal change in field strength. - - Parameters - ---------- - current_map : Map - Processed SunPy magnetogram map from time 't'. - previous_map : Map - Processed SunPy magnetogram map from time 't - delta_t'. - - Returns - ------- - dB_dt : Map - Map showcasing the change in magnetic field strength over time. - dB : Quantity - The change in magnetic field strength. - dt : Quantity - The time interval over which the change in magnetic field strength was measured. - """ - diff_map = diff_rotation(current_map, previous_map) - - dB = (current_map.data - diff_map.data) * u.Gauss - dt = (current_map.date - previous_map.date).to(u.s) - - dB_dt = Map(dB / dt, current_map.meta) - dB_dt.data[~coordinate_is_on_solar_disk(all_coordinates_from_map(dB_dt))] = np.nan - dB_dt.cmap.set_bad("k") - return dB_dt, dt - - -def get_properties(im_map, dB_dt, dt, sorted_labels): - """ - Calculate various properties for each detected feature. - - Parameters - ---------- - im_map : Map - Processed SunPy magnetogram map. - dB_dt : Map - Map showcasing the change in magnetic field strength over time. - dt : Quantity - The time interval over which the change in magnetic field strength was measured. - sorted_labels : numpy.ndarray - An array where each unique label corresponds to a different feature on the solar disk. - - Returns - ------- - properties : list - A list containing properties related to each individual feature - """ - - feature_masks = extract_features(sorted_labels) - - area_map = cosine_weighted_area_map(im_map) - - properties = [] - for i, feature_mask in enumerate(feature_masks, start=1): - region_area_map = area_map * feature_mask - dBdt_data = dB_dt.data * u.G - total_area = np.sum(region_area_map) - - magnetic_flux = np.nansum(dBdt_data * region_area_map) - flux_emergence_rate = magnetic_flux / dt - - B = im_map.data * feature_mask * u.G - B_mean = np.nanmean(B) - B_std = np.nanstd(B) - B_min = np.nanmin(B) - B_max = np.nanmax(B) - - flux_pos = np.nansum(B[B > 0] * area_map[B > 0]) - flux_neg = np.nansum(B[B < 0] * area_map[B < 0]) - flux_uns = np.nansum(np.abs(B) * area_map) - flux_imb = (flux_pos - flux_neg) / (flux_pos + flux_neg) - - properties.append( - { - "feature label": i, - "flux emergence rate": flux_emergence_rate.to(u.Wb / u.s), - "mean B": B_mean.to(u.G), - "std B": B_std.to(u.G), - "minimum B": B_min.to(u.G), - "maximum B": B_max.to(u.G), - "positive flux": flux_pos.to(u.Wb), - "negative flux": flux_neg.to(u.Wb), - "unsigned flux": flux_uns.to(u.Wb), - "flux imbalance": flux_imb, - "total area": total_area.to(u.m**2), - } - ) - - return properties diff --git a/smart/segmentation/indexed_grown_mask.py b/smart/segmentation/indexed_grown_mask.py deleted file mode 100644 index 417d44f..0000000 --- a/smart/segmentation/indexed_grown_mask.py +++ /dev/null @@ -1,117 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import skimage as ski -from skimage.morphology import disk - -import astropy.units as u - -from sunpy.map import Map - -from smart.map_processing import smooth_los_threshold - - -def index_and_grow_mask( - current_map: Map, previous_map: Map, dilation_radius: u.Quantity[u.arcsec] = 2.5 * u.arcsec -): - """ - Performing indexing and growing of the Mask. - - Transient features are removed by comparing the mask at time 't' and the mask differentially - rotated to time 't'. ARs are then assigned ascending integer values (starting from one) in - order of decreasing size. - - Parameters - ---------- - current_map : Map - Processed magnetogram map from time 't'. - previous_map : Map - Processed magnetogtam map from time 't - delta_t' differentially rotated to time t. - dilation_radius : int, optional - Radius of the disk for binary dilation (default is 2.5 arcsecs). - - Returns - ------- - sorted_labels : numpy.ndarray - Individual contiguous features are indexed by assigning ascending integer - values (beginning with one) in order of decreasing feature size. - - """ - pixel_size = (current_map.scale[0] + current_map.scale[1]) / 2 - dilation_radius = (np.round(dilation_radius / pixel_size)).to_value(u.pix) - - filtered_labels = smooth_los_threshold(current_map)[1] - filtered_labels_dt = smooth_los_threshold(previous_map)[1] - - dilated_mask = ski.morphology.binary_dilation(filtered_labels, disk(dilation_radius)) - dilated_mask_dt = ski.morphology.binary_dilation(filtered_labels_dt, disk(dilation_radius)) - - transient_features = dilated_mask_dt & ~dilated_mask - final_mask = dilated_mask & ~transient_features - final_labels = ski.measure.label(final_mask) - - regions = ski.measure.regionprops(final_labels) - region_sizes = [(region.label, region.area) for region in regions] - - sorted_region_sizes = sorted(region_sizes, key=lambda x: x[1], reverse=True) - sorted_labels = np.zeros_like(final_labels) - for new_label, (old_label, _) in enumerate(sorted_region_sizes, start=1): - sorted_labels[final_labels == old_label] = new_label - - return sorted_labels - - -def plot_indexed_grown_mask(im_map: Map, sorted_labels, contours=True, labels=True, figtext=True): - """ - Plotting the fully processed and segmented magnetogram with labels and AR contours optionally displayed. - - Parameters - ---------- - im_map : Map - Processed magnetogram map from time 't'. - diff_map : Map - Processed magnetogtam map from time 't - delta_t' differentially rotated to time t. - contours : bool, optional - If True, contours of the detected regions displayed on map (default is True). - labels : bool, optional - If True, labels with the region numbers will be overlaid on the regions (default is True). - figtext : bool, optional - If True, figtext with the total number of detected regions is displayed on the map (default is True). - - Returns - ------- - None. - - """ - - fig = plt.figure() - ax = fig.add_subplot(projection=im_map) - im_map.plot(axes=ax) - - unique_labels = np.unique(sorted_labels) - unique_labels = unique_labels[unique_labels != 0] - - if contours: - for label_value in unique_labels: - region_mask = sorted_labels == label_value - ax.contour(region_mask, colors="purple", linewidths=1) - - if labels: - for label_value in unique_labels: - region_mask = sorted_labels == label_value - region = ski.measure.regionprops(region_mask.astype(int))[0] - centroid = region.centroid - ax.text( - centroid[1], - centroid[0], - str(label_value), - color="red", - fontsize=12, - weight="bold", - ha="center", - va="center", - ) - - if figtext: - plt.figtext(0.47, 0.2, f"Number of regions = {len(unique_labels)}", color="white") - - plt.show()