diff --git a/unravel/docs/_build/doctrees/environment.pickle b/unravel/docs/_build/doctrees/environment.pickle new file mode 100644 index 00000000..13e548cf Binary files /dev/null and b/unravel/docs/_build/doctrees/environment.pickle differ diff --git a/unravel/docs/_build/doctrees/guide.doctree b/unravel/docs/_build/doctrees/guide.doctree new file mode 100644 index 00000000..64a29097 Binary files /dev/null and b/unravel/docs/_build/doctrees/guide.doctree differ diff --git a/unravel/docs/_build/doctrees/index.doctree b/unravel/docs/_build/doctrees/index.doctree new file mode 100644 index 00000000..4f463dbf Binary files /dev/null and b/unravel/docs/_build/doctrees/index.doctree differ diff --git a/unravel/docs/_build/doctrees/installation.doctree b/unravel/docs/_build/doctrees/installation.doctree new file mode 100644 index 00000000..8d5a3e96 Binary files /dev/null and b/unravel/docs/_build/doctrees/installation.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/brain_model.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/brain_model.doctree new file mode 100644 index 00000000..76ff3ee2 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/brain_model.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/cluster_summary.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/cluster_summary.doctree new file mode 100644 index 00000000..c9dc4b7b Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/cluster_summary.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/cluster_validation.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/cluster_validation.doctree new file mode 100644 index 00000000..5ea19c55 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/cluster_validation.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/crop.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/crop.doctree new file mode 100644 index 00000000..5ebe615e Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/crop.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes.doctree new file mode 100644 index 00000000..04a40b55 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.doctree new file mode 100644 index 00000000..259fb51c Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.doctree new file mode 100644 index 00000000..8b14b43f Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/toc.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/toc.doctree new file mode 100644 index 00000000..7b8363ad Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/effect_sizes/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/fdr.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/fdr.doctree new file mode 100644 index 00000000..94218764 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/fdr.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/fdr_range.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/fdr_range.doctree new file mode 100644 index 00000000..e050779d Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/fdr_range.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/find_incongruent_clusters.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/find_incongruent_clusters.doctree new file mode 100644 index 00000000..585a015c Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/find_incongruent_clusters.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/group_bilateral_data.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/group_bilateral_data.doctree new file mode 100644 index 00000000..fadd152a Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/group_bilateral_data.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/index.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/index.doctree new file mode 100644 index 00000000..d8757cb1 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/index.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/legend.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/legend.doctree new file mode 100644 index 00000000..0d564ae5 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/legend.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/org_data.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/org_data.doctree new file mode 100644 index 00000000..c75b36d3 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/org_data.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/prism.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/prism.doctree new file mode 100644 index 00000000..21834ad2 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/prism.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.doctree new file mode 100644 index 00000000..1225674f Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/stats.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/stats.doctree new file mode 100644 index 00000000..365c65b8 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/stats.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/stats_table.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/stats_table.doctree new file mode 100644 index 00000000..6c6d6b43 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/stats_table.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/sunburst.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/sunburst.doctree new file mode 100644 index 00000000..d5034a80 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/sunburst.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/table.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/table.doctree new file mode 100644 index 00000000..2fd0c6ce Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/table.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/cluster_stats/toc.doctree b/unravel/docs/_build/doctrees/unravel/cluster_stats/toc.doctree new file mode 100644 index 00000000..56d41e10 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/cluster_stats/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/argparse_utils.doctree b/unravel/docs/_build/doctrees/unravel/core/argparse_utils.doctree new file mode 100644 index 00000000..b7ab2ed3 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/argparse_utils.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/argparse_utils_rich.doctree b/unravel/docs/_build/doctrees/unravel/core/argparse_utils_rich.doctree new file mode 100644 index 00000000..98a6567a Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/argparse_utils_rich.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/config.doctree b/unravel/docs/_build/doctrees/unravel/core/config.doctree new file mode 100644 index 00000000..a11b05d0 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/config.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/img_io.doctree b/unravel/docs/_build/doctrees/unravel/core/img_io.doctree new file mode 100644 index 00000000..045a91b1 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/img_io.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/img_tools.doctree b/unravel/docs/_build/doctrees/unravel/core/img_tools.doctree new file mode 100644 index 00000000..0c45f28c Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/img_tools.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/toc.doctree b/unravel/docs/_build/doctrees/unravel/core/toc.doctree new file mode 100644 index 00000000..b263d99d Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/core/utils.doctree b/unravel/docs/_build/doctrees/unravel/core/utils.doctree new file mode 100644 index 00000000..99553e55 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/core/utils.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/h5_to_tifs.doctree b/unravel/docs/_build/doctrees/unravel/image_io/h5_to_tifs.doctree new file mode 100644 index 00000000..efa9e949 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/h5_to_tifs.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/img_to_npy.doctree b/unravel/docs/_build/doctrees/unravel/image_io/img_to_npy.doctree new file mode 100644 index 00000000..bfaa8e1e Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/img_to_npy.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/io_img.doctree b/unravel/docs/_build/doctrees/unravel/image_io/io_img.doctree new file mode 100644 index 00000000..e524d58c Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/io_img.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/io_nii.doctree b/unravel/docs/_build/doctrees/unravel/image_io/io_nii.doctree new file mode 100644 index 00000000..11264d6d Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/io_nii.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/metadata.doctree b/unravel/docs/_build/doctrees/unravel/image_io/metadata.doctree new file mode 100644 index 00000000..1e10a5c7 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/metadata.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/nii_hd.doctree b/unravel/docs/_build/doctrees/unravel/image_io/nii_hd.doctree new file mode 100644 index 00000000..b198079d Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/nii_hd.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/nii_info.doctree b/unravel/docs/_build/doctrees/unravel/image_io/nii_info.doctree new file mode 100644 index 00000000..b8a63972 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/nii_info.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/nii_to_tifs.doctree b/unravel/docs/_build/doctrees/unravel/image_io/nii_to_tifs.doctree new file mode 100644 index 00000000..e191a883 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/nii_to_tifs.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/nii_to_zarr.doctree b/unravel/docs/_build/doctrees/unravel/image_io/nii_to_zarr.doctree new file mode 100644 index 00000000..3263b49e Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/nii_to_zarr.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/reorient_nii.doctree b/unravel/docs/_build/doctrees/unravel/image_io/reorient_nii.doctree new file mode 100644 index 00000000..667e013a Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/reorient_nii.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/tif_to_tifs.doctree b/unravel/docs/_build/doctrees/unravel/image_io/tif_to_tifs.doctree new file mode 100644 index 00000000..81708821 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/tif_to_tifs.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/toc.doctree b/unravel/docs/_build/doctrees/unravel/image_io/toc.doctree new file mode 100644 index 00000000..c372e752 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_io/zarr_to_nii.doctree b/unravel/docs/_build/doctrees/unravel/image_io/zarr_to_nii.doctree new file mode 100644 index 00000000..dbbcc889 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_io/zarr_to_nii.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/DoG.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/DoG.doctree new file mode 100644 index 00000000..498e4417 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/DoG.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/atlas/relabel_nii.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/atlas/relabel_nii.doctree new file mode 100644 index 00000000..d827b7a4 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/atlas/relabel_nii.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/atlas/toc.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/atlas/toc.doctree new file mode 100644 index 00000000..3328d5d4 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/atlas/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/atlas/wireframe.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/atlas/wireframe.doctree new file mode 100644 index 00000000..9b3cab46 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/atlas/wireframe.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/avg.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/avg.doctree new file mode 100644 index 00000000..b7e5ccdc Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/avg.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/bbox.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/bbox.doctree new file mode 100644 index 00000000..4f425e53 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/bbox.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/extend.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/extend.doctree new file mode 100644 index 00000000..fb6e0c04 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/extend.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/max.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/max.doctree new file mode 100644 index 00000000..20819d03 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/max.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/pad.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/pad.doctree new file mode 100644 index 00000000..4bff35ca Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/pad.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/rb.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/rb.doctree new file mode 100644 index 00000000..aa241c6a Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/rb.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/spatial_averaging.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/spatial_averaging.doctree new file mode 100644 index 00000000..f0d6a4aa Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/spatial_averaging.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/toc.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/toc.doctree new file mode 100644 index 00000000..d2ba266d Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/transpose_axes.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/transpose_axes.doctree new file mode 100644 index 00000000..511fb30f Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/transpose_axes.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/image_tools/unique_intensities.doctree b/unravel/docs/_build/doctrees/unravel/image_tools/unique_intensities.doctree new file mode 100644 index 00000000..7de21f12 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/image_tools/unique_intensities.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/region_stats/rstats.doctree b/unravel/docs/_build/doctrees/unravel/region_stats/rstats.doctree new file mode 100644 index 00000000..c5981aee Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/region_stats/rstats.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF.doctree b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF.doctree new file mode 100644 index 00000000..ced9819c Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.doctree b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.doctree new file mode 100644 index 00000000..be610bcc Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF_summary.doctree b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF_summary.doctree new file mode 100644 index 00000000..ead30159 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_mean_IF_summary.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/region_stats/rstats_summary.doctree b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_summary.doctree new file mode 100644 index 00000000..eced1e7b Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/region_stats/rstats_summary.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/region_stats/toc.doctree b/unravel/docs/_build/doctrees/unravel/region_stats/toc.doctree new file mode 100644 index 00000000..923f72e2 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/region_stats/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/register/affine_initializer.doctree b/unravel/docs/_build/doctrees/unravel/register/affine_initializer.doctree new file mode 100644 index 00000000..58fe7b85 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/register/affine_initializer.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/register/reg.doctree b/unravel/docs/_build/doctrees/unravel/register/reg.doctree new file mode 100644 index 00000000..1d3c9dfc Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/register/reg.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/register/reg_check.doctree b/unravel/docs/_build/doctrees/unravel/register/reg_check.doctree new file mode 100644 index 00000000..5deb3d62 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/register/reg_check.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/register/reg_check_brain_mask.doctree b/unravel/docs/_build/doctrees/unravel/register/reg_check_brain_mask.doctree new file mode 100644 index 00000000..f051d406 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/register/reg_check_brain_mask.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/register/reg_prep.doctree b/unravel/docs/_build/doctrees/unravel/register/reg_prep.doctree new file mode 100644 index 00000000..5936e728 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/register/reg_prep.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/register/toc.doctree b/unravel/docs/_build/doctrees/unravel/register/toc.doctree new file mode 100644 index 00000000..bb98e401 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/register/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/segment/brain_mask.doctree b/unravel/docs/_build/doctrees/unravel/segment/brain_mask.doctree new file mode 100644 index 00000000..0a1f4d9e Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/segment/brain_mask.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/segment/copy_tifs.doctree b/unravel/docs/_build/doctrees/unravel/segment/copy_tifs.doctree new file mode 100644 index 00000000..72347245 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/segment/copy_tifs.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/segment/ilastik_pixel_classification.doctree b/unravel/docs/_build/doctrees/unravel/segment/ilastik_pixel_classification.doctree new file mode 100644 index 00000000..41a7e994 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/segment/ilastik_pixel_classification.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/segment/toc.doctree b/unravel/docs/_build/doctrees/unravel/segment/toc.doctree new file mode 100644 index 00000000..d071e282 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/segment/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/toc.doctree b/unravel/docs/_build/doctrees/unravel/toc.doctree new file mode 100644 index 00000000..8550dbd7 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/unravel_commands.doctree b/unravel/docs/_build/doctrees/unravel/unravel_commands.doctree new file mode 100644 index 00000000..16eda775 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/unravel_commands.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/aggregate_files_from_sample_dirs.doctree b/unravel/docs/_build/doctrees/unravel/utilities/aggregate_files_from_sample_dirs.doctree new file mode 100644 index 00000000..d7220aa7 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/aggregate_files_from_sample_dirs.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/aggregate_files_w_recursive_search.doctree b/unravel/docs/_build/doctrees/unravel/utilities/aggregate_files_w_recursive_search.doctree new file mode 100644 index 00000000..8afd8127 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/aggregate_files_w_recursive_search.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/clean_tif_dirs.doctree b/unravel/docs/_build/doctrees/unravel/utilities/clean_tif_dirs.doctree new file mode 100644 index 00000000..b38dfa2e Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/clean_tif_dirs.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/prepend_conditions.doctree b/unravel/docs/_build/doctrees/unravel/utilities/prepend_conditions.doctree new file mode 100644 index 00000000..43df71ea Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/prepend_conditions.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/rename.doctree b/unravel/docs/_build/doctrees/unravel/utilities/rename.doctree new file mode 100644 index 00000000..3439639a Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/rename.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/toc.doctree b/unravel/docs/_build/doctrees/unravel/utilities/toc.doctree new file mode 100644 index 00000000..fdd77961 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/utilities/toggle_samples.doctree b/unravel/docs/_build/doctrees/unravel/utilities/toggle_samples.doctree new file mode 100644 index 00000000..c8cb0fcc Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/utilities/toggle_samples.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/apply_mask.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/apply_mask.doctree new file mode 100644 index 00000000..3fe2264f Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/apply_mask.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/hemi_to_LR_avg.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/hemi_to_LR_avg.doctree new file mode 100644 index 00000000..f5804dc0 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/hemi_to_LR_avg.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/mirror.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/mirror.doctree new file mode 100644 index 00000000..6f97566c Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/mirror.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/other/IF_outliers.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/other/IF_outliers.doctree new file mode 100644 index 00000000..80aba301 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/other/IF_outliers.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/other/r_to_p.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/other/r_to_p.doctree new file mode 100644 index 00000000..f4134703 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/other/r_to_p.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/other/toc.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/other/toc.doctree new file mode 100644 index 00000000..762c8ad4 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/other/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/toc.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/toc.doctree new file mode 100644 index 00000000..5ff76f7e Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/vstats.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/vstats.doctree new file mode 100644 index 00000000..ffce3da3 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/vstats.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/vstats_prep.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/vstats_prep.doctree new file mode 100644 index 00000000..339558f4 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/vstats_prep.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/whole_to_LR_avg.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/whole_to_LR_avg.doctree new file mode 100644 index 00000000..5dc53084 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/whole_to_LR_avg.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/voxel_stats/z_score.doctree b/unravel/docs/_build/doctrees/unravel/voxel_stats/z_score.doctree new file mode 100644 index 00000000..fb54b10a Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/voxel_stats/z_score.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/warp/to_atlas.doctree b/unravel/docs/_build/doctrees/unravel/warp/to_atlas.doctree new file mode 100644 index 00000000..13313c00 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/warp/to_atlas.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/warp/to_native.doctree b/unravel/docs/_build/doctrees/unravel/warp/to_native.doctree new file mode 100644 index 00000000..48528b95 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/warp/to_native.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/warp/toc.doctree b/unravel/docs/_build/doctrees/unravel/warp/toc.doctree new file mode 100644 index 00000000..488f70a1 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/warp/toc.doctree differ diff --git a/unravel/docs/_build/doctrees/unravel/warp/warp.doctree b/unravel/docs/_build/doctrees/unravel/warp/warp.doctree new file mode 100644 index 00000000..4768cb74 Binary files /dev/null and b/unravel/docs/_build/doctrees/unravel/warp/warp.doctree differ diff --git a/unravel/docs/_build/html/.buildinfo b/unravel/docs/_build/html/.buildinfo new file mode 100644 index 00000000..6e25d943 --- /dev/null +++ b/unravel/docs/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 60d78d832952e938ff0d81e41f70c5b3 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/unravel/docs/_build/html/.nojekyll b/unravel/docs/_build/html/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/unravel/docs/_build/html/_images/batch_stitching_1.JPG b/unravel/docs/_build/html/_images/batch_stitching_1.JPG new file mode 100644 index 00000000..bcfb50bf Binary files /dev/null and b/unravel/docs/_build/html/_images/batch_stitching_1.JPG differ diff --git a/unravel/docs/_build/html/_images/batch_stitching_2.JPG b/unravel/docs/_build/html/_images/batch_stitching_2.JPG new file mode 100644 index 00000000..66cdf0d3 Binary files /dev/null and b/unravel/docs/_build/html/_images/batch_stitching_2.JPG differ diff --git a/unravel/docs/_build/html/_modules/index.html b/unravel/docs/_build/html/_modules/index.html new file mode 100644 index 00000000..7699c741 --- /dev/null +++ b/unravel/docs/_build/html/_modules/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + Overview: module code — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

All modules for which code is available

+ + +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/brain_model.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/brain_model.html new file mode 100644 index 00000000..a3839d14 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/brain_model.html @@ -0,0 +1,544 @@ + + + + + + + + + + unravel.cluster_stats.brain_model — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.brain_model

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_brain_model`` from UNRAVEL to prep .nii.gz and RGBA .txt for vizualization in dsi_studio.
+
+Usage
+-----
+    cluster_brain_model -i input.csv -m -sa path/gubra_ano_split_25um.nii.gz -v
+
+The input image will be binarized and multiplied by the split atlas to apply region IDs.
+
+Outputs: 
+    img_WB.nii.gz (bilateral version of cluster index w/ ABA colors)
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+import pandas as pd
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times
+from unravel.voxel_stats.mirror import mirror
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help="path/img.nii.gz (e.g., valid cluster index)", required=True, action=SM) + parser.add_argument('-m', '--mirror', help='Mirror the image in the x-axis for a bilateral representation. Default: False', action='store_true', default=False) + parser.add_argument('-ax', '--axis', help='Axis to flip the image along. Default: 0', default=0, type=int, action=SM) + parser.add_argument('-s', '--shift', help='Number of voxels to shift content after flipping. Default: 2', default=2, type=int, action=SM) + parser.add_argument('-sa', '--split_atlas', help='path/gubra_ano_split_25um.nii.gz. Default: gubra_ano_split_25um.nii.gz', default='gubra_ano_split_25um.nii.gz', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.mirror: + output = args.input.replace('.nii.gz', '_ABA_WB.nii.gz') + else: + output = args.input.replace('.nii.gz', '_ABA.nii.gz') + + txt_output = args.input.replace('.nii.gz', '_rgba.txt') + + if Path(output).exists() and Path(txt_output).exists(): + print(f'{output} and {Path(txt_output).name} exist. Skipping.') + return + + + # Load the input NIFTI file + nii = nib.load(args.input) + img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze() + + # Make a bilateral version of the cluster index + if args.mirror: + mirror_img = mirror(img, axis=args.axis, shift=args.shift) + + # Combine original and mirrored images + img = img + mirror_img + + # Binarize + img[img > 0] = 1 + + # Multiply by atlas to apply region IDs to the cluster index + atlas_nii = nib.load(args.split_atlas) + atlas_img = np.asanyarray(atlas_nii.dataobj, dtype=atlas_nii.header.get_data_dtype()).squeeze() + final_data = img * atlas_img + + # Save the bilateral version of the cluster index with ABA colors + + nib.save(nib.Nifti1Image(final_data, atlas_nii.affine, atlas_nii.header), output) + + # Calculate and save histogram + histogram, _ = np.histogram(final_data, bins=21144, range=(0, 21144)) + + # Exclude the background (region 0) from the histogram + histogram = histogram[1:] + + # Determine what regions are present based on the histogram + present_regions = np.where(histogram > 0)[0] + 1 # Add 1 to account for the background + + # Get R, G, B values for each region + color_map = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'regional_summary.csv') #(Region_ID,ID_Path,Region,Abbr,General_Region,R,G,B) + + # Delete rgba.txt if it exists (used for coloring the regions in DSI Studio) + + if Path(txt_output).exists(): + Path(txt_output).unlink() + + # Determine the RGB color for bars based on the region_id + for region_id in present_regions: + combined_region_id = region_id if region_id < 20000 else region_id - 20000 + region_rgb = color_map[color_map['Region_ID'] == combined_region_id][['R', 'G', 'B']] + + # Convert R, G, B values to space-separated R G B A values (one line per region) + rgba_str = ' '.join(region_rgb.astype(str).values[0]) + ' 255' + + # Save the RGBA values to a .txt file + with open(txt_output, 'a') as f: + f.write(rgba_str + '\n')
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/cluster_summary.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/cluster_summary.html new file mode 100644 index 00000000..586a669f --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/cluster_summary.html @@ -0,0 +1,708 @@ + + + + + + + + + + unravel.cluster_stats.cluster_summary — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.cluster_summary

+#!/usr/bin/env python3
+
+""" 
+Use ``cluster_summary`` from UNRAVEL to aggregate and analyze cluster validation data from ``cluster_validation``.
+
+Usage if running directly after ``cluster_validation``:
+-------------------------------------------------------
+    cluster_summary -c <path/config.ini> -e <exp dir paths> -cvd 'psilocybin_v_saline_tstat1_q<asterisk>' -vd <path/vstats_dir> -sk <path/sample_key.csv> --groups <group1> <group2> -hg <higher_group> -v
+
+Note: 
+    - The current working directory should not have other directories when running this script for the first time. Directories from cluster_org_data are ok though.
+
+Usage if running after ``cluster_validation`` and ``cluster_org_data``:
+-----------------------------------------------------------------------
+    cluster_summary -c <path/config.ini> -sk <path/sample_key.csv> --groups <group1> <group2> -hg <higher_group> -v
+
+Note:
+    - For the second usage, the ``-e``, ``-cvd``, and ``-vd`` arguments are not needed because the data is already in the working directory.
+    - Only process one comparison at a time. If you have multiple comparisons, run this script separately for each comparison in separate directories.
+    - Then aggregate the results as needed (e.g. to make a legend with all relevant abbeviations, copy the .xlsx files to a central location and run ``cluster_legend``).
+
+The current working directory should not have other directories when running this script for the first time. Directories from cluster_org_data are ok though.
+
+``cluster_summary`` runs commands in this order:
+    - ``cluster_org_data``
+    - ``cluster_group_data``
+    - ``utils_prepend``
+    - ``cluster_stats``
+    - ``cluster_index``
+    - ``cluster_brain_model``
+    - ``cluster_table``
+    - ``cluster_prism``
+    - ``cluster_legend``
+
+The sample_key.csv file should have the following format:
+    dir_name,condition
+    sample01,control
+    sample02,treatment
+
+If you need to rerun this script, delete the following directories and files in the current working directory:
+find . -name _valid_clusters -exec rm -rf {} \; -o -name cluster_validation_summary_t-test.csv -exec rm -f {} \; -o -name cluster_validation_summary_tukey.csv -exec rm -f {} \; -o -name 3D_brains -exec rm -rf {} \; -o -name valid_clusters_tables_and_legend -exec rm -rf {} \; -o -name _valid_clusters_stats -exec rm -rf {} \;
+
+If you want to aggregate CSVs for sunburst plots of valid clusters, run this in a root directory:
+find . -name "valid_clusters_sunburst.csv" -exec sh -c 'cp {} ./$(basename $(dirname $(dirname {})))_$(basename {})' \;
+
+Likewise, you can aggregate raw data (raw_data_for_t-test_pooled.csv), stats (t-test_results.csv), and prism files (cell_density_summary_for_valid_clusters.csv). 
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+import subprocess
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.cluster_stats.org_data import cp
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times, load_config
+from unravel.utilities.aggregate_files_w_recursive_search import find_and_copy_files
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-c', '--config', help='Path to the config.ini file. Default: unravel/cluster_stats/cluster_summary.ini', default=Path(__file__).parent / 'cluster_summary.ini', action=SM) + + # cluster_org_data -e <list of experiment directories> -cvd '*' -td <target_dir> -vd <path/vstats_dir> -v + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process. (needed for cluster_org_data)', nargs='*', action=SM) + parser.add_argument('-cvd', '--cluster_val_dirs', help='Glob pattern matching cluster validation output dirs to copy data from (relative to ./sample??/clusters/; for cluster_org_data', action=SM) + parser.add_argument('-vd', '--vstats_path', help='path/vstats_dir (dir vstats was run from) to copy p val, info, and index files (for cluster_org_data)', action=SM) + + # utils_prepend -c <path/sample_key.csv> -f -r + parser.add_argument('-sk', '--sample_key', help='path/sample_key.csv w/ directory names and conditions (for utils_prepend)', action=SM) + + # cluster_stats --groups <group1> <group2> + parser.add_argument('--groups', help='List of group prefixes. 2 groups --> t-test. >2 --> Tukey\'s tests (The first 2 groups reflect the main comparison for validation rates; for cluster_stats)', nargs='+') + parser.add_argument('-cp', '--condition_prefixes', help='Condition prefixes to group related data (optional for cluster_stats)', nargs='*', default=None, action=SM) + parser.add_argument('-hg', '--higher_group', help='Specify the group that is expected to have a higher mean based on the direction of the p value map', required=True) + + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Could add a progress bar that advances after each subdir, but need to adapt running of the first few scripts for this. Include check for completeness (all samples have csvs [from both hemis]). Review outputs and output folders and consider consolidating them. Could make cells vs. labels are arg. Could add a raw data output organized for the SI table. # The valid cluster sunburst could have the val dir name and be copied to a central location +# TODO: Consider moving find_and_copy_files() to unravel/unravel/utils.py +# TODO: Move cell vs. label arg from config back to argparse and make it a required arg to prevent accidentally using the wrong metric +# TODO: Add a reset option to delete all output files and directories from the current working directory +# TODO: Aggregate CSVs for valid cluster sunburst plots +# TODO: Sort the overall valid cluster sunburst csv + +
+[docs] +def run_script(script_name, script_args): + """Run a command/script using subprocess that respects the system's PATH and captures output.""" + # Convert all script arguments to string + script_args = [str(arg) for arg in script_args] + command = [script_name] + script_args + subprocess.run(command, check=True, stdout=None, stderr=None)
+ + +
+[docs] +def main(): + args = parse_args() + + cfg = load_config(args.config) + + # Run cluster_org_data + if args.exp_paths and args.cluster_val_dirs and args.vstats_path: + org_data_args = [ + '-e', *args.exp_paths, + '-p', cfg.org_data.pattern, + '-cvd', args.cluster_val_dirs, + '-vd', args.vstats_path, + '-dt', cfg.org_data.density_type, + '-pvt', cfg.org_data.p_val_txt + ] + if args.verbose: + org_data_args.append('-v') + run_script('cluster_org_data', org_data_args) + + # Run cluster_group_data + if args.verbose: + run_script('cluster_group_data', ['-v']) + else: + run_script('cluster_group_data', []) + + # Run utils_prepend + if args.sample_key: + prepend_conditions_args = [ + '-sk', args.sample_key, + '-f', + '-r' + ] + if args.verbose: + prepend_conditions_args.append('-v') + run_script('utils_prepend', prepend_conditions_args) + + # Run cluster_stats + if args.groups: + stats_args = [ + '--groups', *args.groups, + '-alt', cfg.stats.alternate, + '-pvt', cfg.org_data.p_val_txt, + '-hg', args.higher_group + ] + if args.condition_prefixes: + stats_args.append(['-cp', *args.condition_prefixes]) + if args.verbose: + stats_args.append('-v') + run_script('cluster_stats', stats_args) + + dsi_dir = Path().cwd() / '3D_brains' + dsi_dir.mkdir(parents=True, exist_ok=True) + + # Iterate over all subdirectories in the current working directory and run the following scripts + for subdir in [d for d in Path.cwd().iterdir() if d.is_dir() and d.name != '3D_brains' and d.name != 'valid_clusters_tables_and_legend']: + + # Load all .csv files in the current subdirectory + csv_files = list(subdir.glob('*.csv')) + if not csv_files: + continue # Skip directories with no CSV files + + stats_output = subdir / '_valid_clusters_stats' + valid_clusters_ids_txt = stats_output / 'valid_cluster_IDs_t-test.txt' if len(args.groups) == 2 else stats_output / 'valid_cluster_IDs_tukey.txt' + + if valid_clusters_ids_txt.exists(): + with open(valid_clusters_ids_txt, 'r') as f: + valid_cluster_ids = f.read().split() + + rev_cluster_index_path = subdir / f'{subdir.name}_rev_cluster_index.nii.gz' + if not Path(rev_cluster_index_path).exists(): + rev_cluster_index_path = subdir / f'{subdir.name}_rev_cluster_index_RH.nii.gz' + if not Path(rev_cluster_index_path).exists(): + rev_cluster_index_path = subdir / f'{subdir.name}_rev_cluster_index_LH.nii.gz' + if not Path(rev_cluster_index_path).exists(): + rev_cluster_index_path = next(subdir.glob("*rev_cluster_index*")) + + if rev_cluster_index_path is None: + print(f" No valid cluster index file found in {subdir}. Skipping...") + continue # Skip this directory and move to the next + + valid_clusters_index_dir = subdir / cfg.index.valid_clusters_dir + + if len(valid_cluster_ids) == 0: + print(f" No clusters were valid for {subdir}. Skipping...") + continue + + # Run cluster_index + index_args = [ + '-ci', rev_cluster_index_path, + '-ids', *valid_cluster_ids, + '-vcd', valid_clusters_index_dir, + '-a', cfg.index.atlas + ] + if cfg.index.output_rgb_lut: + index_args.append('-rgb') + if args.verbose: + index_args.append('-v') + run_script('cluster_index', index_args) + + # Run cluster_brain_model + valid_cluster_index_path = valid_clusters_index_dir / str(rev_cluster_index_path.name).replace('.nii.gz', f'_{cfg.index.valid_clusters_dir}.nii.gz') + brain_args = [ + '-i', valid_cluster_index_path, + '-ax', cfg.brain.axis, + '-s', cfg.brain.shift, + '-sa', cfg.brain.split_atlas + ] + if cfg.brain.mirror: + brain_args.append('-m') + if args.verbose: + brain_args.append('-v') + run_script('cluster_brain_model', brain_args) + + # Aggregate files from cluster_brain_model + if cfg.brain.mirror: + find_and_copy_files(f'*{cfg.index.valid_clusters_dir}_ABA_WB.nii.gz', subdir, dsi_dir) + else: + find_and_copy_files(f'*{cfg.index.valid_clusters_dir}_ABA.nii.gz', subdir, dsi_dir) + find_and_copy_files(f'*{cfg.index.valid_clusters_dir}_rgba.txt', subdir, dsi_dir) + + # Run cluster_table + table_args = [ + '-vcd', valid_clusters_index_dir, + '-t', cfg.table.top_regions, + '-pv', cfg.table.percent_vol + ] + if args.verbose: + table_args.append('-v') + run_script('cluster_table', table_args) + find_and_copy_files('*_valid_clusters_table.xlsx', subdir, Path().cwd() / 'valid_clusters_tables_and_legend') + + if Path('valid_clusters_tables_and_legend').exists(): + + # Run cluster_prism + valid_cluster_ids_sorted_txt = valid_clusters_index_dir / 'valid_cluster_IDs_sorted_by_anatomy.txt' + if valid_cluster_ids_sorted_txt.exists(): + with open(valid_cluster_ids_sorted_txt, 'r') as f: + valid_cluster_ids_sorted = f.read().split() + else: + valid_cluster_ids_sorted = valid_cluster_ids + if len(valid_cluster_ids_sorted) > 0: + prism_args = [ + '-ids', *valid_cluster_ids_sorted, + '-p', subdir, + ] + if cfg.prism.save_all: + prism_args.append('-sa') + if args.verbose: + prism_args.append('-v') + run_script('cluster_prism', prism_args) + else: + print(f"\n No valid cluster IDs found for {subdir}. Skipping cluster_prism...\n") + + # Copy the atlas and binarize it for visualization in DSI studio + dest_atlas = dsi_dir / Path(cfg.index.atlas).name + if not dest_atlas.exists(): + cp(cfg.index.atlas, dsi_dir) + atlas_nii = nib.load(dest_atlas) + atlas_img = np.asanyarray(atlas_nii.dataobj, dtype=atlas_nii.header.get_data_dtype()).squeeze() + atlas_img[atlas_img > 0] = 1 + atlas_img.astype(np.uint8) + atlas_nii_bin = nib.Nifti1Image(atlas_img, atlas_nii.affine, atlas_nii.header) + atlas_nii_bin.header.set_data_dtype(np.uint8) + nib.save(atlas_nii_bin, str(dest_atlas).replace('.nii.gz', '_bin.nii.gz')) + + # Run cluster_legend + if Path('valid_clusters_tables_and_legend').exists(): + legend_args = [ + '-p', 'valid_clusters_tables_and_legend' + ] + run_script('cluster_legend', legend_args)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/cluster_validation.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/cluster_validation.html new file mode 100644 index 00000000..68ba8e6e --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/cluster_validation.html @@ -0,0 +1,751 @@ + + + + + + + + + + unravel.cluster_stats.cluster_validation — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.cluster_validation

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_validation`` from UNRAVEL to warp a cluster index from atlas space to tissue space, crop clusters, apply a segmentation mask, and quantify cell/label densities.
+
+Usage:
+------
+    cluster_validation -e <experiment paths> -m <path/rev_cluster_index_to_warp_from_atlas_space.nii.gz> -s cfos_seg_ilastik_1 -v
+
+cluster_index_dir = Path(args.moving_img).name w/o "_rev_cluster_index" and ".nii.gz"
+
+Outputs:
+    - ./sample??/clusters/<cluster_index_dir>/outer_bounds.txt
+    - ./sample??/clusters/<cluster_index_dir>/<args.density>_data.csv
+
+For -s, if a dir name is provided, the command will load ./sample??/seg_dir/sample??_seg_dir.nii.gz. 
+If a relative path is provided, the command will load the image at the specified path.
+
+Next command:
+    ``cluster_summary``
+"""
+
+
+import argparse
+import concurrent.futures
+import cc3d
+import numpy as np
+import os
+import pandas as pd
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.img_io import load_3D_img, load_image_metadata_from_txt, load_nii_subset, resolve_path
+from unravel.core.img_tools import cluster_IDs
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples, print_func_name_args_times
+from unravel.warp.to_native import to_native
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + + # Key args + parser.add_argument('-m', '--moving_img', help='path/*_rev_cluster_index.nii.gz to warp from atlas space', required=True, action=SM) + parser.add_argument('-s', '--seg', help='rel_path/seg_img.nii.gz. 1st glob match processed', required=True, action=SM) + parser.add_argument('-c', '--clusters', help='Clusters to process: all or list of clusters (e.g., 1 3 4). Default: all', nargs='*', default='all', action=SM) + parser.add_argument('-de', '--density', help='Density to measure: cell_density (default) or label_density', default='cell_density', choices=['cell_density', 'label_density'], action=SM) + parser.add_argument('-o', '--output', help='rel_path/clusters_info.csv (Default: clusters/<cluster_index_dir>/cluster_data.csv)', default=None, action=SM) + + # Optional to_native() args + parser.add_argument('-n', '--native_idx', help='Load/save native cluster index from/to rel_path/native_image.zarr (fast) or rel_path/native_image.nii.gz if provided', default=None, action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Fixed input for registration (unravel.register.reg). Default: autofl_50um_masked_fixed_reg_input.nii.gz', default="autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-inp', '--interpol', help='Interpolator for ants.apply_transforms (nearestNeighbor [default], multiLabel [slow])', default="nearestNeighbor", action=SM) + parser.add_argument('-ro', '--reg_outputs', help="Name of folder w/ outputs from unravel.register.reg (e.g., transforms). Default: reg_outputs", default="reg_outputs", action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of registration inputs in microns. Default: 50', default='50',type=int, action=SM) + parser.add_argument('-md', '--metadata', help='path/metadata.txt. Default: parameters/metadata.txt', default="parameters/metadata.txt", action=SM) + parser.add_argument('-zo', '--zoom_order', help='SciPy zoom order for scaling to full res. Default: 0 (nearest-neighbor)', default='0',type=int, action=SM) + parser.add_argument('-mi', '--miracl', help='Mode for compatibility (accounts for tif to nii reorienting)', action='store_true', default=False) + + # Optional arg for count_cells() + parser.add_argument('-cc', '--connect', help='Connected component connectivity (6, 18, or 26). Default: 6', type=int, default=6, action=SM) + + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: QC. Aggregate .csv results for all samples if args.exp_dirs, script to load image subset. +# TODO: Make config file for defaults or a command_generator.py script +# TODO: Consider adding an option to quantify mean IF intensity in each cluster in segmented voxels. Also make a script for mean IF intensity in clusters in atlas space. + +
+[docs] +@print_func_name_args_times() +def crop_outer_space(native_cluster_index, output_path): + """Crop outer space around all clusters and save bounding box to .txt file (outer_bounds.txt) + Return cropped native_cluster_index, outer_xmin, outer_xmax, outer_ymin, outer_ymax, outer_zmin, outer_zmax""" + + # Create boolean arrays indicating presence of clusters along each axis + presence_x = np.any(native_cluster_index, axis=(1, 2)) + presence_y = np.any(native_cluster_index, axis=(0, 2)) + presence_z = np.any(native_cluster_index, axis=(0, 1)) + + # Use np.argmax on presence arrays to find first occurrence of clusters + # For max, reverse the array, use np.argmax, and subtract from the length + outer_xmin, outer_xmax = np.argmax(presence_x), len(presence_x) - np.argmax(presence_x[::-1]) + outer_ymin, outer_ymax = np.argmax(presence_y), len(presence_y) - np.argmax(presence_y[::-1]) + outer_zmin, outer_zmax = np.argmax(presence_z), len(presence_z) - np.argmax(presence_z[::-1]) + + # Adjust the max bounds to include the last slice where the cluster is present + outer_xmax += 1 + outer_ymax += 1 + outer_zmax += 1 + + # Crop the native_cluster_index to the bounding box + native_cluster_index_cropped = native_cluster_index[outer_xmin:outer_xmax, outer_ymin:outer_ymax, outer_zmin:outer_zmax] + + # Save the bounding box to a file + with open(f"{output_path.parent}/outer_bounds.txt", "w") as file: + file.write(f"{outer_xmin}:{outer_xmax}, {outer_ymin}:{outer_ymax}, {outer_zmin}:{outer_zmax}") + + return native_cluster_index_cropped, outer_xmin, outer_xmax, outer_ymin, outer_ymax, outer_zmin, outer_zmax
+ + +
+[docs] +def cluster_bbox(cluster_ID, native_cluster_index_cropped): + """Get bounding box for the current cluster. Return cluster_ID, xmin, xmax, ymin, ymax, zmin, zmax.""" + cluster_mask = native_cluster_index_cropped == cluster_ID + presence_x = np.any(cluster_mask, axis=(1, 2)) + presence_y = np.any(cluster_mask, axis=(0, 2)) + presence_z = np.any(cluster_mask, axis=(0, 1)) + + xmin, xmax = np.argmax(presence_x), len(presence_x) - np.argmax(presence_x[::-1]) + ymin, ymax = np.argmax(presence_y), len(presence_y) - np.argmax(presence_y[::-1]) + zmin, zmax = np.argmax(presence_z), len(presence_z) - np.argmax(presence_z[::-1]) + + return cluster_ID, xmin, xmax, ymin, ymax, zmin, zmax
+ + +
+[docs] +@print_func_name_args_times() +def cluster_bbox_parallel(native_cluster_index_cropped, clusters): + """Get bounding boxes for each cluster in parallel. Return list of results.""" + results = [] + num_cores = os.cpu_count() # This is good for CPU-bound tasks. Could try 2 * num_cores + 1 for io-bound tasks + workers = min(num_cores, len(clusters)) + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: + future_to_cluster = {executor.submit(cluster_bbox, cluster_ID, native_cluster_index_cropped): cluster_ID for cluster_ID in clusters} + for future in concurrent.futures.as_completed(future_to_cluster): + cluster_ID = future_to_cluster[future] + try: + result = future.result() + results.append(result) + except Exception as exc: + print(f'Cluster {cluster_ID} generated an exception: {exc}') + return results
+ + +
+[docs] +def count_cells(seg_in_cluster, connectivity=6): + """Count cells (objects) in each cluster using connected-components-3d + Return the number of cells in the cluster.""" + + # If the data is big-endian, convert it to little-endian + if seg_in_cluster.dtype.byteorder == '>': + seg_in_cluster = seg_in_cluster.byteswap().newbyteorder() + seg_in_cluster = seg_in_cluster.astype(np.uint8) + + # Count the number of cells in the cluster + labels_out, n = cc3d.connected_components(seg_in_cluster, connectivity=connectivity, out_dtype=np.uint32, return_N=True) + + return n
+ + +
+[docs] +def density_in_cluster(cluster_data, native_cluster_index_cropped, seg_cropped, xy_res, z_res, connectivity=6, density='cell_count'): + """Measure cell count or volume of segmented voxels in the current cluster. + For cell densities, return: cluster_ID, cell_count, cluster_volume_in_cubic_mm, cell_density, xmin, xmax, ymin, ymax, zmin, zmax + For label densities, return: cluster_ID, seg_volume_in_cubic_mm, cluster_volume_in_cubic_mm, label_density, xmin, xmax, ymin, ymax, zmin, zmax. + """ + cluster_ID, xmin, xmax, ymin, ymax, zmin, zmax = cluster_data + + # Crop the cluster from the native cluster index + cropped_cluster = native_cluster_index_cropped[xmin:xmax, ymin:ymax, zmin:zmax] + + # Crop the segmentation image for the current cluster + seg_in_cluster = seg_cropped[xmin:xmax, ymin:ymax, zmin:zmax] + + # Zero out segmented voxels outside of the current cluster + seg_in_cluster[cropped_cluster == 0] = 0 + + # Measure cluster volume + cluster_volume_in_cubic_mm = ((xy_res**2) * z_res) * np.count_nonzero(cropped_cluster) / 1e9 + + # Count cells or measure the volume of segmented voxels + if density == "cell_density": + cell_count = count_cells(seg_in_cluster, connectivity=connectivity) + cell_density = cell_count / cluster_volume_in_cubic_mm + return cluster_ID, cell_count, cluster_volume_in_cubic_mm, cell_density, xmin, xmax, ymin, ymax, zmin, zmax + else: + seg_volume_in_cubic_mm = ((xy_res**2) * z_res) * np.count_nonzero(seg_in_cluster) / 1e9 + label_density = seg_volume_in_cubic_mm / cluster_volume_in_cubic_mm * 100 + return cluster_ID, seg_volume_in_cubic_mm, cluster_volume_in_cubic_mm, label_density, xmin, xmax, ymin, ymax, zmin, zmax
+ + +
+[docs] +@print_func_name_args_times() +def density_in_cluster_parallel(cluster_bbox_results, native_cluster_index_cropped, seg_cropped, xy_res, z_res, connectivity=6, density='cell_count'): + """Measure cell count or volume of segmented voxels in each cluster in parallel. Return list of results.""" + results = [] + num_cores = os.cpu_count() + workers = min(num_cores, len(cluster_bbox_results)) + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: + future_to_cluster = {executor.submit(density_in_cluster, cluster_data, native_cluster_index_cropped, seg_cropped, xy_res, z_res, connectivity, density): cluster_data[0] for cluster_data in cluster_bbox_results} # cluster_data[0] is the cluster_ID + for future in concurrent.futures.as_completed(future_to_cluster): + cluster_ID = future_to_cluster[future] + try: + result = future.result() + results.append(result) + except Exception as exc: + print(f'Cluster {cluster_ID} generated an exception: {exc}') + return results
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define final output and check if it exists + cluster_index_dir = str(Path(args.moving_img).name).replace(".nii.gz", "").replace("_rev_cluster_index_", "_") + if args.output: + output_path = resolve_path(sample_path, args.output) + else: + output_path = resolve_path(sample_path, Path("clusters", cluster_index_dir, f"{args.density}_data.csv"), make_parents=True) + if output_path and output_path.exists(): + print(f"\n\n {output_path} already exists. Skipping.\n") + continue + + # Use lower bit-depth possible for cluster index + rev_cluster_index = load_3D_img(args.moving_img) + + # Define paths relative to sample?? folder + native_idx_path = resolve_path(sample_path, args.native_idx) if args.native_idx else None + + # Load cluster index and convert to ndarray + if args.native_idx and Path(args.native_idx).exists(): + native_cluster_index = load_3D_img(Path(args.native_idx).exists()) + else: + fixed_reg_input = Path(sample_path, args.reg_outputs, args.fixed_reg_in) + if not fixed_reg_input.exists(): + fixed_reg_input = sample_path / args.reg_outputs / "autofl_50um_fixed_reg_input.nii.gz" + native_cluster_index = to_native(sample_path, args.reg_outputs, fixed_reg_input, args.moving_img, args.metadata, args.reg_res, args.miracl, args.zoom_order, args.interpol, output=native_idx_path) + + # Get clusters to process + if args.clusters == "all": + clusters = cluster_IDs(rev_cluster_index) + else: + clusters = args.clusters + clusters = [int(cluster) for cluster in clusters] + + # Crop outer space around all clusters + native_cluster_index_cropped, outer_xmin, outer_xmax, outer_ymin, outer_ymax, outer_zmin, outer_zmax = crop_outer_space(native_cluster_index, output_path) + + # Load image metadata from .txt + metadata_path = resolve_path(sample_path, args.metadata) + xy_res, z_res, _, _, _ = load_image_metadata_from_txt(metadata_path) + if xy_res is None or z_res is None: + print(" [red bold]./sample??/parameters/metadata.txt missing. cd to sample?? dir and run: io_metadata") + + # Get bounding boxes for each cluster in parallel + cluster_bbox_data = cluster_bbox_parallel(native_cluster_index_cropped, clusters) + + # Load the segmentation image and crop it to the outer bounds of all clusters + seg_path = next(sample_path.glob(str(args.seg)), None) + if seg_path is None: + print(f"\n [red bold]No files match the pattern {args.seg} in {sample_path}\n") + continue + seg_cropped = load_nii_subset(seg_path, outer_xmin, outer_xmax, outer_ymin, outer_ymax, outer_zmin, outer_zmax) + + # Process each cluster to count cells or measure volume, in parallel + cluster_data_results = density_in_cluster_parallel(cluster_bbox_data, native_cluster_index_cropped, seg_cropped, xy_res, z_res, args.connect, args.density) + + # Process cluster_data_results to save to CSV or perform further analysis + data_list = [] + for result in cluster_data_results: + cluster_ID, cell_count_or_seg_vol, cluster_volume_in_cubic_mm, density_measure, xmin, xmax, ymin, ymax, zmin, zmax = result + + # Determine the appropriate headers based on the density measure type + if args.density == "cell_density": + count_or_vol_header, density_header = "cell_count", "cell_density" + else: + count_or_vol_header, density_header = "label_volume", "label_density" + + # Prepare the data dictionary + data = { + "sample": sample_path.name, + "cluster_ID": cluster_ID, + count_or_vol_header: cell_count_or_seg_vol, + "cluster_volume": cluster_volume_in_cubic_mm, + density_header: density_measure, + "xmin": xmin, "xmax": xmax, "ymin": ymin, "ymax": ymax, "zmin": zmin, "zmax": zmax + } + + data_list.append(data) + + # Create a DataFrame from the list of data dictionaries + df = pd.DataFrame(data_list) + + # Sort the DataFrame by 'cluster_ID' in ascending order + df_sorted = df.sort_values(by='cluster_ID', ascending=True) + + # Save the sorted DataFrame to the CSV file + df_sorted.to_csv(output_path, index=False) + print(f"\n Output: [default bold]{output_path}") + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/crop.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/crop.html new file mode 100644 index 00000000..7416ff46 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/crop.html @@ -0,0 +1,520 @@ + + + + + + + + + + unravel.cluster_stats.crop — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.crop

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_crop`` from UNRAVEL to load image, load bounding box, crop cluster, and save as .nii.gz.
+
+Usage
+-----
+    cluster_crop -i path/img.nii.gz -b path/bbox.txt -o path/output_img.nii.gz -a -x $XY -z $Z -v
+"""
+
+import argparse
+from rich.traceback import install
+from rich import print
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_nii
+from unravel.core.img_tools import find_bounding_box, cluster_IDs, crop
+from unravel.core.utils import print_cmd_and_times, load_text_from_file
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.czi, path/img.nii.gz, or path/tif_dir', action=SM) + parser.add_argument('-o', '--output', help='path/output_img.nii.gz', action=SM) + parser.add_argument('-b', '--bbox', help='path/bbox.txt', action=SM) + parser.add_argument('-c', '--cluster', help='Cluster intensity to get bbox and crop', action=SM) + parser.add_argument('-a', '--all_clusters', help='Crop each cluster. Default: False', action='store_true', default=False) + parser.add_argument('-x', '--xy_res', help='xy voxel size in microns for the raw data', type=float, action=SM) + parser.add_argument('-z', '--z_res', help='z voxel size in microns for the raw data', type=float, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def save_cropped_img(img_cropped, xy_res, z_res, args, cluster=None): + if args.output: + save_path = args.output + elif args.bbox: + save_path = args.input.replace('.nii.gz', f'_cropped.nii.gz') + elif cluster is not None: + save_path = args.input.replace('.nii.gz', f'_cluster{cluster}_cropped.nii.gz') + else: + print(" [red1]No output specified. Exiting.") + exit() + + if max(img_cropped.flatten()) < 255: + save_as_nii(img_cropped, save_path, xy_res, z_res, data_type='uint8') + else: + save_as_nii(img_cropped, save_path, xy_res, z_res, data_type='uint16')
+ + +
+[docs] +def main(): + args = parse_args() + + if args.xy_res is None or args.z_res is None: + img, xy_res, z_res = load_3D_img(args.input, return_res=True) + else: + img = load_3D_img(args.input, return_res=True) + xy_res, z_res = args.xy_res, args.z_res + + + # Crop image + if args.bbox: + bbox = load_text_from_file(args.bbox) + img_cropped = crop(img, bbox) + save_cropped_img(img_cropped, xy_res, z_res, args) + elif args.cluster: + xmin, xmax, ymin, ymax, zmin, zmax = find_bounding_box(img, cluster_ID=args.cluster) + bbox_str = f"{xmin}:{xmax}, {ymin}:{ymax}, {zmin}:{zmax}" + img_cropped = crop(img, bbox_str) + save_cropped_img(img_cropped, xy_res, z_res, args, cluster=args.cluster) + elif args.all_clusters: + clusters = cluster_IDs(img, min_extent=1, print_IDs=False, print_sizes=False) + for cluster in clusters: + xmin, xmax, ymin, ymax, zmin, zmax = find_bounding_box(img, cluster_ID=cluster) + bbox_str = f"{xmin}:{xmax}, {ymin}:{ymax}, {zmin}:{zmax}" + img_cropped = crop(img, bbox_str) + save_cropped_img(img_cropped, xy_res, z_res, args, cluster=cluster) + else: + print(" [red1]No bbox or cluster specified. Exiting.") + exit()
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes.html new file mode 100644 index 00000000..884aaead --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes.html @@ -0,0 +1,603 @@ + + + + + + + + + + unravel.cluster_stats.effect_sizes.effect_sizes — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.effect_sizes.effect_sizes

+#!/usr/bin/env python3
+
+"""
+Use ``effect_sizes`` from UNRAVEL to calculate the effect size for a comparison between two groups for each cluster [or a valid cluster list].
+
+Usage
+-----
+    effect_sizes -i densities.csv -c1 saline -c2 psilocybin
+
+Inputs:
+    - CSV with densities (Columns: Samples, Conditions, Cluster_1, Cluster_2, ...)
+
+Arguments:
+    - -c1 and -c2 should match the condition name in the Conditions column of the input CSV or be a prefix of the condition name.
+
+Outputs CSV w/ the effect size and CI for each cluster:
+    <input>_Hedges_g_<condition_1>_<condition_2>.csv
+
+If -c is used, outputs a CSV with the effect sizes and CI for valid clusters:
+    <input>_Hedges_g_<condition_1>_<condition_2>_valid_clusters.csv
+
+The effect size is calculated as the unbiased Hedge\'s g effect sizes (corrected for sample size): 
+    Hedges' g = ((c2-c1)/spooled*corr_factor)
+    CI = Hedges' g +/- t * SE
+    0.2 - 0.5 = small effect; 0.5 - 0.8 = medium; 0.8+ = large
+"""
+
+import argparse
+import os
+import pandas as pd
+from scipy.stats import t
+from termcolor import colored
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input_csv', help='CSV with densities (Columns: Samples, Conditions, Cluster_1, Cluster_2, ...)', action=SM) + parser.add_argument('-c1', '--condition_1', help='First condition of interest from csv (e.g., saline [data matching prefix pooled])', action=SM) + parser.add_argument('-c2', '--condition_2', help='Second condition (e.g, psilocybin [data matching prefix pooled])', action=SM) + parser.add_argument('-c', '--clusters', help='Space separated list of valid cluster IDs (default: process all clusters)', default=None, nargs='*', type=int, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +# Create a condition selector to handle pooling of data +
+[docs] +def condition_selector(df, condition, unique_conditions, condition_column='Conditions'): + """Create a condition selector to handle pooling of data in a DataFrame based on specified conditions. + This function checks if the 'condition' is exactly present in the 'Conditions' column or is a prefix of any condition in this column. + If the exact condition is found, it selects those rows. + If the condition is a prefix (e.g., 'saline' matches 'saline-1', 'saline-2'), it selects all rows where the 'Conditions' column starts with this prefix. + An error is raised if the condition is neither found as an exact match nor as a prefix. + + Args: + df (pd.DataFrame): DataFrame whose 'Conditions' column contains the conditions of interest. + condition (str): The condition or prefix of interest. + unique_conditions (list): List of unique conditions in the 'Conditions' column to validate against. + + Returns: + pd.Series: A boolean Series to select rows based on the condition.""" + + if condition in unique_conditions: + return (df[condition_column] == condition) + elif any(cond.startswith(condition) for cond in unique_conditions): + return df[condition_column].str.startswith(condition) + else: + raise ValueError(colored(f"Condition {condition} not recognized!", 'red'))
+ + +# Calculate the effect size for each cluster +
+[docs] +def hedges_g(df, condition_1, condition_2): + + df = pd.read_csv(df) + cluster_columns = [col for col in df if col.startswith('Cluster')] + + # Create a list of unique values in 'Conditions' + unique_conditions = df['Conditions'].unique().tolist() + + # Adjust condition selectors based on potential pooling + cond1_selector = condition_selector(df, condition_1, unique_conditions) + cond2_selector = condition_selector(df, condition_2, unique_conditions) + + # Update data selection using the modified condition selectors + mean1 = df[cond1_selector][cluster_columns].mean() + std1 = df[cond1_selector][cluster_columns].std() + count1 = df[cond1_selector][cluster_columns].count() + + mean2 = df[cond2_selector][cluster_columns].mean() + std2 = df[cond2_selector][cluster_columns].std() + count2 = df[cond2_selector][cluster_columns].count() + + # Calculate pooled standard deviation for each cluster + spooled = ((count1 - 1) * std1**2 + (count2 - 1) * std2**2) / (count1 + count2 - 2) + spooled = spooled ** 0.5 + + # Calculate Hedges' g + g = (mean2 - mean1) / spooled + + # Calculate the correction factor + correction_factor = 1 - 3 / (4 * (count1 + count2) - 9) + + # Apply the correction factor to Hedges' g + d = g * correction_factor + + # Calculate the standard error of Hedges' g + se = (((count1 + count2) / (count1 * count2)) + ((d**2) / (2 * (count1 + count2 - 2)))) ** 0.5 + + # For a two-tailed t-test, you want the critical value for alpha/2 in one tail + alpha = 0.05 + degrees_of_freedom = (count1 + count2) - 2 + + # Get the critical t-value for alpha/2 + critical_t_value = t.ppf(1 - alpha/2, degrees_of_freedom) + + # Calculate the confidence interval + ci = critical_t_value * se + + # Calculate the lower and upper bounds of the confidence interval + lower = d - ci + upper = d + ci + + # Create a dataframe combining Hedges' g, lower and upper CIs (organized for plotting w/ Prism --> Grouped data) + results_df = pd.DataFrame({ + 'Cluster': cluster_columns, + 'Hedges_g': d, + 'Upper_Limit': upper, + 'Lower_Limit': lower, + }) + + # Replace "Cluster_" with an empty string in the "Cluster" column + results_df['Cluster'] = results_df['Cluster'].str.replace('Cluster_', '') + + # Reverse the order of the rows (so that the first cluster is at the top when graphed in Prism) + results_df = results_df.iloc[::-1] + + return results_df
+ + +
+[docs] +def filter_dataframe(df, cluster_list): + # If no clusters provided, return the original DataFrame + if cluster_list is None: + return df + + # Keep only rows where 'Cluster' value after removing "Cluster_" matches an integer in the cluster list + return df[df['Cluster'].str.replace('Cluster_', '').astype(int).isin(cluster_list)]
+ + + +
+[docs] +def main(): + args = parse_args() + + # Generate CSVs with effect sizes + effect_sizes = hedges_g(args.input_csv, args.condition_1, args.condition_2) + output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}.csv" + effect_sizes.to_csv(output, index=False) + + if args.clusters is not None: + # Filter DataFrame based on valid clusters list + effect_sizes_filtered = filter_dataframe(effect_sizes, args.clusters) + filtered_output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_valid_clusters.csv" + effect_sizes_filtered.to_csv(filtered_output, index=False)
+ + + +if __name__ == '__main__': + main() + +# Effect size calculations described in the supplemental information: https://pubmed.ncbi.nlm.nih.gov/37248402/ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.html new file mode 100644 index 00000000..a4bcc095 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.html @@ -0,0 +1,606 @@ + + + + + + + + + + unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute

+#!/usr/bin/env python3
+
+"""
+Use ``effect_sizes_sex_abs`` from UNRAVEL to calculate the effect size for a comparison between two groups for each cluster [or a valid cluster list].
+    
+Usage:
+    effect_sizes_sex_abs -i densities.csv -c1 saline -c2 psilocybin
+
+Inputs:
+    - CSV with densities (Columns: Samples, Sex, Conditions, Cluster_1, Cluster_2, ...)
+    - Enter M or F in the Sex column.
+
+Arguments:
+    - -c1 and -c2 should match the condition name in the Conditions column of the input CSV or be a prefix of the condition name.
+
+Outputs CSV w/ the effect size and CI for each cluster:
+    <input>_Hedges_g_<condition_1>_<condition_2>.csv
+
+If -c is used, outputs a CSV with the effect sizes and CI for valid clusters:
+    <input>_Hedges_g_<condition_1>_<condition_2>_valid_clusters.csv
+
+The effect size is calculated as the unbiased Hedge\'s g effect sizes (corrected for sample size): 
+    Hedges' g = ((c2-c1)/spooled*corr_factor)
+    CI = Hedges' g +/- t * SE
+    0.2 - 0.5 = small effect; 0.5 - 0.8 = medium; 0.8+ = large
+"""
+
+import argparse
+import os
+import pandas as pd
+from scipy.stats import t
+from termcolor import colored
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input_csv', help='CSV with densities (Columns: Samples, Sex, Conditions, Cluster_1, Cluster_2, ...)', action=SM) + parser.add_argument('-c1', '--condition_1', help='First condition of interest from csv (e.g., saline [data matching prefix pooled])', action=SM) + parser.add_argument('-c2', '--condition_2', help='Second condition (e.g, psilocybin [data matching prefix pooled])', action=SM) + parser.add_argument('-c', '--clusters', help='Space separated list of valid cluster IDs (default: process all clusters)', default=None, nargs='*', type=int, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def condition_selector(df, condition, unique_conditions, condition_column='Conditions'): + """Create a condition selector to handle pooling of data in a DataFrame based on specified conditions. + This function checks if the 'condition' is exactly present in the 'Conditions' column or is a prefix of any condition in this column. + If the exact condition is found, it selects those rows. + If the condition is a prefix (e.g., 'saline' matches 'saline-1', 'saline-2'), it selects all rows where the 'Conditions' column starts with this prefix. + An error is raised if the condition is neither found as an exact match nor as a prefix. + + Args: + df (pd.DataFrame): DataFrame whose 'Conditions' column contains the conditions of interest. + condition (str): The condition or prefix of interest. + unique_conditions (list): List of unique conditions in the 'Conditions' column to validate against. + + Returns: + pd.Series: A boolean Series to select rows based on the condition.""" + + if condition in unique_conditions: + return (df[condition_column] == condition) + elif any(cond.startswith(condition) for cond in unique_conditions): + return df[condition_column].str.startswith(condition) + else: + raise ValueError(colored(f"Condition {condition} not recognized!", 'red'))
+ + +
+[docs] +def filter_dataframe(df, cluster_list): + # If no clusters provided, return the original DataFrame + if cluster_list is None: + return df + + # Keep only rows where 'Cluster' value after removing "Cluster_" matches an integer in the cluster list + return df[df['Cluster'].str.replace('Cluster_', '').astype(int).isin(cluster_list)]
+ + +# Calculate the effect size for each cluster and sex +
+[docs] +def hedges_g(df, condition_1, condition_2, sex): + + df = pd.read_csv(df) + cluster_columns = [col for col in df if col.startswith('Cluster')] + + # Create a list of unique values in 'Conditions' + unique_conditions = df['Conditions'].unique().tolist() + + # Adjust condition selectors based on potential pooling + cond1_selector = condition_selector(df, condition_1, unique_conditions) + cond2_selector = condition_selector(df, condition_2, unique_conditions) + + # Update data selection using the modified condition selectors + mean1 = df[(df['Sex'] == sex) & cond1_selector][cluster_columns].mean() + std1 = df[(df['Sex'] == sex) & cond1_selector][cluster_columns].std() + count1 = df[(df['Sex'] == sex) & cond1_selector][cluster_columns].count() + + mean2 = df[(df['Sex'] == sex) & cond2_selector][cluster_columns].mean() + std2 = df[(df['Sex'] == sex) & cond2_selector][cluster_columns].std() + count2 = df[(df['Sex'] == sex) & cond2_selector][cluster_columns].count() + + # Calculate pooled standard deviation for each cluster + spooled = ((count1 - 1) * std1**2 + (count2 - 1) * std2**2) / (count1 + count2 - 2) + spooled = spooled ** 0.5 + + # Calculate Hedges' g + g = (mean2 - mean1) / spooled + + # Calculate the correction factor + correction_factor = 1 - 3 / (4 * (count1 + count2) - 9) + + # Apply the correction factor to Hedges' g + d = g * correction_factor + + # Calculate the standard error of Hedges' g + se = (((count1 + count2) / (count1 * count2)) + ((d**2) / (2 * (count1 + count2 - 2)))) ** 0.5 + + # For a two-tailed t-test, you want the critical value for alpha/2 in one tail + alpha = 0.05 + degrees_of_freedom = (count1 + count2) - 2 + + # Get the critical t-value for alpha/2 + critical_t_value = t.ppf(1 - alpha/2, degrees_of_freedom) + + # Calculate the confidence interval + ci = critical_t_value * se + + # Calculate the lower and upper bounds of the confidence interval + lower = d - ci + upper = d + ci + + # Create a dataframe combining Hedges' g, lower and upper CIs (organized for plotting w/ Prism --> Grouped data) + results_df = pd.DataFrame({ + 'Cluster': cluster_columns, + 'Hedges_g': d, + 'Upper_Limit': upper, + 'Lower_Limit': lower, + }) + + # Replace "Cluster_" with an empty string in the "Cluster" column + results_df['Cluster'] = results_df['Cluster'].str.replace('Cluster_', '') + + # Reverse the order of the rows (so that the first cluster is at the top when graphed in Prism) + results_df = results_df.iloc[::-1] + + return results_df
+ + +
+[docs] +def main(): + args = parse_args() + + # Generate CSVs with absolute sex effect sizes + female_effect_sizes = hedges_g(args.input_csv, args.condition_1, args.condition_2, 'F') + f_output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_F.csv" + female_effect_sizes.to_csv(f_output, index=False) + + male_effect_sizes = hedges_g(args.input_csv, args.condition_1, args.condition_2, 'M') + m_output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_M.csv" + male_effect_sizes.to_csv(m_output, index=False) + + if args.clusters is not None: + # Filter DataFrame based on valid clusters list + female_effect_sizes_filtered = filter_dataframe(female_effect_sizes, args.clusters) + filtered_f_output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_F_valid_clusters.csv" + female_effect_sizes_filtered.to_csv(filtered_f_output, index=False) + + male_effect_sizes_filtered = filter_dataframe(male_effect_sizes, args.clusters) + filtered_m_output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_M_valid_clusters.csv" + male_effect_sizes_filtered.to_csv(filtered_m_output, index=False)
+ + +if __name__ == '__main__': + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.html new file mode 100644 index 00000000..7fb97176 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.html @@ -0,0 +1,619 @@ + + + + + + + + + + unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative

+#!/usr/bin/env python3
+
+"""
+Use ``effect_sizes_sex_rel`` from UNRAVEL to calculate the relative effect size for (comparing sexes) for a comparison between two groups for each cluster [or a valid cluster list].
+    
+Usage
+-----
+    effect_sizes_sex_rel -i densities.csv -c1 saline -c2 psilocybin
+
+Inputs:
+    - CSV with densities (Columns: Samples, Sex, Conditions, Cluster_1, Cluster_2, ...)
+    - Enter M or F in the Sex column.
+
+Arguments:
+    - -c1 and -c2 should match the condition name in the Conditions column of the input CSV or be a prefix of the condition name.
+    
+Outputs CSV w/ the relative effect size (F>M) and CI for each cluster:
+    <input>_Hedges_g_<condition_1>_<condition_2>_F_gt_M.csv
+
+If -c is used, outputs a CSV with the effect sizes and CI for valid clusters:
+    <input>_Hedges_g_<condition_1>_<condition_2>_F_gt_M_valid_clusters.csv
+
+The effect size is calculated as the unbiased Hedge\'s g effect sizes (corrected for sample size): 
+    Hedges' g = ((c2-c1)/spooled*corr_factor)
+    CI = Hedges' g +/- t * SE
+    0.2 - 0.5 = small effect; 0.5 - 0.8 = medium; 0.8+ = large
+"""
+
+import argparse
+import os
+import pandas as pd
+from scipy.stats import t
+from termcolor import colored
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input_csv', help='CSV with densities (Columns: Samples, Sex, Conditions, Cluster_1, Cluster_2, ...)', action=SM) + parser.add_argument('-c1', '--condition_1', help='First condition of interest from csv (e.g., saline [data matching prefix pooled])', action=SM) + parser.add_argument('-c2', '--condition_2', help='Second condition (e.g, psilocybin [data matching prefix pooled])', action=SM) + parser.add_argument('-c', '--clusters', help='Space separated list of valid cluster IDs (default: process all clusters)', default=None, nargs='*', type=int, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def condition_selector(df, condition, unique_conditions, condition_column='Conditions'): + """Create a condition selector to handle pooling of data in a DataFrame based on specified conditions. + This function checks if the 'condition' is exactly present in the 'Conditions' column or is a prefix of any condition in this column. + If the exact condition is found, it selects those rows. + If the condition is a prefix (e.g., 'saline' matches 'saline-1', 'saline-2'), it selects all rows where the 'Conditions' column starts with this prefix. + An error is raised if the condition is neither found as an exact match nor as a prefix. + + Args: + df (pd.DataFrame): DataFrame whose 'Conditions' column contains the conditions of interest. + condition (str): The condition or prefix of interest. + unique_conditions (list): List of unique conditions in the 'Conditions' column to validate against. + + Returns: + pd.Series: A boolean Series to select rows based on the condition.""" + + if condition in unique_conditions: + return (df[condition_column] == condition) + elif any(cond.startswith(condition) for cond in unique_conditions): + return df[condition_column].str.startswith(condition) + else: + raise ValueError(colored(f"Condition {condition} not recognized!", 'red'))
+ + +
+[docs] +def filter_dataframe(df, cluster_list): + # If no clusters provided, return the original DataFrame + if cluster_list is None: + return df + + # Keep only rows where 'Cluster' value after removing "Cluster_" matches an integer in the cluster list + return df[df['Cluster'].str.replace('Cluster_', '').astype(int).isin(cluster_list)]
+ + +# Create a series with the mean, std, and count for each cluster +
+[docs] +def mean_std_count(df, sex, selector, cluster_columns): + mean = df[(df['Sex'] == sex) & selector][cluster_columns].mean() + std = df[(df['Sex'] == sex) & selector][cluster_columns].std() + count = df[(df['Sex'] == sex) & selector][cluster_columns].count() + return mean, std, count
+ + +# Calculate the relative effect size between sexes for each cluster +
+[docs] +def relative_hedges_g(df, condition_1, condition_2): + + df = pd.read_csv(df) + cluster_columns = [col for col in df if col.startswith('Cluster')] + + # Create a list of unique values in 'Conditions' + unique_conditions = df['Conditions'].unique().tolist() + + # Adjust condition selectors based on potential pooling + cond1_selector = condition_selector(df, condition_1, unique_conditions) + cond2_selector = condition_selector(df, condition_2, unique_conditions) + + # Create a series with the mean, std, and count for each cluster + mean_F_cond1, std_F_cond1, count_F_cond1 = mean_std_count(df, 'F', cond1_selector, cluster_columns) + mean_M_cond1, std_M_cond1, count_M_cond1 = mean_std_count(df, 'M', cond1_selector, cluster_columns) + mean_F_cond2, std_F_cond2, count_F_cond2 = mean_std_count(df, 'F', cond2_selector, cluster_columns) + mean_M_cond2, std_M_cond2, count_M_cond2 = mean_std_count(df, 'M', cond2_selector, cluster_columns) + + # Calculate the N for each sex + n_F = count_F_cond1 + count_F_cond2 + n_M = count_M_cond1 + count_M_cond2 + + # Calulate the standard deviation of F_diff and M_diff + std_F_diff = (std_F_cond1**2 + std_F_cond2**2)**0.5 + std_M_diff = (std_M_cond1**2 + std_M_cond2**2)**0.5 + + # Calculate pooled standard deviation for each cluster + spooled = (((n_F -1) * std_F_diff**2 + (n_M -1) * std_M_diff**2)/(n_F + n_M - 2))**0.5 + + # Calculate the mean difference between conditions + F_diff = mean_F_cond2 - mean_F_cond1 + M_diff = mean_M_cond2 - mean_M_cond1 + + # Calculate Hedges' g + g = (F_diff - M_diff) / spooled + + # Calculate the correction factor + correction_factor = 1 - 3 / (4 * (n_F + n_M) - 9) + + # Apply the correction factor to Hedges' g + d = g * correction_factor + + # Calculate the standard error of Hedges' g + se = (((n_F + n_M) / (n_F * n_M)) + ((d**2) / (2 * (n_F + n_M - 2)))) ** 0.5 + + # For a two-tailed t-test, you want the critical value for alpha/2 in one tail + alpha = 0.05 + degrees_of_freedom = (n_F + n_M) - 2 + + # Get the critical t-value for alpha/2 + critical_t_value = t.ppf(1 - alpha/2, degrees_of_freedom) + + # Calculate the confidence interval + ci = critical_t_value * se + + # Calculate the lower and upper bounds of the confidence interval + lower = d - ci + upper = d + ci + + # Create a dataframe combining Hedges' g, lower and upper CIs (organized for plotting w/ Prism --> Grouped data) + results_df = pd.DataFrame({ + 'Cluster': cluster_columns, + 'Hedges_g': d, + 'Upper_Limit': upper, + 'Lower_Limit': lower, + }) + + # Replace "Cluster_" with an empty string in the "Cluster" column + results_df['Cluster'] = results_df['Cluster'].str.replace('Cluster_', '') + + # Reverse the order of the rows (so that the first cluster is at the top when graphed in Prism) + results_df = results_df.iloc[::-1] + + return results_df
+ + +
+[docs] +def main(): + args = parse_args() + + # Generate CSVs with relative sex effect sizes + f_gt_m_effect_sizes = relative_hedges_g(args.input_csv, args.condition_1, args.condition_2) + output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_F_gt_M.csv" + f_gt_m_effect_sizes.to_csv(output, index=False) + + if args.clusters is not None: + # Filter DataFrame based on valid clusters list + relative_effect_sizes_filtered = filter_dataframe(f_gt_m_effect_sizes, args.clusters) + output = f"{os.path.splitext(args.input_csv)[0]}_Hedges_g_{args.condition_1}_{args.condition_2}_F_gt_M_valid_clusters.csv" + relative_effect_sizes_filtered.to_csv(output, index=False)
+ + +if __name__ == '__main__': + main() + +# Effect size calculations described in the supplemental information: https://pubmed.ncbi.nlm.nih.gov/37248402/ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/fdr.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/fdr.html new file mode 100644 index 00000000..3361225c --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/fdr.html @@ -0,0 +1,704 @@ + + + + + + + + + + unravel.cluster_stats.fdr — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.fdr

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_fdr`` from UNRAVEL to perform FDR correction on a 1 - p value map to define clusters.
+
+Usage
+-----
+    cluster_fdr -i path/vox_p_tstat1.nii.gz -mas path/mask.nii.gz -q 0.05
+
+Inputs: 
+    - p value map (e.g., <asterisk>vox_p_<asterisk>stat<asterisk>.nii.gz from vstats)    
+
+Outputs saved in the output directory:
+    - FDR-adjusted p value map
+    - Cluster information CSV
+    - Reversed cluster index image (output_dir/input_name_rev_cluster_index.nii.gz)
+    - min_cluster_size_in_voxels.txt
+    - p_value_threshold.txt
+    - 1-p_value_threshold.txt
+
+Cluster IDs are reversed in the cluster index image so that the largest cluster is 1, the second largest is 2, etc.
+
+Making directional cluster indices from non-directional p value maps output from ANOVAs: 
+    - Provide the average immunostaining intensity images for each group being contrasted (``img_avg``)
+    - The --output needs to have <group1>_v_<group2> in the name
+    - _v_ will be replaced with _gt_ or _lt_ based on the effect direction 
+    - The cluster index will be split accoding to the effect directions
+    - ``cluster_fdr`` -i vox_p_fstat1.nii.gz -mas mask.nii.gz -q 0.05 -a1 group1_avg.nii.gz -a2 group2_avg.nii.gz -o stats_info_g1_v_g2 -v
+
+For bilateral data processed with a hemispheric mask, next run ``cluster_mirror_indices`` to mirror the cluster indices to the other hemisphere.
+
+For unilateral data or bilateral data processed with a whole brain mask, the cluster indices are ready for validation with ``cluster_validation``.
+"""
+
+import argparse
+import concurrent.futures
+import subprocess
+import numpy as np
+import nibabel as nib
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/p_value_map.nii.gz', required=True, action=SM) + parser.add_argument('-mas', '--mask', help='path/mask.nii.gz', required=True, action=SM) + parser.add_argument('-q', '--q_value', help='Space-separated list of FDR q values', required=True, nargs='*', type=float, action=SM) + parser.add_argument('-ms', '--min_size', help='Min cluster size in voxels. Default: 100', default=100, type=int, action=SM) + parser.add_argument('-o', '--output', help='Output directory. Default: input_name_q{args.q_value}"', default=None, action=SM) + parser.add_argument('-a1', '--avg_img1', help='path/averaged_immunofluo_group1.nii.gz for spliting the cluster index based on effect direction', action=SM) + parser.add_argument('-a2', '--avg_img2', help='path/averaged_immunofluo_group2.nii.gz for spliting the cluster index based on effect direction', action=SM) + parser.add_argument('-th', '--threads', help='Number of threads. Default: 10', default=10, type=int, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: could add optional args like in ``vstats`` for running the ``cluster_fdr`` command. + +
+[docs] +@print_func_name_args_times() +def fdr(input_path, fdr_path, mask_path, q_value): + """Perform FDR correction on the input p value map using a mask. + + Args: + - input_path (str): the path to the p value map + - fdr_path (str): the path to the output directory + - mask_path (str): the path to the mask + - q_value (float): the q value for FDR correction + + Saves in the fdr_path: + - FDR-adjusted p value map + + Returns: + - adjusted_pval_output_path (str): the path to the FDR-adjusted p value map + - probability_threshold (float): the probability threshold for the FDR correction + """ + + prefix = str(Path(input_path).name).replace('.nii.gz', '') + adjusted_pval_output_path = fdr_path / f"{prefix}_q{q_value}_adjusted_p_values.nii.gz" + + fdr_command = [ + 'fdr', + '-i', str(input_path), + '--oneminusp', + '-m', str(mask_path), + '-q', str(q_value), + '-a', str(adjusted_pval_output_path) + ] + + result = subprocess.run(fdr_command, capture_output=True, text=True) + if result.returncode != 0: + raise Exception(f"Error in FDR correction: {result.stderr}") + print(result.stdout) + probability_threshold = result.stdout.strip().split()[-1] + print(f'[default]1-p Threshold is:[/]\n{1-float(probability_threshold)}') + + return adjusted_pval_output_path, float(probability_threshold)
+ + +
+[docs] +@print_func_name_args_times() +def cluster_index(adj_p_val_img_path, min_size, q_value, output_index): + + print('') + thres = 1 - float(q_value) + + command = [ + 'cluster', + '-i', adj_p_val_img_path, + '-t', str(thres), + '--oindex=' + str(output_index), + '--minextent=' + str(min_size) + ] + result = subprocess.run(command, capture_output=True, text=True) + if result.returncode != 0: + print("Error:", result.stderr) + else: + print("Output:", result.stdout) + return result.stdout
+ + + +
+[docs] +@print_func_name_args_times() +def reverse_clusters(cluster_index_img, output, data_type, cluster_index_nii): + """Reverse the cluster IDs in a cluster index image (ndarray). Return the reversed cluster index ndarray.""" + max_cluster_id = int(cluster_index_img.max()) + rev_cluster_index_img = np.zeros_like(cluster_index_img) + + # Reassign cluster IDs in reverse order + for cluster_id in range(1, max_cluster_id + 1): + rev_cluster_index_img[cluster_index_img == cluster_id] = max_cluster_id - cluster_id + 1 + + rev_cluster_index_img = rev_cluster_index_img.astype(data_type) + rev_cluster_index_nii = nib.Nifti1Image(rev_cluster_index_img, cluster_index_nii.affine, cluster_index_nii.header) + rev_cluster_index_nii.set_data_dtype(data_type) + nib.save(rev_cluster_index_nii, output) + + return rev_cluster_index_img
+ + +
+[docs] +@print_func_name_args_times() +def split_clusters_based_on_effect(rev_cluster_index_img, avg_img1, avg_img2, output, max_cluster_id, data_type, cluster_index_nii): + if avg_img1 and avg_img2: + if Path(avg_img1).exists() and Path(avg_img2).exists(): + print("\n Splitting the rev_cluster_index into 2 parts (group 1 > group 2 and group 1 < group 2)\n") + avg_img1 = nib.load(avg_img1) + avg_img2 = nib.load(avg_img2) + avg_img1_data = np.asanyarray(avg_img1.dataobj, dtype=avg_img1.header.get_data_dtype()).squeeze() + avg_img2_data = np.asanyarray(avg_img2.dataobj, dtype=avg_img2.header.get_data_dtype()).squeeze() + + # Create a dict w/ mean intensities in each cluster for each group + cluster_means = {} + for cluster_id in range(1, max_cluster_id + 1): + cluster_mask = rev_cluster_index_img == cluster_id + cluster_means[cluster_id] = { + "group1": avg_img1_data[cluster_mask].mean(), + "group2": avg_img2_data[cluster_mask].mean() + } + + # Make two new cluster index images based on the effect directions (group1 > group2, group2 > group1) + img_g1_gt_g2 = np.zeros_like(rev_cluster_index_img, dtype=data_type) + img_g1_lt_g2 = np.zeros_like(rev_cluster_index_img, dtype=data_type) + for cluster_id, means in cluster_means.items(): + if means["group1"] > means["group2"]: + img_g1_gt_g2[rev_cluster_index_img == cluster_id] = cluster_id + else: + img_g1_lt_g2[rev_cluster_index_img == cluster_id] = cluster_id + + # Save the new cluster index images + rev_cluster_index_g1_gt_g2 = nib.Nifti1Image(img_g1_gt_g2, cluster_index_nii.affine, cluster_index_nii.header) + rev_cluster_index_g1_lt_g2 = nib.Nifti1Image(img_g1_lt_g2, cluster_index_nii.affine, cluster_index_nii.header) + rev_cluster_index_g1_gt_g2.set_data_dtype(data_type) + rev_cluster_index_g1_lt_g2.set_data_dtype(data_type) + nib.save(rev_cluster_index_g1_gt_g2, output.parent / str(output.name).replace('_v_', '_gt_')) + nib.save(rev_cluster_index_g1_lt_g2, output.parent / str(output.name).replace('_v_', '_lt_')) + else: + print(f"\n [red]The specified average image files do not exist.") + import sys ; sys.exit()
+ + + +
+[docs] +@print_func_name_args_times() +def process_fdr_and_clusters(input, mask, q, min_size, avg_img1, avg_img2, output=None): + """Process FDR correction and cluster index generation for a given q value.""" + if output is None: + fdr_dir_name = f"{Path(input).name[:-7]}_q{q}" + else: + fdr_dir_name = f"{output}_q{q}" + fdr_path = Path(input).parent / fdr_dir_name + output = Path(fdr_path, f"{fdr_dir_name}_rev_cluster_index.nii.gz") + if output.exists(): + return "The FDR-corrected reverse cluster index exists, skipping..." + fdr_path.mkdir(exist_ok=True, parents=True) + + # Perform FDR Correction + adjusted_pval_output_path, probability_threshold = fdr(input, fdr_path, mask, q) + + # Save the probability threshold and the 1-P threshold to a .txt file + with open(fdr_path / "p_value_threshold.txt", "w") as f: + f.write(f"{probability_threshold}\n") + with open(fdr_path / "1-p_value_threshold.txt", "w") as f: + f.write(f"{1 - probability_threshold}\n") + + # Save the min cluster size to a .txt file + with open(fdr_path / "min_cluster_size_in_voxels.txt", "w") as f: + f.write(f"{min_size}\n") + + # Generate cluster index + cluster_index_path = f"{fdr_path}/{fdr_dir_name}_cluster_index.nii.gz" + cluster_info = cluster_index(adjusted_pval_output_path, min_size, q, cluster_index_path) + + # Save the cluster info + with open(fdr_path / f"{fdr_dir_name}_cluster_info.txt", "w") as f: + f.write(cluster_info) + + # Load the cluster index and convert to an ndarray + cluster_index_nii = nib.load(cluster_index_path) + cluster_index_img = np.asanyarray(cluster_index_nii.dataobj, dtype=np.uint16).squeeze() + + # Lower the data type if the max cluster ID is less than 256 + max_cluster_id = int(cluster_index_img.max()) + data_type = np.uint16 if max_cluster_id >= 256 else np.uint8 + cluster_index_img = cluster_index_img.astype(data_type) + + # Reverse cluster ID order in cluster_index and save it + rev_cluster_index_img = reverse_clusters(cluster_index_img, output, data_type, cluster_index_nii) + + # Split the cluster index based on the effect directions + split_clusters_based_on_effect(rev_cluster_index_img, avg_img1, avg_img2, output, max_cluster_id, data_type, cluster_index_nii) + + # Remove the original cluster index file + Path(cluster_index_path).unlink()
+ + + +
+[docs] +def main(): + args = parse_args() + + # Prepare directory paths and outputs + results = [] + with ThreadPoolExecutor(max_workers=args.threads) as executor: + future_to_q = { + executor.submit(process_fdr_and_clusters, args.input, args.mask, q, args.min_size, args.avg_img1, args.avg_img2, args.output): q + for q in args.q_value + } + + for future in concurrent.futures.as_completed(future_to_q): + q_value = future_to_q[future] + try: + result = future.result() + results.append((q_value, result)) + except Exception as exc: + print(f'{q_value} generated an exception: {exc}')
+ + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/fdr_range.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/fdr_range.html new file mode 100644 index 00000000..bc8aaf36 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/fdr_range.html @@ -0,0 +1,532 @@ + + + + + + + + + + unravel.cluster_stats.fdr_range — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.fdr_range

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_fdr_range`` from UNRAVEL to output a list of FDR q values that yeild clusters.
+
+Usage
+-----
+    cluster_fdr_range -i path/vox_p_tstat1.nii.gz -mas path/mask.nii.gz
+
+Inputs: 
+    - p value map (e.g., *vox_p_*stat*.nii.gz from vstats)    
+"""
+
+import argparse
+import concurrent.futures
+import subprocess
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + q_values_default = [0.00001, 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 0.999, 0.9999] + parser.add_argument('-i', '--input', help='path/p_value_map.nii.gz', required=True, action=SM) + parser.add_argument('-mas', '--mask', help='path/mask.nii.gz', required=True, action=SM) + parser.add_argument('-q', '--q_values', help='Space-separated list of q values. If omitted, a default list is used.', nargs='*', default=q_values_default, type=float, action=SM) + parser.add_argument('-th', '--threads', help='Number of threads. Default: 22', default=22, type=int, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Sometimes different q values yield the same p value threshold. In this case, not this in the dir name (don't process it). Case: ET z s50 tstat2 + +
+[docs] +def smart_float_format(value, max_decimals=9): + + """Format float with up to `max_decimals` places, but strip unnecessary trailing zeros.""" + formatted = f"{value:.{max_decimals}f}" # Format with maximum decimal places + return formatted.rstrip('0').rstrip('.') if '.' in formatted else formatted
+ + +
+[docs] +def fdr_range(input_path, mask_path, q_value): + """Perform FDR correction on the input p value map using a mask. + + Args: + - input_path (str): the path to the p value map + - mask_path (str): the path to the mask + - q_value (float): the q value for FDR correction + + """ + + fdr_command = [ + 'fdr', + '-i', str(input_path), + '--oneminusp', + '-m', str(mask_path), + '-q', str(q_value), + ] + + result = subprocess.run(fdr_command, capture_output=True, text=True) + if result.returncode != 0: + raise Exception(f"Error in FDR correction: {result.stderr}") + + # Extract the probability threshold from the output + probability_threshold = result.stdout.strip().split()[-1] + probability_threshold_float = float(probability_threshold) + + return q_value, probability_threshold_float
+ + + +
+[docs] +def main(): + args = parse_args() + + # Initialize ThreadPoolExecutor + with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor: + # Submit tasks to the executor for each q_value + futures = [executor.submit(fdr_range, args.input, args.mask, q_value) for q_value in args.q_values] + + q_values_resulting_in_clusters = [] + # Process results as they complete + for future in concurrent.futures.as_completed(futures): + q_value, probability_threshold = future.result() + if 0 < probability_threshold < 0.05: + q_values_resulting_in_clusters.append(q_value) + + # Sort q_values numerically + q_values_resulting_in_clusters.sort() + + # Convert the sorted list to a string and print + q_values_resulting_in_clusters_str = ' '.join([smart_float_format(q) for q in q_values_resulting_in_clusters]) + print(f'\n{q_values_resulting_in_clusters_str}\n')
+ + +if __name__ == '__main__': + install() + args = parse_args() + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/find_incongruent_clusters.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/find_incongruent_clusters.html new file mode 100644 index 00000000..fe897dcc --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/find_incongruent_clusters.html @@ -0,0 +1,524 @@ + + + + + + + + + + unravel.cluster_stats.find_incongruent_clusters — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.find_incongruent_clusters

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_find_incongruent`` from UNRAVEL if ``cluster_fdr`` was used to convert non-directional p value maps into directional cluster indices. This helps to find clusters where the direction of the mean intensity difference between groups does not match direction of the difference in cell/label density between groups.
+
+Usage
+-----
+    cluster_find_incongruent -c tukey_results.csv -l groupA -g groupB
+    
+This is useful to find clusters where z-scoring introduces incongruencies between the mean intensity difference and the density difference.
+    
+For example, if group A has increased IF in region A and not B, z-scoring may decrease the relative intensity of region B. 
+This decrease for region B for one group, may introduce a difference in the mean intensity between groups that is not reflected in the density difference.
+
+Input csv: 
+    ./_cluster_validation_info/tukey_results.csv  or ttest_results.csv from ``cluster_stats``
+
+Columns: 
+    'cluster_ID', 'comparison', 'higher_mean_group', 'p-value', 'significance'
+
+Output:
+    Cluster IDs where the mean intensity difference does not match the density difference between groups A and B.
+"""
+
+import argparse
+from pathlib import Path
+import pandas as pd
+from glob import glob
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.utils import print_cmd_and_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-c', '--csv_name', help='Name of the CSV file.', required=True, action=SM) + parser.add_argument('-l', '--lesser_group', help='Group with a lower mean for the comparison of interest.', required=True, action=SM) + parser.add_argument('-g', '--greater_group', help='Group with a higher mean for the comparison of interest.', required=True, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def find_incongruent_clusters(df, expected_lower_mean_group, expected_higher_mean_group): + + # Determine the comparison string (e.g. 'groupA vs groupB' in the 'comparison' column) + comparison_str1 = f'{expected_lower_mean_group} vs {expected_higher_mean_group}' + comparison_str2 = f'{expected_higher_mean_group} vs {expected_lower_mean_group}' + + # Filter data based on the comparison string + filtered_df = df[ + (df['comparison'] == comparison_str1) | + (df['comparison'] == comparison_str2) + ] + + # Find clusters that are significant and incongruent with the prediction + incongruent_clusters = filtered_df[ + (filtered_df['significance'] != 'n.s.') & + (filtered_df['higher_mean_group'] != expected_higher_mean_group) + ]['cluster_ID'].tolist() + + return incongruent_clusters
+ + + +
+[docs] +def main(): + args = parse_args() + + current_dir = Path.cwd() + + # Consctruct substring to find matching subdirs + substring_str1 = f'{args.greater_group}_gt_{args.lesser_group}' + substring_str2 = f'{args.lesser_group}_lt_{args.greater_group}' + + # Iterate over all subdirectories in the current working directory + for subdir in [d for d in current_dir.iterdir() if d.is_dir() and (substring_str1 in d.name or substring_str2 in d.name)]: + print(f"\nProcessing directory: [default bold]{subdir.name}[/]") + + df = pd.read_csv(subdir / "_valid_clusters_stats" / args.csv_name) + incongruent_clusters = find_incongruent_clusters(df, args.lesser_group, args.greater_group) + + if incongruent_clusters: + print(f'\n CSV: {args.csv_name}') + print(f" Incongruent clusters: {incongruent_clusters}\n") + else: + print(f' CSV: {args.csv_name}') + print(" No incongruent clusters found.\n")
+ + +if __name__ == '__main__': + install() + args = parse_args() + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/group_bilateral_data.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/group_bilateral_data.html new file mode 100644 index 00000000..06e8825b --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/group_bilateral_data.html @@ -0,0 +1,534 @@ + + + + + + + + + + unravel.cluster_stats.group_bilateral_data — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.group_bilateral_data

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_group_data`` from UNRAVEL to organize bilateral csv outputs from ``cluster_validation``
+
+Run this command in the target_dir from ``cluster_org_data``
+        
+Usage
+-----
+    cluster_group_data
+
+It consolidates CSV files into pooled directories based on hemisphere.
+
+Folder naming convention: 
+    - <cluster_validation_dir>_LH for left hemisphere files
+    - <cluster_validation_dir>_RH for right hemisphere files
+
+For example, if the command is run in a directory containing the following directories:
+    - cluster_valid_results_1_LH
+    - cluster_valid_results_1_RH
+    - cluster_valid_results_2_LH
+    - cluster_valid_results_2_RH
+
+The command will create a new directory for each cluster and move the corresponding left and right hemisphere files into it. 
+The original directories will be removed.
+
+The resulting directory structure will be:
+    - cluster_valid_results_1
+    - cluster_valid_results_2
+"""
+
+import argparse
+import shutil
+from pathlib import Path
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +@print_func_name_args_times() +def group_hemisphere_data(base_path): + + # Collect all relevant directories + base_path = Path(base_path) + dirs = [d for d in base_path.iterdir() if d.is_dir()] + + # Separate left and right hemisphere directories into dictionaries + # (key: common basename, value: side-specific directory path) + lh_dirs_dict = {d.name[:-3]: d for d in dirs if d.name.endswith('_LH')} + rh_dirs_dict = {d.name[:-3]: d for d in dirs if d.name.endswith('_RH')} + + # Process matching pairs + for common_name, lh_dir in lh_dirs_dict.items(): + rh_dir = rh_dirs_dict.get(common_name) # Check if there is a matching RH dir + if rh_dir: # Only proceed if both left and right directories exist + new_dir_path = base_path / common_name + new_dir_path.mkdir(exist_ok=True) + + # Move files from left hemisphere directory to new directory + for file in lh_dir.iterdir(): + dest_file = Path(new_dir_path, file.name) + if not dest_file.exists(): + shutil.move(str(file), dest_file) + + # Move files from right hemisphere directory to new directory + for file in rh_dir.iterdir(): + dest_file = Path(new_dir_path, file.name) + if not dest_file.exists(): + shutil.move(str(file), dest_file) + + # Remove the original directories + shutil.rmtree(lh_dir) + shutil.rmtree(rh_dir)
+ + + +
+[docs] +def main(): + args = parse_args() + + base_path = Path.cwd() + + has_hemisphere = False + for subdir in [d for d in Path.cwd().iterdir() if d.is_dir()]: + if str(subdir).endswith('_LH') or str(subdir).endswith('_RH'): + has_hemisphere = True + + if has_hemisphere: + group_hemisphere_data(base_path)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/index.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/index.html new file mode 100644 index 00000000..c3ba1b38 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/index.html @@ -0,0 +1,547 @@ + + + + + + + + + + unravel.cluster_stats.index — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.index

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_index`` from UNRAVEL to create a cluster index with valid clusters from a given NIfTI image.
+
+Usage
+-----
+    cluster_index -ci path/rev_cluster_index.nii.gz -a path/atlas.nii.gz -ids 1 2 3
+    
+Outputs:
+    - path/valid_clusters/rev_cluster_index_valid_clusters.nii.gz
+    - path/valid_clusters/cluster_<asterisk>_sunburst.csv
+"""
+
+from pathlib import Path
+import nibabel as nib
+import numpy as np
+import argparse
+from concurrent.futures import ThreadPoolExecutor
+from rich import print
+from rich.traceback import install
+
+from unravel.cluster_stats.sunburst import sunburst
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-ci', '--cluster_idx', help='Path to the reverse cluster index NIfTI file.', default=None, action=SM) + parser.add_argument('-ids', '--valid_cluster_ids', help='Space-separated list of valid cluster IDs.', nargs='+', type=int, default=None, action=SM) + parser.add_argument('-vcd', '--valid_clusters_dir', help='path/name_of_the_output_directory. Default: valid_clusters', default='_valid_clusters', action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz (Default: path/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-rgb', '--output_rgb_lut', help='Output sunburst_RGBs.csv if flag provided (for Allen brain atlas coloring)', action='store_true') + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def generate_sunburst(cluster, img, atlas, xyz_res_in_um, data_type, output_dir): + """Generate a sunburst plot for a given cluster. + + Args: + - cluster (int): the cluster ID. + - img (ndarray): the input image ndarray. + - atlas (ndarray): the atlas ndarray. + - atlas_res_in_um (tuple): the atlas resolution in microns. For example, (25, 25, 25) + - data_type (type): the data type of the image. + - output_dir (Path): the output directory. + """ + mask = (img == cluster) + if np.any(mask): + cluster_image = np.where(mask, cluster, 0).astype(data_type) + cluster_sunburst_path = output_dir / f'cluster_{cluster}_sunburst.csv' + sunburst_df = sunburst(cluster_image, atlas, xyz_res_in_um, cluster_sunburst_path)
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.cluster_idx is None: + print(f"\n No cluster index provided. Skipping.") + return + + if args.valid_cluster_ids is None: + print(f"\n No valid clusters provided. Skipping.") + return + + output_dir = Path(args.valid_clusters_dir) + output_dir.mkdir(exist_ok=True, parents=True) + output_image_path = output_dir / str(Path(args.cluster_idx).name).replace('.nii.gz', f'_{output_dir.name}.nii.gz') + if output_image_path.exists(): + print(f"\n {output_image_path.name} already exists. Skipping.") + return + + nii = nib.load(args.cluster_idx) + img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze() + max_cluster_id = int(img.max()) + data_type = np.uint16 if max_cluster_id >= 256 else np.uint8 + img = img.astype(data_type) + + atlas_nii = nib.load(args.atlas) + atlas = np.asanyarray(atlas_nii.dataobj, dtype=atlas_nii.header.get_data_dtype()).squeeze() + atlas_res = atlas_nii.header.get_zooms() # (x, y, z) in mm + xyz_res_in_um = atlas_res[0] * 1000 + + # Write valid cluster indices to a file + with open(output_dir / 'valid_clusters.txt', 'w') as file: + file.write(' '.join(map(str, args.valid_cluster_ids))) + + # Generate the valid cluster index + valid_cluster_index = np.zeros_like(img, dtype=data_type) + for cluster in args.valid_cluster_ids: + valid_cluster_index = np.where(img == cluster, cluster, valid_cluster_index) + + # Parallel processing of sunburst plots + with ThreadPoolExecutor() as executor: + futures = [executor.submit(generate_sunburst, cluster, img, atlas, xyz_res_in_um, data_type, output_dir) for cluster in args.valid_cluster_ids] + for future in futures: + future.result() # Wait for all threads to complete + + print(f' Saved valid cluster index: {output_image_path}') + nib.save(nib.Nifti1Image(valid_cluster_index, nii.affine, nii.header), output_image_path) + + # Generate the sunburst plot for the valid cluster index + sunburst_df = sunburst(valid_cluster_index, atlas, xyz_res_in_um, output_dir / 'valid_clusters_sunburst.csv', args.output_rgb_lut)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/legend.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/legend.html new file mode 100644 index 00000000..f2b0095d --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/legend.html @@ -0,0 +1,762 @@ + + + + + + + + + + unravel.cluster_stats.legend — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.legend

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_legend`` from UNRAVEL to summarize regional abbreviations from <asterisk>_valid_clusters_table.xlsx files.
+
+Usage:
+------
+    cluster_legend
+
+Inputs:
+    <asterisk>_valid_clusters_table.xlsx files in the working directory output from ``cluster_table``
+
+Outputs:
+    legend.xlsx
+"""
+
+import argparse
+from glob import glob
+from pathlib import Path
+import openpyxl
+import pandas as pd
+from rich import print
+from rich.traceback import install
+from openpyxl.styles import PatternFill
+from openpyxl.utils.dataframe import dataframe_to_rows
+from openpyxl.styles import Border, Side, Font
+from openpyxl.styles import Alignment
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-p', '--path', help='Path to the directory containing the *_valid_clusters_table.xlsx files. Default: current working directory', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def extract_unique_regions_from_file(file_path): + # Load the Excel file into a DataFrame, specifying that the header is in the second row (index 1) + df = pd.read_excel(file_path, header=1, engine='openpyxl') + + unique_regions = set() + + # Iterate through columns and find those that start with "Top_Region" + for col in df.columns: + if col.startswith("Top_Region"): + # Extract the regions from the column, remove percent volumes and add to the set + unique_regions.update(df[col].dropna().apply(lambda x: ' '.join(x.split()[:-1]))) + + return unique_regions
+ + +
+[docs] +def apply_rgb_to_cell(ws, df_w_rgb, col_w_labels, col_num): + """Apply RGB values to cells in the worksheet. + + Parameters: ws (openpyxl worksheet), df_w_rgb (DataFrame with RGB values), col_w_labels (column with region labels), region (region name), col_num (column number to apply the RGB values starting from 0)""" + for row in ws.iter_rows(min_row=3, min_col=2, max_col=4, max_row=ws.max_row-1): + region_cell = row[col_num] + + r = df_w_rgb.loc[df_w_rgb[col_w_labels] == region_cell.value, 'R'].values[0] + g = df_w_rgb.loc[df_w_rgb[col_w_labels] == region_cell.value, 'G'].values[0] + b = df_w_rgb.loc[df_w_rgb[col_w_labels] == region_cell.value, 'B'].values[0] + hex_color = "{:02x}{:02x}{:02x}".format(r, g, b) + fill = PatternFill(start_color=hex_color, end_color=hex_color, fill_type='solid') + region_cell.fill = fill
+ + + +
+[docs] +def main(): + args = parse_args() + + path = Path(args.path) if args.path else Path.cwd() + + # Find cluster_* dirs in the current dir + xlsx_files = path.glob('*_valid_clusters_table.xlsx') + + # Filter out files starting with '~$' + xlsx_files = [f for f in xlsx_files if not str(f).split('/')[-1].startswith('~$')] + + # Filter out files that start with legend + xlsx_files = [f for f in xlsx_files if not str(f).split('/')[-1].startswith('legend')] + + # Initialize a set to store unique regions from all files, accounting for headers in the second row + all_unique_regions = set() # Using a set to avoid duplicates + + # Iterate through each file and extract unique regions from each file + for file_path in xlsx_files: + unique_regions = extract_unique_regions_from_file(file_path) + all_unique_regions.update(unique_regions) + + # Convert the set to a sorted list for easier reading + all_unique_regions = sorted(list(all_unique_regions)) + print(f'\n{all_unique_regions=}\n') + + # Specify the column names you want to load + columns_to_load = ['structure_id_path', 'very_general_region', 'collapsed_region_name', 'abbreviation', 'collapsed_region', 'other_abbreviation', 'other_abbreviation_defined', 'layer', 'sunburst'] + + # Load the specified columns from the CSV with CCFv3 info + ccfv3_info_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'CCFv3_info.csv', usecols=columns_to_load) + + # Creat a dictionary to hold the mappings for the region abbreviation to collapsed region abbreviation + abbreviation_to_collapsed_dict = dict(zip(ccfv3_info_df['abbreviation'], ccfv3_info_df['collapsed_region'])) + + # Now collapse the regions in the unique_regions set + unique_regions_collapsed = {abbreviation_to_collapsed_dict.get(region, region) for region in all_unique_regions} + unique_regions_collapsed = sorted(list(unique_regions_collapsed)) + print(f'{unique_regions_collapsed=}\n') + + # If a region in all_unique_regions has a digit in it, check if the 'layer' column is defined for it. Then, add unique layers to a set + layers_set = set() + for region in all_unique_regions: + if any(char.isdigit() for char in region): + layer = ccfv3_info_df.loc[ccfv3_info_df['abbreviation'] == region, 'layer'].values + if len(layer) > 0: + layers_set.add(str(layer[0])) # Convert float to string + + # Sort the layers + layers_set = sorted(list(layers_set)) + layers_set = [layer for layer in layers_set if str(layer) != 'nan'] + + # Get all regions with digits that are not defined as layers + other_regions_w_digits = [ + region for region in all_unique_regions + if any(char.isdigit() for char in region) and not ccfv3_info_df[ccfv3_info_df['abbreviation'] == region]['layer'].notna().any() + ] + + # Print the cortical layers and any regions with digits that are not defined as layers + if len(other_regions_w_digits) > 0: + print(f"Numbers ({layers_set}) = cortical layers (with these exceptions {other_regions_w_digits})\n") + else: + print(f"Numbers ({layers_set}) = cortical layers\n") + + # For regions in all_unique_regions, determine abbreviations to offload from the table (i.e., abbreviations mentioned in 'other_abbreviation' and defined in 'other_abbreviation_defined') + list_of_regions_w_other_abbreviation_in_all_unique_regions = [region for region in all_unique_regions if ccfv3_info_df.loc[ccfv3_info_df['abbreviation'] == region, 'other_abbreviation'].notna().any()] + + # Initialize an empty dictionary to hold the mapping of other_abbreviations to their definitions + other_abbreviation_to_definitions = {} + + for region in list_of_regions_w_other_abbreviation_in_all_unique_regions: + # Extract 'other_abbreviation' and 'other_abbreviation_defined' for the current region + rows = ccfv3_info_df[ccfv3_info_df['abbreviation'] == region] + for _, row in rows.iterrows(): + other_abbreviation = row['other_abbreviation'] + other_abbreviation_defined = row['other_abbreviation_defined'] + + if pd.notna(other_abbreviation) and pd.notna(other_abbreviation_defined): + # Initialize the set for this abbreviation if it doesn't exist + if other_abbreviation not in other_abbreviation_to_definitions: + other_abbreviation_to_definitions[other_abbreviation] = set() + + # Add the current definition to the set of definitions for this abbreviation + other_abbreviation_to_definitions[other_abbreviation].add(other_abbreviation_defined) + + # Convert sets to strings with " or " as the separator + for abbreviation, definitions_set in other_abbreviation_to_definitions.items(): + other_abbreviation_to_definitions[abbreviation] = " or ".join(definitions_set) + + # Sort the dictionary by key + other_abbreviation_to_definitions = dict(sorted(other_abbreviation_to_definitions.items())) + + print(f'Additional abbreviations not shown in the region abbreviation legend (SI table): {other_abbreviation_to_definitions}') + + + # Get the 'very_general_region' column from the CCFv3_info.csv file and use it to get the 'very_general_region' for each region in unique_regions_collapsed + very_general_region_dict = dict(zip(ccfv3_info_df['collapsed_region'], ccfv3_info_df['very_general_region'])) + very_general_regions = [very_general_region_dict.get(region, '') for region in unique_regions_collapsed] + + # If the same string is in the 'very_general_regions' list and the 'unique_regions_collapsed' list, remove it from both at the same index + for i, region in enumerate(unique_regions_collapsed): + if very_general_regions[i] == region: + very_general_regions[i] = '' + unique_regions_collapsed[i] = '' + + # Reset the indices of the lists + very_general_regions = [region for region in very_general_regions if region != ''] + unique_regions_collapsed = [region for region in unique_regions_collapsed if region != ''] + + # Make a dataframe with the 'very_general_regions' and 'unique_regions_collapsed' lists + legend_df = pd.DataFrame({'Region': very_general_regions, 'Abbrev.': unique_regions_collapsed}) + + # Add the 'Subregion' column to the dataframe + legend_df['Subregion'] = [ccfv3_info_df.loc[ccfv3_info_df['collapsed_region'] == region, 'collapsed_region_name'].values[0] for region in unique_regions_collapsed] + + # Add the 'structure_id_path' column to the dataframe + legend_df['structure_id_path'] = [ccfv3_info_df.loc[ccfv3_info_df['collapsed_region'] == region, 'structure_id_path'].values[0] for region in unique_regions_collapsed] + + # Sort the dataframe by the 'structure_id_path' column in descending order + legend_df.sort_values(by='structure_id_path', ascending=False, inplace=True) + + # Reset the index of the dataframe + legend_df.reset_index(drop=True, inplace=True) + + # Drop the 'structure_id_path' column + legend_df.drop(columns='structure_id_path', inplace=True) + + # Create an Excel workbook and select the active worksheet + wb = openpyxl.Workbook() + ws = wb.active + + thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) + + # Write the dataframe to the worksheet + for region in dataframe_to_rows(legend_df, index=False, header=True): + ws.append(region) + + # Apply a thin border style to cells with content + for row in ws.iter_rows(): + for cell in row: + if cell.value: + cell.border = thin_border + cell.font = Font(name='Arial', size=11) + + # Apply the font to the header row + header_font = Font(name='Arial', bold=True) + for cell in ws['1:1']: + cell.font = header_font + + # Make the first column bold + for cell in ws['A']: + cell.font = Font(name='Arial', bold=True) + + # Insert a new row at the top + ws.insert_rows(1) + + # Insert a new column at the left + ws.insert_cols(1) + + # Fill cells in first column with white + for cell in ws['A']: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Fill cells in fifth column with white + for cell in ws['E']: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Fill cells in first row with white + for cell in ws[1]: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Fill cells in last row with white + num_rows = legend_df.shape[0] + 3 + for cell in ws[num_rows]: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Adjust the column width to fit the content + for col in ws.columns: + max_length = 0 + for cell in col: + if cell.value: + max_length = max(max_length, len(str(cell.value))) + if max_length > 0: + adjusted_width = max_length + 2 + column_letter = openpyxl.utils.get_column_letter(col[0].column) + ws.column_dimensions[column_letter].width = adjusted_width + + # # Add columns for R, G, and B values to ccfv3_info_df + ccfv3_info_df[['R', 'G', 'B']] = ccfv3_info_df['sunburst'].str.extract(r'rgb\((\d+),(\d+),(\d+)\)') + ccfv3_info_df[['R', 'G', 'B']] = ccfv3_info_df[['R', 'G', 'B']].apply(pd.to_numeric) + + # Apply RGB values to cells + apply_rgb_to_cell(ws, ccfv3_info_df, 'very_general_region', 0) + apply_rgb_to_cell(ws, ccfv3_info_df, 'collapsed_region', 1) + apply_rgb_to_cell(ws, ccfv3_info_df, 'collapsed_region_name', 2) + + # Iterate through the cells and merge cells with the same value in column B + current_region = None + first_row = None + + # Adjusted min_row to 2 and min_col/max_col to merge_column because of the added padding row and column + merge_column = 2 + for row in ws.iter_rows(min_row=2, max_row=ws.max_row - 1, min_col=merge_column, max_col=merge_column): + cell = row[0] # row[0] since we're only looking at one column, and iter_rows yields tuples + if cell.value != current_region: + # If the cell value changes, merge the previous cells if there are more than one with the same value + if first_row and first_row < cell.row - 1: + ws.merge_cells(start_row=first_row, start_column=merge_column, end_row=cell.row - 1, end_column=merge_column) + # After merging, we need to set the alignment for the merged cells + merged_cell = ws.cell(row=first_row, column=merge_column) + merged_cell.alignment = Alignment(vertical='center') + # Update the current region and reset the first_row to the current cell's row + current_region = cell.value + first_row = cell.row + + # After the loop, check and merge the last set of cells if needed + if first_row and first_row < ws.max_row: + ws.merge_cells(start_row=first_row, start_column=merge_column, end_row=ws.max_row - 1, end_column=merge_column) + # Align the last merged cell as well + merged_cell = ws.cell(row=first_row, column=merge_column) + merged_cell.alignment = Alignment(vertical='center') + + # Center align the content + for row in ws.iter_rows(min_row=1, min_col=1): + for cell in row: + cell.alignment = Alignment(horizontal='center', vertical='center') + + # Ensure that fonts are black + for row in ws.iter_rows(min_col=2): + for cell in row: + if cell.font: # If the cell already has font settings applied + cell.font = Font(name='Arial', size=cell.font.size, bold=cell.font.bold, color='FF000000') + else: + cell.font = Font(name='Arial', color='FF000000') + + # Save the workbook to a file + excel_file_path = path / 'legend.xlsx' + wb.save(excel_file_path) + + # Save text for figure legend + fig_legend_txt = path / "fig_legend.txt" + with open(fig_legend_txt, "w") as file: + file.write(f'\n{all_unique_regions=}\n') + file.write(f'\n{unique_regions_collapsed=}\n') + if len(other_regions_w_digits) > 0: + file.write(f"\nNumbers ({layers_set}) = cortical layers (with these exceptions {other_regions_w_digits})\n") + else: + file.write(f"\nNumbers ({layers_set}) = cortical layers\n") + file.write(f'\nAdditional abbreviations not shown in the region abbreviation legend (SI table): {other_abbreviation_to_definitions}\n')
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/org_data.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/org_data.html new file mode 100644 index 00000000..db90641a --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/org_data.html @@ -0,0 +1,632 @@ + + + + + + + + + + unravel.cluster_stats.org_data — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.org_data

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_org_data`` from UNRAVEL to aggregate and organize csv outputs from ``cluster_validation``.
+
+Usage
+-----
+    cluster_org_data -e <list of experiment directories> -cvd '<asterisk>' -td <target_dir> -vd <path/vstats_dir> -v
+"""
+
+import argparse
+import re
+import shutil
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-cvd', '--cluster_val_dirs', help='Glob pattern matching cluster validation output dirs to copy data from (relative to ./sample??/clusters/)', required=True, action=SM) + parser.add_argument('-vd', '--vstats_path', help='path/vstats_dir (the dir ``vstats`` was run from) to copy p val, info, and index files if provided', default=None, action=SM) + parser.add_argument('-dt', '--density_type', help='Type of density data to aggregate (cell [default] or label).', default='cell', action=SM) + parser.add_argument('-td', '--target_dir', help='path/dir to copy results. If omitted, copy data to the cwd', default=None, action=SM) + parser.add_argument('-pvt', '--p_val_txt', help='Name of the file w/ the corrected p value thresh (e.g., from ``cluster_fdr``). Default: p_value_threshold.txt', default='p_value_threshold.txt', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Copy the rev_cluster_index.nii.gz to the target_dir + +
+[docs] +def find_matching_directory(base_path, long_name): + base_path = Path(base_path) + + # Get all directories in base_path + dirs = [d for d in base_path.iterdir() if d.is_dir()] + + # Find the directory whose name is a substring of long_name + for dir in dirs: + if dir.name in long_name: + return dir.name + + return None
+ + +
+[docs] +def cp(src, dest): + """Copy a file from src path to a dest path, optionally printing the action. + + Args: + - src (Path): the source path + - dest (Path): the destination path""" + shutil.copy(src, dest)
+ + +
+[docs] +def copy_stats_files(validation_dir, dest_path, vstats_path, p_val_txt): + """Copy the cluster info, p value threshold, and rev_cluster_index files to the target directory. + + Args: + - validation_dir (Path): the path to the validation directory + - dest_path (Path): the path to the new directory + - vstats_path (Path): the path to the vstats directory + - p_val_txt (str): the name of the file with the corrected p value threshold""" + + vstats_path = Path(vstats_path) + + if vstats_path.exists(): + validation_dir_name = str(validation_dir.name) + + validation_dir_name = validation_dir_name.replace('_gt_', '_v_').replace('_lt_', '_v_') + + if validation_dir_name.endswith('_LH') or validation_dir_name.endswith('_RH'): + cluster_correction_dir = validation_dir_name[:-3] # Remove last 3 characters (_LH or _RH) + else: + cluster_correction_dir = validation_dir_name + + # Regular expression to match the part before and after 'q*' to remove any suffix added to the rev_cluster_index<suffix>.nii.gz + pattern = r'(.*q\d+\.\d+)(_.*)' + match = re.match(pattern, cluster_correction_dir) + if match: + cluster_correction_dir = match.group(1) + suffix = match.group(2)[1:] # Remove the leading underscore + else: + print("\n [red1]No match found in cluster_org_data\n") + + cluster_correction_path = vstats_path / 'stats' / cluster_correction_dir + + if not cluster_correction_path.exists(): + cluster_correction_dir = find_matching_directory(vstats_path / 'stats', cluster_correction_dir) + cluster_correction_path = vstats_path / 'stats' / cluster_correction_dir + + if not cluster_correction_path.exists(): + print(f'\n [red]Path for copying the rev_cluster_index.nii.gz and p value threshold does not exist: {cluster_correction_path}\n') + cluster_info = cluster_correction_path / f'{cluster_correction_dir}_cluster_info.txt' + if cluster_info.exists(): + dest_stats = dest_path / cluster_info.name + if not dest_stats.exists(): + cp(src=cluster_info, dest=dest_stats) + else: + print(f'\n [red]The cluster_info.txt ({cluster_info}) does not exist\n') + + p_val_thresh_file = cluster_correction_path / p_val_txt + if p_val_thresh_file.exists(): + dest_p_val_thresh = dest_path / p_val_txt + if not dest_p_val_thresh.exists(): + cp(src=p_val_thresh_file, dest=dest_p_val_thresh) + else: + print(f'\n [red]The p value threshold txt ({p_val_thresh_file}) does not exist\n') + + if validation_dir_name.endswith('_LH'): + rev_cluster_index_path = cluster_correction_path / f'{str(validation_dir.name)[:-3]}_rev_cluster_index_LH.nii.gz' + elif validation_dir_name.endswith('_RH'): + rev_cluster_index_path = cluster_correction_path / f'{str(validation_dir.name)[:-3]}_rev_cluster_index_RH.nii.gz' + else: + rev_cluster_index_path = cluster_correction_path / f'{str(validation_dir.name)}_rev_cluster_index.nii.gz' + + if not rev_cluster_index_path.exists(): + suffix = str(validation_dir_name).replace(str(cluster_correction_path.name), '') + rev_cluster_index_path = cluster_correction_path / f"{cluster_correction_path.name}_rev_cluster_index{suffix}.nii.gz" + + if rev_cluster_index_path.exists(): + dest_rev_cluster_index = dest_path / rev_cluster_index_path.name + if not dest_rev_cluster_index.exists(): + cp(src=rev_cluster_index_path, dest=dest_rev_cluster_index) + else: + print(f'\n [red]The rev_cluster_index.nii.gz ({rev_cluster_index_path}) does not exist\n') + import sys ; sys.exit()
+ + +
+[docs] +def organize_validation_data(sample_path, clusters_path, validation_dir_pattern, density_type, target_dir, vstats_path, p_val_txt): + """Copy the cluster validation, p value, cluster info, and rev_cluster_index files to the target directory. + + Args: + - sample_path (Path): the path to the sample directory + - clusters_path (Path): the path to the clusters directory + - validation_dir_pattern (str): the pattern to match the validation directories + - density_type (str): the type of density data to aggregate (cell or label) + - target_dir (Path): the path to the target directory + - vstats_path (Path): the path to the vstats directory + - p_val_txt (str): the name of the file with the corrected p value threshold + - cluster_idx (str): the name of the rev_cluster_index file""" + + validation_dirs = list(clusters_path.glob(validation_dir_pattern)) + if not validation_dirs: + print(f"\n [red1]No directories found matching pattern: {validation_dir_pattern} in {clusters_path}\n") + import sys ; sys.exit() + + for validation_dir in clusters_path.glob(validation_dir_pattern): + if validation_dir.is_dir(): + dest_path = target_dir / validation_dir.name + dest_path.mkdir(parents=True, exist_ok=True) + src_csv = validation_dir / f'{density_type}_density_data.csv' + + if src_csv.exists(): + dest_csv = dest_path / f'{sample_path.name}__{density_type}_density_data__{validation_dir.name}.csv' + + if not dest_csv.exists(): + cp(src=src_csv, dest=dest_csv) + + if vstats_path is not None: + copy_stats_files(validation_dir, dest_path, vstats_path, p_val_txt)
+ + + +
+[docs] +def main(): + args = parse_args() + + target_dir = Path(args.target_dir).resolve() if args.target_dir else Path.cwd() + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + clusters_path = sample_path / 'clusters' + if clusters_path.exists(): + organize_validation_data(sample_path, clusters_path, args.cluster_val_dirs, args.density_type, target_dir, args.vstats_path, args.p_val_txt)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/prism.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/prism.html new file mode 100644 index 00000000..58b7212e --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/prism.html @@ -0,0 +1,647 @@ + + + + + + + + + + unravel.cluster_stats.prism — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.prism

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_prism`` from UNRAVEL to organize cell_count|label_volume, cluster_volume, and <cell|label>_density data from cluster and sample and save as csv for plotting in Prism.
+
+Usage
+-----
+    cluster_prism -ids 1 2 3
+        
+Inputs:
+    <asterisk>.csv from cluster_org_data (in working dir)
+
+CSV naming conventions:
+    - Condition: first word before '_' in the file name
+    - Sample: second word in file name
+
+Example unilateral inputs:
+    - condition1_sample01_<cell|label>_density_data.csv
+    - condition1_sample02_<cell|label>_density_data.csv
+    - condition2_sample03_<cell|label>_density_data.csv
+    - condition2_sample04_<cell|label>_density_data.csv
+
+Example bilateral inputs (if any file has _LH.csv or _RH.csv, the command will attempt to pool data):
+    - condition1_sample01_<cell|label>_density_data_LH.csv
+    - condition1_sample01_<cell|label>_density_data_RH.csv
+    - ...
+
+Columns in the .csv files:
+    sample, cluster_ID, <cell_count|label_volume>, cluster_volume, <cell_density|label_density>, ...
+
+Outputs saved in ./cluster_validation_summary/
+"""
+
+import argparse
+import pandas as pd
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-ids', '--valid_cluster_ids', help='Space-separated list of valid cluster IDs to include in the summary.', nargs='+', type=int, required=True, action=SM) + parser.add_argument('-sa', '--save_all', help='Also save CSVs w/ cell_count|label_volume and cluster_volume data', action='store_true', default=False) + parser.add_argument('-p', '--path', help='Path to the directory containing the CSV files from ``cluster_validation``. Default: current directory', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def sort_samples(sample_names): + # Extract the numeric part of the sample names and sort by it + return sorted(sample_names, key=lambda x: int(''.join(filter(str.isdigit, x))))
+ + +
+[docs] +def generate_summary_table(csv_files, data_column_name): + # Create a dictionary to hold data for each condition + data_by_condition = {} + + # Check if any files contain hemisphere indicators + has_hemisphere = any('_LH.csv' in str(file) or '_RH.csv' in str(file) for file in csv_files) + + # Loop through each file in the working directory + for file in csv_files: + + # Extract the condition and sample name + parts = str(Path(file).name).split('_') + condition = parts[0] + sample = parts[1] + + if has_hemisphere: + # if has_hemisphere, pool data from LH and RH files + if str(file).endswith('_RH.csv'): + continue # Skip RH files + + if str(file).endswith('_LH.csv'): + LH_df = pd.read_csv(file, usecols=['sample', 'cluster_ID', data_column_name]) + + if not Path(str(file).replace('_LH.csv', '_RH.csv')).exists(): + print(f"[red] {Path(str(file).replace('_LH.csv', '_RH.csv'))} is missing") + with open(file.parent / "missing_csv_files.txt", 'a') as f: + f.write(f"{Path(str(file).replace('_LH.csv', '_RH.csv'))} is missing") + import sys ; sys.exit() + + RH_df = pd.read_csv(str(file).replace('_LH.csv', '_RH.csv'), usecols=['sample', 'cluster_ID', data_column_name]) + + # Sum the data_col of the LH and RH dataframes + df = pd.concat([LH_df, RH_df], ignore_index=True).groupby(['sample', 'cluster_ID']).agg( # Group by sample and cluster_ID + **{data_column_name: pd.NamedAgg(column=data_column_name, aggfunc='sum')} # Sum cell_count or label_volume, unpacking the dict into keyword arguments for the .agg() method + ).reset_index() # Reset the index to avoid a multi-index dataframe + + else: + # Load the CSV file into a pandas dataframe + df = pd.read_csv(file, usecols=['sample', 'cluster_ID', data_column_name]) + + # Set the cluster_ID as index and select the density column + df.set_index('cluster_ID', inplace=True) + df = df[[data_column_name]] + + # Rename the density column with the sample name to avoid column name collision during concat + df.rename(columns={data_column_name: sample}, inplace=True) + + # If the condition is not already in the dictionary, initialize it with the dataframe + if condition not in data_by_condition: + data_by_condition[condition] = df + else: + # Concatenate the new dataframe with the existing one for the same condition + data_by_condition[condition] = pd.concat([data_by_condition[condition], df], axis=1) + + # Loop through each condition and sort the columns by sample number + for condition in data_by_condition: + # Get current columns for the condition + current_columns = data_by_condition[condition].columns + # Sort the columns + sorted_columns = sort_samples(current_columns) + # Reindex the DataFrame with the sorted columns + data_by_condition[condition] = data_by_condition[condition][sorted_columns] + + # Concatenate all condition dataframes side by side + all_conditions_df = pd.concat(data_by_condition.values(), axis=1, keys=data_by_condition.keys()) + + # Reset the index so that 'Cluster_ID' becomes a column + all_conditions_df.reset_index(inplace=True) + + return all_conditions_df
+ + + +
+[docs] +def main(): + args = parse_args() + + path = Path(args.path) if args.path else Path.cwd() + + # Load all .csv files + csv_files = list(path.glob('*.csv')) + + if not csv_files: + print(f"\n[red] No CSV files found in {path}.[/]") + import sys ; sys.exit() + + # Load the first .csv file to check for data columns and set the appropriate column names + first_df = pd.read_csv(csv_files[0]) + if 'cell_count' in first_df.columns: + data_col, density_col = 'cell_count', 'cell_density' + elif 'label_volume' in first_df.columns: + data_col, density_col = 'label_volume', 'label_density' + else: + print("Error: Unrecognized data columns in input files.") + return + + # Generate a summary table for the cell_count or label_volume data + data_col_summary_df = generate_summary_table(csv_files, data_col) + + # Generate a summary table for the cluster volume data + cluster_volume_summary_df = generate_summary_table(csv_files, 'cluster_volume') + + # Generate a summary table for the cell_density or label_density data + density_col_summary_df = generate_summary_table(csv_files, density_col) + + # Exclude clusters that are not in the list of valid clusters + if args.valid_cluster_ids is not None: + data_col_summary_df = data_col_summary_df[data_col_summary_df['cluster_ID'].isin(args.valid_cluster_ids)] + cluster_volume_summary_df = cluster_volume_summary_df[cluster_volume_summary_df['cluster_ID'].isin(args.valid_cluster_ids)] + density_col_summary_df = density_col_summary_df[density_col_summary_df['cluster_ID'].isin(args.valid_cluster_ids)] + + # Sort data frames such that the 'cluster_ID' column matches the order of clusters in args.valid_cluster_ids + data_col_summary_df = data_col_summary_df.sort_values(by='cluster_ID', key=lambda x: x.map({cluster: i for i, cluster in enumerate(args.valid_cluster_ids)})) + cluster_volume_summary_df = cluster_volume_summary_df.sort_values(by='cluster_ID', key=lambda x: x.map({cluster: i for i, cluster in enumerate(args.valid_cluster_ids)})) + density_col_summary_df = density_col_summary_df.sort_values(by='cluster_ID', key=lambda x: x.map({cluster: i for i, cluster in enumerate(args.valid_cluster_ids)})) + + # Sum each column in the summary tables other than the 'cluster_ID' column, which could be dropped + data_col_summary_df_sum = data_col_summary_df.sum() + cluster_volume_summary_df_sum = cluster_volume_summary_df.sum() + + # Calculate the density sum from the sum of the cell_count or label_volume and cluster_volume sums + if 'cell_count' in first_df.columns: + density_col_summary_df_sum = data_col_summary_df_sum / cluster_volume_summary_df_sum + elif 'label_volume' in first_df.columns: + density_col_summary_df_sum = data_col_summary_df_sum / cluster_volume_summary_df_sum * 100 + + # Organize the df like the original summary tables + multi_index = data_col_summary_df.columns + density_col_summary_df_sum.columns = multi_index + density_col_summary_df_sum = density_col_summary_df_sum.drop('cluster_ID').reset_index().T + + # Make output dir + output_dir = path / '_prism' + Path(output_dir).mkdir(exist_ok=True) + + # Save the summary tables to .csv files + if args.save_all: + data_col_summary_df.to_csv(output_dir / f'{data_col}_summary.csv', index=False) + cluster_volume_summary_df.to_csv(output_dir / 'cluster_volume_summary.csv', index=False) + + if args.valid_cluster_ids is not None: + density_col_summary_df.to_csv(output_dir / f'{density_col}_summary_for_valid_clusters.csv', index=False) + density_col_summary_df_sum.to_csv(output_dir / f'{density_col}_summary_across_valid_clusters.csv', index=False) + else: + density_col_summary_df.to_csv(output_dir / f'{density_col}_summary.csv', index=False) + density_col_summary_df_sum.to_csv(output_dir / f'{density_col}_summary_across_clusters.csv', index=False) + + print(f"\n Saved results in [bright_magenta]{output_dir}")
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.html new file mode 100644 index 00000000..aebc7a8c --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.html @@ -0,0 +1,502 @@ + + + + + + + + + + unravel.cluster_stats.recursively_mirror_rev_cluster_indices — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.recursively_mirror_rev_cluster_indices

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_mirror_indices`` from UNRAVEL to recursively process img.nii.gz files, apply mirroring, and save new files.
+
+Usage
+-----
+    cluster_mirror_indices -m RH -v
+    
+Use this command after ``cluster_fdr`` to mirror the cluster indices for the other side of the brain before running ``cluster_validation``.  
+"""
+
+import argparse
+import numpy as np
+import nibabel as nib
+import shutil
+from pathlib import Path
+from rich.traceback import install
+from concurrent.futures import ThreadPoolExecutor
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times
+from unravel.voxel_stats.mirror import mirror
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-m', '--mas_side', help='Side of the brain corresponding to the mask used for ``vstats`` and ``cluster_fdr`` (RH or LH)', choices=['RH', 'LH'], required=True, action=SM) + parser.add_argument('-p', '--pattern', help='Glob pattern to match files. Default: **/*rev_cluster_index.nii.gz', default='**/*rev_cluster_index.nii.gz', action=SM) + parser.add_argument('-ax', '--axis', help='Axis to flip the image along. Default: 0', default=0, type=int, action=SM) + parser.add_argument('-s', '--shift', help='Number of voxels to shift content after flipping. Default: 2', default=2, type=int, action=SM) + parser.add_argument('-v', '--verbose', action='store_true', help='Increase verbosity') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: adapt to work with CCFv3 images if needed + + +
+[docs] +def process_file(file_path, args): + if not file_path.is_file(): + return + + basename = str(file_path.name).replace('.nii.gz', '') + new_file_path = file_path.parent / f"{basename}_{args.mas_side}.nii.gz" + shutil.copy(file_path, new_file_path) + + nii = nib.load(str(file_path)) + img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze() + mirrored_img = mirror(img, axis=args.axis, shift=args.shift) + mirrored_nii = nib.Nifti1Image(mirrored_img, nii.affine, nii.header) + + mirrored_filename = file_path.parent / f"{basename}_{'LH' if args.mas_side == 'RH' else 'RH'}.nii.gz" + nib.save(mirrored_nii, mirrored_filename)
+ + +
+[docs] +def main(): + args = parse_args() + + root_path = Path().resolve() + files = list(root_path.glob(args.pattern)) + + with ThreadPoolExecutor() as executor: + executor.map(lambda file: process_file(file, args), files)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/stats.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/stats.html new file mode 100644 index 00000000..5cade26e --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/stats.html @@ -0,0 +1,881 @@ + + + + + + + + + + unravel.cluster_stats.stats — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.stats

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_stats`` from UNRAVEL to validate clusters based on differences in cell/object or label density w/ t-tests.    
+
+T-test usage:  
+------------- 
+    cluster_stats --groups <group1> <group2> -hg <group1|group2>
+
+Tukey's test usage: 
+-------------------
+    cluster_stats --groups <group1> <group2> <group3> <group4> ... -hg <group1|group2>
+
+Note: 
+    - Organize data in directories for each comparison (e.g., psilocybin > saline, etc.)
+    - This script will loop through all directories in the current working dir and process the data in each subdir.
+    - Each subdir should contain .csv files with the density data for each cluster.
+    - The first 2 groups reflect the main comparison for validation rates.
+    - Clusters are not considered valid if the effect direction does not match the expected direction.
+
+Input files: 
+    <asterisk>_density_data.csv from ``cluster_validation`` (e.g., in each subdir named after the rev_cluster_index.nii.gz file)    
+
+CSV naming conventions:
+    - Condition: first word before '_' in the file name
+    - Side: last word before .csv (LH or RH)
+
+Example unilateral inputs in the subdirs:
+    - condition1_sample01_<cell|label>_density_data.csv 
+    - condition1_sample02_<cell|label>_density_data.csv
+    - condition2_sample03_<cell|label>_density_data.csv
+    - condition2_sample04_<cell|label>_density_data.csv
+
+Example bilateral inputs (if any file has _LH.csv or _RH.csv, the command will attempt to pool data):
+    - condition1_sample01_<cell|label>_density_data_LH.csv
+    - condition1_sample01_<cell|label>_density_data_RH.csv
+
+Examples:
+    - Grouping data by condition prefixes: 
+        ``cluster_stats`` --groups psilocybin saline --condition_prefixes saline psilocybin
+        - This will treat all 'psilocybin*' conditions as one group and all 'saline*' conditions as another
+        - Since there will then effectively be two conditions in this case, they will be compared using a t-test
+
+Columns in the .csv files:
+    sample, cluster_ID, <cell_count|label_volume>, cluster_volume, <cell_density|label_density>, ...
+
+Outputs:
+    - ./_valid_clusters_stats/
+"""
+
+import argparse
+import numpy as np
+import pandas as pd
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+from rich.live import Live
+from scipy.stats import ttest_ind
+from statsmodels.stats.multicomp import pairwise_tukeyhsd
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import initialize_progress_bar, print_cmd_and_times
+from unravel.cluster_stats.stats_table import cluster_summary
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('--groups', help='List of group prefixes. 2 groups --> t-test. >2 --> Tukey\'s tests (The first 2 groups reflect the main comparison for validation rates)', nargs='+', required=True) + parser.add_argument('-cp', '--condition_prefixes', help='Condition prefixes to group data (e.g., see info for examples)', nargs='*', default=None, action=SM) + parser.add_argument('-hg', '--higher_group', help='Specify the group that is expected to have a higher mean based on the direction of the p value map', required=True) + parser.add_argument('-alt', "--alternate", help="Number of tails and direction ('two-sided' [default], 'less' [group1 < group2], or 'greater')", default='two-sided', action=SM) + parser.add_argument('-pvt', '--p_val_txt', help='Name of the file w/ the corrected p value thresh (e.g., from cluster_fdr). Default: p_value_threshold.txt', default='p_value_threshold.txt', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +# TODO: Test grouping of conditions. Test w/ label densities data. Could set up dunnett's tests and/or holm sidak tests. + + +
+[docs] +def condition_selector(df, condition, unique_conditions, condition_column='Conditions'): + """Create a condition selector to handle pooling of data in a DataFrame based on specified conditions. + This function checks if the 'condition' is exactly present in the 'Conditions' column or is a prefix of any condition in this column. + If the exact condition is found, it selects those rows. + If the condition is a prefix (e.g., 'saline' matches 'saline-1', 'saline-2'), it selects all rows where the 'Conditions' column starts with this prefix. + An error is raised if the condition is neither found as an exact match nor as a prefix. + + Args: + df (pd.DataFrame): DataFrame whose 'Conditions' column contains the conditions of interest. + condition (str): The condition or prefix of interest. + unique_conditions (list): List of unique conditions in the 'Conditions' column to validate against. + + Returns: + pd.Series: A boolean Series to select rows based on the condition.""" + + if condition in unique_conditions: + return (df[condition_column] == condition) + elif any(cond.startswith(condition) for cond in unique_conditions): + return df[condition_column].str.startswith(condition) + else: + raise ValueError(f" [red]Condition {condition} not recognized!")
+ + +
+[docs] +def cluster_validation_data_df(density_col, has_hemisphere, csv_files, groups, data_col, data_col_pooled, condition_prefixes=None): + """Aggregate the data from all .csv files, pool bilateral data if hemispheres are present, optionally pool data by condition, and return the DataFrame. + + Args: + - density_col (str): the column name for the density data + - has_hemisphere (bool): whether the data files contain hemisphere indicators (e.g., _LH.csv or _RH.csv) + - csv_files (list): a list of .csv files + - groups (list): a list of group names + - data_col (str): the column name for the data (cell_count or label_volume) + - data_col_pooled (str): the column name for the pooled data + + Returns: + - data_df (pd.DataFrame): the DataFrame containing the cluster data + - Columns: 'condition', 'sample', 'cluster_ID', 'cell_count', 'cluster_volume', 'cell_density'""" + + # Create a results dataframe + data_df = pd.DataFrame(columns=['condition', 'sample', 'side', 'cluster_ID', data_col, 'cluster_volume', density_col]) + + if has_hemisphere: + # Process files with hemisphere pooling + print(f"Organizing [red1 bold]bilateral[/red1 bold] [dark_orange bold]{density_col}[/] data from [orange1 bold]_LH.csv[/] and [orange1 bold]_RH.csv[/] files...") + for file in csv_files: + condition_name = str(file.name).split('_')[0] + if condition_name in groups: + side = str(file.name).split('_')[-1].split('.')[0] + df = pd.read_csv(file) + df = df.drop(columns=['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']) + df['condition'] = condition_name # Add the condition to the df + df['side'] = side # Add the side + data_df = pd.concat([data_df, df], ignore_index=True) + + # Pool data by condition, sample, and cluster_ID + data_df = data_df.groupby(['condition', 'sample', 'cluster_ID']).agg( # Group by condition, sample, and cluster_ID + **{data_col_pooled: pd.NamedAgg(column=data_col, aggfunc='sum'), # Sum cell_count or label_volume, unpacking the dict into keyword arguments for the .agg() method + 'pooled_cluster_volume': pd.NamedAgg(column='cluster_volume', aggfunc='sum')} # Sum cluster_volume + ).reset_index() # Reset the index to avoid a multi-index dataframe + + data_df[density_col] = data_df[data_col_pooled] / data_df['pooled_cluster_volume'] # Add a column for cell/label density + else: + # Process files without hemisphere pooling + print(f"Organizing [red1 bold]unilateral[/] [dark_orange bold]{density_col}[/] data...") + for file in csv_files: + df = pd.read_csv(file) + condition_name = file.stem.split('_')[0] + if condition_name in groups: + df['condition'] = str(file.name).split('_')[0] + df = df.drop(columns=[data_col, 'cluster_volume', 'xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']) + data_df = pd.concat([data_df, df], ignore_index=True) + + if condition_prefixes is not None: + unique_conditions = data_df['condition'].unique().tolist() + print(f"Unique conditions before grouping with condition_prefixes: {unique_conditions}") + + # Iterate over the condition prefixes + for condition_prefix in condition_prefixes: + # Adjust condition selectors based on potential pooling (return a boolean Series to select rows based on the condition) + cond_selector = condition_selector(data_df, condition_prefix, unique_conditions) + + # Update the conditions in 'condition' column to reflect the pooled conditions + data_df.loc[cond_selector, 'condition'] = condition_prefix + + unique_conditions = data_df['condition'].unique().tolist() + print(f"Unique conditions after grouping with condition_prefixes: {unique_conditions}") + + return data_df
+ + +
+[docs] +def valid_clusters_t_test(df, group1, group2, density_col, alternative='two-sided'): + """Perform unpaired t-tests for each cluster in the DataFrame and return the results as a DataFrame. + + Args: + - df (pd.DataFrame): the DataFrame containing the cluster data + - Columns: 'condition', 'sample', 'cluster_ID', 'cell_count', 'cluster_volume', 'cell_density' + - group1 (str): the name of the first group + - group2 (str): the name of the second group + - density_col (str): the column name for the density data + - alternative (str): the alternative hypothesis ('two-sided', 'less', or 'greater') + + Returns: + - stats_df (pd.DataFrame): the DataFrame containing the t-test results + - Columns: 'cluster_ID', 'comparison', 'higher_mean_group', 'p-value', 'significance' + """ + + stats_df = pd.DataFrame() + for cluster_id in df['cluster_ID'].unique(): + cluster_data = df[df['cluster_ID'] == cluster_id] + group1_data = np.array([value for value in cluster_data[cluster_data['condition'] == group1][density_col].values.ravel()]) + group2_data = np.array([value for value in cluster_data[cluster_data['condition'] == group2][density_col].values.ravel()]) + + # Perform unpaired two-tailed t-test + t_stat, p_value = ttest_ind(group1_data, group2_data, equal_var=True, alternative=alternative) + p_value = float(f"{p_value:.6f}") + + # Create a temporary DataFrame for the current t-test result + temp_df = pd.DataFrame({'cluster_ID': [cluster_id], 'p-value': [p_value]}) + + # Use pd.concat to append the temporary DataFrame + stats_df = pd.concat([stats_df, temp_df], ignore_index=True) + + # Add a column the higher mean group + stats_df['group1'] = group1 # Add columns for the group names + stats_df['group2'] = group2 + stats_df['comparison'] = stats_df['group1'] + ' vs ' + stats_df['group2'] + stats_df['group1_mean'] = stats_df['cluster_ID'].apply(lambda cluster_id: df[(df['cluster_ID'] == cluster_id) & (df['condition'] == group1)][density_col].mean()) + stats_df['group2_mean'] = stats_df['cluster_ID'].apply(lambda cluster_id: df[(df['cluster_ID'] == cluster_id) & (df['condition'] == group2)][density_col].mean()) + stats_df['meandiff'] = stats_df['group1_mean'] - stats_df['group2_mean'] + stats_df['higher_mean_group'] = stats_df['meandiff'].apply(lambda diff: group1 if diff > 0 else group2) + stats_df['significance'] = stats_df['p-value'].apply(lambda p: '****' if p < 0.0001 else '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else 'n.s.') + + # Update columns + stats_df.drop(columns=['group1_mean', 'group2_mean', 'meandiff', 'group1', 'group2'], inplace=True) + stats_df = stats_df[['cluster_ID', 'comparison', 'higher_mean_group', 'p-value', 'significance']] + + return stats_df
+ + +
+[docs] +def perform_tukey_test(df, groups, density_col): + """Perform Tukey's HSD test for each cluster in the DataFrame and return the results as a DataFrame + + Args: + - df (pd.DataFrame): the DataFrame containing the cluster data + - Columns: 'condition', 'sample', 'cluster_ID', 'cell_count', 'cluster_volume', 'cell_density' + - groups (list): a list of group names + - density_col (str): the column name for the density data + + Returns: + - stats_df (pd.DataFrame): the DataFrame containing the Tukey's HSD test results + - Columns: 'cluster_ID', 'comparison', 'higher_mean_group', 'p-value', 'significance' + """ + + stats_df = pd.DataFrame() + progress, task_id = initialize_progress_bar(len(df['cluster_ID'].unique()), "[default]Processing clusters...") + with Live(progress): + for cluster_id in df['cluster_ID'].unique(): + cluster_data = df[df['cluster_ID'] == cluster_id] + if not cluster_data.empty: + # Flatten the data + densities = np.array([value for value in cluster_data[density_col].values.ravel()]) + groups = np.array([value for value in cluster_data['condition'].values.ravel()]) + + # Perform Tukey's HSD test + tukey_results = pairwise_tukeyhsd(endog=densities, groups=groups, alpha=0.05) + + # Extract significant comparisons from Tukey's results + # Columns: group1, group2, meandiff, p-adj, lower, upper, reject, cluster_ID + test_results_df = pd.DataFrame(data=tukey_results.summary().data[1:], columns=tukey_results.summary().data[0]) + + # Add the cluster ID to the DataFrame + test_results_df['cluster_ID'] = cluster_id + + # Add a column for the group with the higher mean density + test_results_df['higher_mean_group'] = test_results_df.apply(lambda row: row['group1'] if row['meandiff'] < 0 else row['group2'], axis=1) + + # Append the current test results to the overall DataFrame + stats_df = pd.concat([stats_df, test_results_df], ignore_index=True) + + progress.update(task_id, advance=1) + + # Update columns + stats_df.rename(columns={'p-adj': 'p-value'}, inplace=True) + stats_df['comparison'] = stats_df['group1'] + ' vs ' + stats_df['group2'] + stats_df.drop(columns=['lower', 'upper', 'reject', 'meandiff', 'group1', 'group2'], inplace=True) + stats_df['significance'] = stats_df['p-value'].apply(lambda p: '****' if p < 0.0001 else '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else 'n.s.') + stats_df = stats_df[['cluster_ID', 'comparison', 'higher_mean_group', 'p-value', 'significance']] + + return stats_df
+ + + +
+[docs] +def main(): + args = parse_args() + current_dir = Path.cwd() + + # Check for subdirectories in the current working directory + subdirs = [d for d in current_dir.iterdir() if d.is_dir()] + if not subdirs: + print(f" [red1]No directories found in the current working directory: {current_dir}") + return + if subdirs[0].name == '_valid_clusters_stats': + print(f" [red1]Only the '_valid_clusters_stats' directory found in the current working directory: {current_dir}") + print(" [red1]The script was likely run from a subdirectory instead of a directory containing subdirectories.") + return + + # Iterate over all subdirectories in the current working directory + for subdir in subdirs: + print(f"\nProcessing directory: [default bold]{subdir.name}[/]") + + # Load all .csv files in the current subdirectory + csv_files = list(subdir.glob('*.csv')) + if not csv_files: + continue # Skip directories with no CSV files + + # Make output dir + output_dir = Path(subdir) / '_valid_clusters_stats' + output_dir.mkdir(exist_ok=True) + validation_info_csv = output_dir / 'cluster_validation_info_t-test.csv' if len(args.groups) == 2 else output_dir / 'cluster_validation_info_tukey.csv' + if validation_info_csv.exists(): + continue + + # Load the first .csv file to check for data columns and set the appropriate column names + first_df = pd.read_csv(csv_files[0]) + if 'cell_count' in first_df.columns: + data_col, data_col_pooled, density_col = 'cell_count', 'pooled_cell_count', 'cell_density' + elif 'label_volume' in first_df.columns: + data_col, data_col_pooled, density_col = 'label_volume', 'pooled_label_volume', 'label_density' + else: + print("Error: Unrecognized data columns in input files.") + return + + # Get the total number of clusters + total_clusters = len(first_df['cluster_ID'].unique()) + + # Check if any files contain hemisphere indicators + has_hemisphere = any('_LH.csv' in str(file.name) or '_RH.csv' in str(file.name) for file in csv_files) + + # Aggregate the data from all .csv files and pool the data if hemispheres are present + data_df = cluster_validation_data_df(density_col, has_hemisphere, csv_files, args.groups, data_col, data_col_pooled, args.condition_prefixes) + if data_df.empty: + print(" [red1]No data files match the specified groups. The prefixes of the csv files must match the group names.") + continue + + # Check the number of groups and perform the appropriate statistical test + if len(args.groups) == 2: + # Perform a t-test + if args.alternate not in ['two-sided', 'less', 'greater']: + print("Error: Invalid alternative hypothesis. Please specify 'two-sided', 'less', or 'greater'.") + return + elif args.alternate == 'two-sided': + print(f"Running [gold1 bold]{args.alternate} unpaired t-tests") + else: + print(f"Running [gold1 bold]one-sided unpaired t-tests") + stats_df = valid_clusters_t_test(data_df, args.groups[0], args.groups[1], density_col, args.alternate) + else: + # Perform a Tukey's test + print(f"Running [gold1 bold]Tukey's tests") + stats_df = perform_tukey_test(data_df, args.groups, density_col) + + # Validate the clusters based on the expected direction of the effect + if args.higher_group not in args.groups: + print(f" [red1]Error: The specified higher group '{args.higher_group}' is not one of the groups.") + return + expected_direction = '>' if args.higher_group == args.groups[0] else '<' + incongruent_clusters = stats_df[(stats_df['higher_mean_group'] != args.higher_group) & (stats_df['significance'] != 'n.s.')]['cluster_ID'].tolist() + + with open(output_dir / 'incongruent_clusters.txt', 'w') as f: + f.write('\n'.join(map(str, incongruent_clusters))) + + print(f"Expected effect direction: [green bold]{args.groups[0]} {expected_direction} {args.groups[1]}") + + if not incongruent_clusters: + print("All significant clusters are congruent with the expected direction") + else: + print(f"{len(incongruent_clusters)} of {total_clusters} clusters are incongruent with the expected direction.") + print (f"Although they had a significant difference, they not considered valid.") + print (f"'incongruent_clusters.txt' lists cluster IDs for incongruent clusters.") + + # Invalidate clusters that are incongruent with the expected direction + stats_df['significance'] = stats_df.apply(lambda row: 'n.s.' if row['cluster_ID'] in incongruent_clusters else row['significance'], axis=1) + + # Remove invalidated clusters from the list of significant clusters + significant_clusters = stats_df[stats_df['significance'] != 'n.s.']['cluster_ID'] + significant_cluster_ids = significant_clusters.unique().tolist() + significant_cluster_ids_str = ' '.join(map(str, significant_cluster_ids)) + + # Save the results to a .csv file + stats_results_csv = output_dir / 't-test_results.csv' if len(args.groups) == 2 else output_dir / 'tukey_results.csv' + stats_df.to_csv(stats_results_csv, index=False) + + # Extract the FDR q value from the first csv file (float after 'FDR' or 'q' in the file name) + first_csv_name = csv_files[0] + fdr_q = float(str(first_csv_name).split('FDR')[-1].split('q')[-1].split('_')[0]) + + # Extract the p-value threshold from the specified .txt file + try: + p_val_txt = next(Path(subdir).glob('**/*' + args.p_val_txt), None) + if p_val_txt is None: + # If no file is found, print an error message and skip further processing for this directory + print(f" [red1]No p-value file found matching '{args.p_val_txt}' in directory {subdir}. Please check the file name and path.") + import sys ; sys.exit() + with open(p_val_txt, 'r') as f: + p_value_thresh = float(f.read()) + except Exception as e: + # Handle other exceptions that may occur during file opening or reading + print(f"An error occurred while processing the p-value file: {e}") + import sys ; sys.exit() + + # Print validation info: + print(f"FDR q: [cyan bold]{fdr_q}[/] == p-value threshold: [cyan bold]{p_value_thresh}") + print(f"Valid cluster IDs: {significant_cluster_ids_str}") + print(f"[default]# of valid / total #: [bright_magenta]{len(significant_cluster_ids)} / {total_clusters}") + validation_rate = len(significant_cluster_ids) / total_clusters * 100 + print(f"Cluster validation rate: [purple bold]{validation_rate:.2f}%") + + # Save the raw data dataframe as a .csv file + raw_data_csv_prefix = output_dir / 'raw_data_for_t-test' if len(args.groups) == 2 else output_dir / 'raw_data_for_tukey' + if has_hemisphere: + data_df.to_csv(output_dir / f'{raw_data_csv_prefix}_pooled.csv', index=False) + else: + data_df.to_csv(output_dir / f'{raw_data_csv_prefix}.csv', index=False) + + # Save the # of sig. clusters, total clusters, and cluster validation rate to a .txt file + validation_inf_txt = output_dir / 'cluster_validation_info_t-test.txt' if len(args.groups) == 2 else output_dir / 'cluster_validation_info_tukey.txt' + with open(validation_inf_txt, 'w') as f: + f.write(f"Direction: {args.groups[0]} {expected_direction} {args.groups[1]}\n") + f.write(f"FDR q: {fdr_q} == p-value threshold {p_value_thresh}\n") + f.write(f"Valid cluster IDs: {significant_cluster_ids_str}\n") + f.write(f"# of valid / total #: {len(significant_cluster_ids)} / {total_clusters}\n") + f.write(f"Cluster validation rate: {validation_rate:.2f}%\n") + + # Save the valid cluster IDs to a .txt file + valid_cluster_IDs = output_dir / 'valid_cluster_IDs_t-test.txt' if len(args.groups) == 2 else output_dir / 'valid_cluster_IDs_tukey.txt' + with open(valid_cluster_IDs, 'w') as f: + f.write(significant_cluster_ids_str) + + # Save cluster validation info for ``cluster_summary`` + data_df = pd.DataFrame({ + 'Direction': [f"{args.groups[0]} {expected_direction} {args.groups[1]}"], + 'FDR q': [fdr_q], + 'P value thresh': [p_value_thresh], + 'Valid clusters': [significant_cluster_ids_str], + '# of valid clusters': [len(significant_cluster_ids)], + '# of clusters': [total_clusters], + 'Validation rate': [f"{len(significant_cluster_ids) / total_clusters * 100}%"] + }) + + data_df.to_csv(validation_info_csv, index=False) + + # Concat all cluster_validation_info.csv files + if len(args.groups) == 2: + cluster_summary('cluster_validation_info_t-test.csv', 'cluster_validation_summary_t-test.csv') + else: + cluster_summary('cluster_validation_info_tukey.csv', 'cluster_validation_summary_tukey.csv')
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/stats_table.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/stats_table.html new file mode 100644 index 00000000..7c30874c --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/stats_table.html @@ -0,0 +1,487 @@ + + + + + + + + + + unravel.cluster_stats.stats_table — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.stats_table

+#!/usr/bin/env python3
+
+"""
+Use stats_table.py from UNRAVEL to recursively find and concatenate matching CSVs (e.g., to summarize cluster validation info).
+
+Usage:
+------
+    path/stats_table.py -cp cluster_validation_info.csv -o cluster_validation_summary.csv
+"""
+
+import argparse
+import pandas as pd
+from pathlib import Path
+from glob import glob
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-cp', '--csv_pattern', help="Pattern to match csv files. Default: cluster_validation_results.csv", default='cluster_validation_info.csv', action=SM) + parser.add_argument('-o', '--output', help='path/output.csv. Default: cluster_validation_summary.csv', default='cluster_validation_summary.csv', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def cluster_summary(csv_pattern, output): + # Use glob to find all matching CSV files recursively + csv_files = glob(str(f'**/{csv_pattern}'), recursive=True) + if not csv_files: + print(f"No CSV files found matching the pattern {csv_pattern}.") + return + + # Read and concatenate all matching CSV files + concatenated_df = pd.concat([pd.read_csv(f) for f in csv_files]) + + # Sort by the first and second columns if they exist + if len(concatenated_df.columns) >= 2: + concatenated_df = concatenated_df.sort_values(by=[concatenated_df.columns[0], concatenated_df.columns[1]]) + + # Save the concatenated CSV file + output = Path(output) + output.parent.mkdir(parents=True, exist_ok=True) + concatenated_df.to_csv(output, index=False)
+ + +
+[docs] +def main(): + args = parse_args() + + cluster_summary(args.csv_pattern, args.output)
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/sunburst.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/sunburst.html new file mode 100644 index 00000000..9d3933d3 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/sunburst.html @@ -0,0 +1,583 @@ + + + + + + + + + + unravel.cluster_stats.sunburst — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.sunburst

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_sunburst`` from UNRAVEL to generate a sunburst plot of regional volumes across all levels of the ABA hierarchy.
+
+Usage:
+------ 
+    cluster_sunburst -i path/rev_cluster_index.nii.gz -a path/atlas.nii.gz -v
+
+Prereqs: 
+    - ``cluster_validation`` generates a rev_cluster_index.nii.gz (clusters of significant voxels) and validates them. 
+    - Optional: ``cluster_index`` generates a rev_cluster_index.nii.gz w/ valid clusters.
+    
+Outputs:
+    path/input_sunburst.csv and [input_path/sunburst_RGBs.csv]
+
+Plot region volumes (https://app.flourish.studio/)
+
+Data tab: 
+    Paste in data from csv, categories columns = Depth_<asterisk> columns, Size by = Volumes column
+    
+Preview tab:
+    Hierarchy -> Depth to 10, Colors -> paste RGB codes into Custom overrides
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+import pandas as pd
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='Generate a sunburst plot of regional volumes that cluster comprise across the ABA hierarchy', formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/rev_cluster_index.nii.gz (e.g., with valid clusters)', required=True, action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz (Default: path/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-rgb', '--output_rgb_lut', help='Output sunburst_RGBs.csv if flag provided (for Allen brain atlas coloring)', action='store_true') + parser.add_argument('-v', '--verbose', help='Increase verbosity', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def calculate_regional_volumes(img, atlas, atlas_res_in_um): + """Calculate the volumes of labeled regions in the input image. + + Args: + - img (ndarray): the input image ndarray. + - atlas (ndarray): the atlas ndarray. + - atlas_res_in_um (tuple): the atlas resolution in in microns. + + Returns: + - volumes_dict (dict): a dictionary of region volumes (key = region ID, value = volume in mm^3) + """ + + img[img > 0] = 1 # Binarize input image + img = img.astype(np.int16) + img *= atlas + uniq_values, counts = np.unique(img, return_counts=True) + volumes = (atlas_res_in_um**3 * counts) / 1000000000 # Convert voxel counts to cubic mm + uniq_values = uniq_values[1:] + volumes = volumes[1:] + + return dict(zip(uniq_values, volumes))
+ + +
+[docs] +def sunburst(img, atlas, atlas_res_in_um, output_path, output_rgb_lut=False): + """Generate a sunburst plot of regional volumes that cluster comprise across the ABA hierarchy. + + Args: + - img (ndarray) + - atlas (ndarray) + - atlas_res_in_um (tuple): the atlas resolution in microns. For example, (25, 25, 25) + - output_rgb_lut (bool): flag to output the RGB values for each abbreviation to a CSV file + + Outputs: + - CSV file containing the regional volumes for the sunburst plot (input_sunburst.csv) + """ + + volumes_dict = calculate_regional_volumes(img, atlas, atlas_res_in_um) + + sunburst_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'sunburst_IDPath_Abbrv.csv') + ccf_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'CCFv3_info.csv', usecols=['lowered_ID', 'abbreviation']) + + # Create a mapping from region ID to volume + histo_df = pd.DataFrame(list(volumes_dict.items()), columns=['Region', 'Volume_(mm^3)']) + merged_df = pd.merge(histo_df, ccf_df, left_on='Region', right_on='lowered_ID', how='inner') + + # Determine the maximum depth for each abbreviation + depth_columns = [f'Depth_{i}' for i in range(10)] + sunburst_df['max_depth_abbr'] = sunburst_df[depth_columns].apply(lambda row: row.dropna().iloc[-1], axis=1) + + # Merge the volumes into sunburst_df based on the finest granularity abbreviation + final_df = sunburst_df.merge(merged_df, left_on='max_depth_abbr', right_on='abbreviation', how='left') + + # Drop rows without volume data + final_df = final_df[final_df['Volume_(mm^3)'].notna()] + + # Drop columns not needed for the sunburst plot + final_df.drop(columns=['max_depth_abbr', 'Region', 'lowered_ID', 'abbreviation'], inplace=True) + + # Save the output to a CSV file + final_df.to_csv(output_path, index=False) + + + if output_rgb_lut: + # Save the RGB values for each abbreviation to a CSV file + rgb_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'sunburst_RGBs.csv') + rgb_path = Path(output_path).parent / 'sunburst_RGBs.csv' + rgb_df.to_csv(rgb_path, index=False) + + return final_df
+ + +
+[docs] +def main(): + args = parse_args() + + # Load the input image and convert to numpy array + nii = nib.load(args.input) + img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze() + + # Load the atlas image and convert to numpy array + atlas_nii = nib.load(args.atlas) + atlas = np.asanyarray(atlas_nii.dataobj, dtype=atlas_nii.header.get_data_dtype()).squeeze() + + # Get the atlas resolution + atlas_res = atlas_nii.header.get_zooms() # (x, y, z) in mm + xyx_res_in_mm = atlas_res[0] + xyz_res_in_um = xyx_res_in_mm * 1000 + + output_name = str(Path(args.input).name).replace('.nii.gz', '_sunburst.csv') + output_path = Path(args.input).parent / output_name + + sunburst_df = sunburst(img, atlas, xyz_res_in_um, output_path, args.output_rgb_lut) + + print(f'\n\n[magenta bold]{output_name}[/]:') + print(f'\n{sunburst_df}\n')
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/cluster_stats/table.html b/unravel/docs/_build/html/_modules/unravel/cluster_stats/table.html new file mode 100644 index 00000000..f8b98f79 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/cluster_stats/table.html @@ -0,0 +1,997 @@ + + + + + + + + + + unravel.cluster_stats.table — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.cluster_stats.table

+#!/usr/bin/env python3
+
+"""
+Use ``cluster_table`` from UNRAVEL to summarize volumes of the top x regions and collapsing them into parent regions until a criterion is met.
+
+Usage:
+------
+    cluster_table
+
+Prereqs:
+    ``cluster_index`` has been run. Run this command from the valid_clusters dir. <asterisk>cluster_info.txt in working dir.
+
+Sorting by hierarchy and volume:
+--------------------------------
+Group by Depth: Starting from the earliest depth column, for each depth level:
+   - Sum the volumes of all rows sharing the same region (or combination of regions up to that depth).
+   - Sort these groups by their aggregate volume in descending order, ensuring larger groups are prioritized.
+
+Sort Within Groups: Within each group created in step 1:
+   - Sort the rows by their individual volume in descending order.
+
+Maintain Grouping Order:
+   - As we move to deeper depth levels, maintain the grouping and ordering established in previous steps (only adjusting the order within groups based on the new depth's aggregate volumes).
+"""
+
+
+import argparse
+import openpyxl
+import math
+import numpy as np
+import pandas as pd
+from glob import glob
+from pathlib import Path
+from openpyxl.styles import PatternFill
+from openpyxl.utils import get_column_letter
+from openpyxl.utils.dataframe import dataframe_to_rows
+from openpyxl.styles import Border, Side, Font, Alignment
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-vcd', '--val_clusters_dir', help='Path to the valid_clusters dir output from unravel.cluster_stats.index (else cwd)', action=SM) + parser.add_argument('-t', '--top_regions', help='Number of top regions to output. Default: 4', default=4, type=int, action=SM) + parser.add_argument('-pv', '--percent_vol', help='Percentage of the total volume the top regions must comprise [after collapsing]. Default: 0.8', default=0.8, type=float, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Correct font color for the volumes column. 'fiber tracts' is filled with white rather than the color of the fiber tracts + + +
+[docs] +def fill_na_with_last_known(df): + depth_columns = [col for col in df.columns if 'Depth' in col] + + # Fill NaN with the last known non-NaN value within each row for depth columns + df_filled = df.copy() + df_filled[depth_columns] = df_filled[depth_columns].fillna(method='ffill', axis=1) + + return df_filled
+ + +
+[docs] +def sort_sunburst_hierarchy(df): + """Sort the DataFrame by hierarchy and volume.""" + + depth_columns = [col for col in df.columns if 'Depth' in col] + volume_column = 'Volume_(mm^3)' + + # For each depth, process groups and sort + for i, depth in enumerate(depth_columns): + # Temporary DataFrame to hold sorting results for each depth + sorted_partial = pd.DataFrame() + + # Identify unique groups (rows) up to the current depth (e.g, if Depth_2, then root, grey, CH) + unique_groups = df[depth_columns[:i + 1]].drop_duplicates() + + for _, group_values in unique_groups.iterrows(): + # Filter rows belonging to the current group + mask = (df[depth_columns[:i + 1]] == group_values).all(axis=1) # Boolean series to check which rows == group_values + group_df = df[mask].copy() # Apply mask to df to get a df for each group (copy to avoid SettingWithCopyWarning) + + # Calculate aggregate volume for the group and add it as a new column + group_df.loc[:, 'aggregate_volume'] = group_df[volume_column].sum() + + # Sort the group by individual volume + group_df = group_df.sort_values(by=[volume_column], ascending=False) + + # Append sorted group to the partial result + sorted_partial = pd.concat([sorted_partial, group_df], axis=0) + + # Replace df with the sorted_partial for the next iteration + df = sorted_partial.drop(columns=['aggregate_volume']) + + return df
+ + +
+[docs] +def undo_fill_with_original(df_sorted, df_original): + # Ensure the original DataFrame has not been altered; otherwise, use a saved copy before any modifications + depth_columns = [col for col in df_original.columns if 'Depth' in col] + + # Use the index to replace filled values with original ones where NaN existed + for column in depth_columns: + df_sorted[column] = df_original.loc[df_sorted.index, column] # Use the index to replace filled values with original ones where NaN existed + + return df_sorted
+ + +
+[docs] +def can_collapse(df, depth_col): + """ + Determine if regions in the specified depth column can be collapsed. + Returns a DataFrame with regions that can be collapsed based on volume and count criteria. + """ + volume_column = 'Volume_(mm^3)' + + # Group by the parent region and aggregate both volume and count (.e.g, if a parent has 3 children, the count is 3) + subregion_aggregates = df.groupby(depth_col).agg({volume_column: ['sum', 'count']}) + subregion_aggregates.columns = ['Volume_Sum', 'Count'] + + # Adjust the condition to check for both a volume threshold and a minimum count of subregions + pooling_potential = subregion_aggregates[(subregion_aggregates['Volume_Sum'] > 0) & (subregion_aggregates['Count'] > 1)] + + return pooling_potential # DataFrame with regions that can be collapsed based on volume and count criteria (Depth_*, Volume_Sum, Count)
+ + +
+[docs] +def collapse_hierarchy(df, verbose=False): + volume_column = 'Volume_(mm^3)' + depth_columns = [col for col in df.columns if 'Depth' in col] + + for depth_level in reversed(range(len(depth_columns))): + depth_col = depth_columns[depth_level] + if depth_level == 0: break # Stop if we're at the top level + + # Identify regions that can be collapsed into their parent + collapsible_regions = can_collapse(df, depth_col) + + # If collapsible_regions is not empty, proceed with collapsing + if not collapsible_regions.empty: + + # For each collapsible region name, get the name and the aggregate volume + for region, row in collapsible_regions.iterrows(): + + aggregated_volume = row['Volume_Sum'] + + # Collapse rows containing the region name and set the volume to the aggregate volume + df.loc[df[depth_col] == region, volume_column] = aggregated_volume + + # Set the child region name to NaN in the depth column using the depth level + child_depth_col = depth_columns[depth_level + 1] + df.loc[df[depth_col] == region, child_depth_col] = np.nan + + # Also set any subsequent depth columns to NaN + for subsequent_depth_col in depth_columns[depth_level + 2:]: + df.loc[df[depth_col] == region, subsequent_depth_col] = np.nan + + + # Remove duplicate rows for the collapsed region + df = df.drop_duplicates() + + return df
+ + + +
+[docs] +def calculate_top_regions(df, top_n, percent_vol_threshold, verbose=False): + """ + Identify the top regions based on the dynamically collapsed hierarchy, + ensuring they meet the specified percentage volume criterion. + + :param df_collapsed: DataFrame with the hierarchy collapsed where meaningful. + :param top_n: The number of top regions to identify. + :param percent_vol_threshold: The minimum percentage of total volume these regions should represent. + :return: DataFrame with the top regions and their volumes if the criterion is met; otherwise, None. + """ + # Get the total volume + total_volume = df['Volume_(mm^3)'].sum() + + # Get top regions + df_sorted = df.sort_values(by='Volume_(mm^3)', ascending=False).reset_index(drop=True) + top_regions_df = df_sorted.head(top_n) + + # Sum the volumes of the top regions + top_regions_volume = top_regions_df['Volume_(mm^3)'].sum() + + # Calculate the percentage of the total volume the top regions represent + percent_vol = top_regions_volume / total_volume + + # Check if the top regions meet the percentage volume criterion + if percent_vol >= percent_vol_threshold: + return top_regions_df + else: + return None
+ + +
+[docs] +def get_top_regions_and_percent_vols(sunburst_csv_path, top_regions, percent_vol, verbose=False): + df = pd.read_csv(sunburst_csv_path) + + # Check if the DataFrame is empty print a message and return + if df.empty: + print(f'\n{sunburst_csv_path} is empty. Exiting...') + import sys ; sys.exit() + + # Fill NaN values in the original DataFrame + df_filled_na = fill_na_with_last_known(df.copy()) + + # Sort the DataFrame by hierarchy and volume + df_filled_sorted = sort_sunburst_hierarchy(df_filled_na) + + # Undo the fill with the original values + df_final = undo_fill_with_original(df_filled_sorted, df) + + # Save the sorted DataFrame to a new CSV file + sorted_parent_path = sunburst_csv_path.parent / '_sorted_sunburst_CSVs' + sorted_parent_path.mkdir(parents=True, exist_ok=True) + sorted_csv_name = str(sunburst_csv_path.name).replace('sunburst.csv', 'sunburst_sorted.csv') + df_final.to_csv(sorted_parent_path / sorted_csv_name, index=False) + + # Attempt to calculate top regions, collapsing as necessary + criteria_met = False + while not criteria_met: + top_regions_df = calculate_top_regions(df_final, top_regions, percent_vol, verbose) + + if top_regions_df is not None and not top_regions_df.empty: # If top regions are found + criteria_met = True + + # If a top region contributes to less than 1% of the total volume, remove it + total_volume = df_final['Volume_(mm^3)'].sum() + top_regions_df = top_regions_df[top_regions_df['Volume_(mm^3)'] / total_volume > 0.01] + + # Initialize lists to hold the top region names and their aggregate volumes + top_region_names_and_percent_vols = [] + + # For each top region, get the region name and the aggregate volume + for i, row in top_regions_df.iterrows(): + + # Get the highest depth region name that is not NaN + row_wo_volume = row[:10] + region_name = row_wo_volume[::-1].dropna().iloc[0] + + # Calculate the percentage of the total volume the top regions represent + aggregate_volume = row['Volume_(mm^3)'] + percent_vol = round(aggregate_volume / total_volume * 100) + + # Append the region name and the percentage volume to the list + top_region_names_and_percent_vols.append(f'{region_name} ({percent_vol}%)') + + # Save the top regions DataFrame to a new CSV file + top_regions_parent_path = sunburst_csv_path.parent / '_top_regions_for_each_cluster' + top_regions_parent_path.mkdir(parents=True, exist_ok=True) + top_regions_csv_name = str(sunburst_csv_path.name).replace('sunburst.csv', 'sunburst_top_regions.csv') + top_regions_df.to_csv(top_regions_parent_path / top_regions_csv_name, index=False) + else: + # Attempt to collapse the hierarchy further + df_final = collapse_hierarchy(df_final, verbose) + if df_final.empty: + break # Exit if no further collapsing is possible + + return top_region_names_and_percent_vols, total_volume
+ + +# Function to create fill color based on the volume +
+[docs] +def get_fill_color(value, max_value): + # Apply a log transform to the volume to enhance visibility of smaller values + # Adding 1 to the value to avoid log(0), and another 1 to max_value to ensure the max_value maps to 1 after log transform + log_transformed_value = math.log10(value + 1) + log_transformed_max = math.log10(max_value + 1) + normalized_value = log_transformed_value / log_transformed_max + + # Convert to a scale of 0-255 (for RGB values) + rgb_value = int(normalized_value * 255) + # Create fill color as a hex string + fill_color = f"{rgb_value:02x}{rgb_value:02x}{rgb_value:02x}" + + # Set the font color to black if the fill color is light (more than 127 in RGB scale), otherwise white + font_color = "000000" if rgb_value > 127 else "FFFFFF" + + return fill_color, font_color
+ + + +
+[docs] +def main(): + args = parse_args() + + # Find cluster_* dirs in the current dir + valid_clusters_dir = Path(args.val_clusters_dir) if args.val_clusters_dir else Path.cwd() + cluster_sunburst_csvs = valid_clusters_dir.glob('cluster_*_sunburst.csv') + + # Remove directories from the list + cluster_sunburst_csvs = [f for f in cluster_sunburst_csvs if f.is_file()] + + # Generate dynamic column names based on args.top_regions + column_names = ['Cluster'] + ['Volume'] + ['CoG'] + ['~Region'] + ['ID_Path'] + [f'Top_Region_{i+1}' for i in range(args.top_regions)] + + # Load the *cluster_info.txt file from the parent dir to get the cluster Centroid of Gravity (CoG) + cluster_info_txt_parent = Path(args.val_clusters_dir).parent if args.val_clusters_dir else Path.cwd() + cluster_info_files = list(cluster_info_txt_parent.glob('*cluster_info.txt')) + + # If no cluster_info.txt file is found, exit + if not cluster_info_files: + print(f'\n [red]No *cluster_info.txt file found in {cluster_info_txt_parent}. Exiting...') + return + else: + cluster_info_file = cluster_info_files[0] # first match + + # Load the cluster info file + cluster_info_df = pd.read_csv(cluster_info_file, sep='\t', header=None) # Assuming tab-separated values; adjust if different + + # Reverse the row order of only the first column (excluding the header) + first_column_name = cluster_info_df.columns[0] + reversed_data = cluster_info_df[first_column_name].iloc[1:].iloc[::-1].reset_index(drop=True) # Reverse the data + new_first_column = pd.concat([cluster_info_df[first_column_name].iloc[:1], reversed_data]) # Concat header w/ reversed data + cluster_info_df[first_column_name] = new_first_column.values # Assign the new column back to the DataFrame + + # Get the CoG for each cluster (values in last three columns of each row) + CoGs = cluster_info_df.iloc[:, -3:].values + + # Convert to this format: 'x,y,z' + CoGs = [','.join(map(str, CoG)) for CoG in CoGs] + + # Create a dict w/ the first column as keys and the CoG as values + cluster_CoGs = dict(zip(cluster_info_df[first_column_name], CoGs)) + + # Create an empty DataFrame with the column names + top_regions_and_percent_vols_df = pd.DataFrame(columns=column_names) + + # Specify the column names you want to load + columns_to_load = ['abbreviation', 'general_region', 'structure_id_path'] + + # Load the specified columns from the CSV with CCFv3 info + ccfv3_info_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'CCFv3_info.csv', usecols=columns_to_load) + + # For each cluster directory + for cluster_sunburst_csv in cluster_sunburst_csvs: + + # Get the cluster number from the file name 'cluster_*_sunburst.csv' + cluster_num = str(cluster_sunburst_csv.name).split('_')[1] + + # Get the CoG string for the current cluster from the dictionary + cog_string = cluster_CoGs.get(cluster_num) if cluster_CoGs.get(cluster_num) else "Not found" + + # Get the top regions and their percentage volumes for the current cluster + top_regions_and_percent_vols, cluster_volume = get_top_regions_and_percent_vols(cluster_sunburst_csv, args.top_regions, args.percent_vol, args.verbose) + + # Get the top region + top_region = top_regions_and_percent_vols[0].split(' ')[0] + + # Lookup the general_regio and structure_id_path in the DataFrame using the abbreviation for the top region + general_region = ccfv3_info_df.loc[ccfv3_info_df['abbreviation'] == top_region, 'general_region'].values + id_path = ccfv3_info_df.loc[ccfv3_info_df['abbreviation'] == top_region, 'structure_id_path'].values + + # Since there could be multiple matches, we take the first one or a default value if not found + general_region = general_region[0] if len(general_region) > 0 else "General region not found" + id_path = id_path[0] if len(id_path) > 0 else "ID path not found" + + # Ensure the list has the exact number of top regions (pad with None if necessary) + padded_top_regions = (list(top_regions_and_percent_vols) + [None] * args.top_regions)[:args.top_regions] + + # Prepare the row data, including placeholders for 'Volume', 'CoG', '~Region', and top regions + row_data = [cluster_num, cluster_volume, cog_string, general_region, id_path] + padded_top_regions + + # Ensure column_names matches the structure of row_data + column_names = ['Cluster', 'Volume', 'CoG', '~Region', 'ID_Path'] + [f'Top_Region_{i+1}' for i in range(args.top_regions)] + + # Create a temporary DataFrame for the current cluster's data + temp_df = pd.DataFrame([row_data], columns=column_names) + + # Concatenate the temporary DataFrame with the main DataFrame + top_regions_and_percent_vols_df = pd.concat([top_regions_and_percent_vols_df, temp_df], ignore_index=True) + + # Sort the DataFrame by the 'ID_Path' column in descending order + top_regions_and_percent_vols_df = top_regions_and_percent_vols_df.sort_values(by='ID_Path', ascending=False) + + # Drop the 'ID_Path' column + top_regions_and_percent_vols_df = top_regions_and_percent_vols_df.drop(columns=['ID_Path']) + + # Convert the 'Volume' column to 4 decimal places + top_regions_and_percent_vols_df['Volume'] = top_regions_and_percent_vols_df['Volume'].round(4) + print(f'\nThe top regions and their percentage volumes for each cluster:') + print(f'\n{top_regions_and_percent_vols_df.to_string(index=False)}\n') + + # Load csv with RGB values + sunburst_RGBs_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'sunburst_RGBs.csv', header=None) + + # Parse the dataframe to get a dictionary of region names and their corresponding RGB values + rgb_values = {} + for index, row in sunburst_RGBs_df.iterrows(): + # Format is 'region_name: rgb(r,g,b)' + region, rgb = row[0].split(': rgb(') + r, g, b = map(int, rgb.strip(')').split(',')) + rgb_values[region] = (r, g, b) + + # Create an Excel workbook and select the active worksheet + wb = openpyxl.Workbook() + ws = wb.active + + thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) + + # Write each row of the DataFrame to the worksheet and color cells for the top regions + for region in dataframe_to_rows(top_regions_and_percent_vols_df, index=False, header=True): + ws.append(region) + + for i in range(args.top_regions): + + # Find the region name without the percentage to match the RGB values + top_region_column_num = 4 + i + + region_key = None + if region[top_region_column_num] is not None: + region_key = region[top_region_column_num].split(' ')[0] + + # Apply the color to the cell if it matches one of the RGB values + if region_key in rgb_values: + # Convert the RGB to a hex string + rgb = rgb_values[region_key] + hex_color = "{:02x}{:02x}{:02x}".format(rgb[0], rgb[1], rgb[2]) + fill = PatternFill(start_color=hex_color, end_color=hex_color, fill_type='solid') + top_region_column_num = 5 + i + ws.cell(row=ws.max_row, column=top_region_column_num).fill = fill + ws.cell(row=ws.max_row, column=top_region_column_num).border = thin_border + elif region_key is None: + hex_color = "{:02x}{:02x}{:02x}".format(100, 100, 100) # Grey + fill = PatternFill(start_color=hex_color, end_color=hex_color, fill_type='solid') + top_region_column_num = 5 + i + ws.cell(row=ws.max_row, column=top_region_column_num).fill = fill + ws.cell(row=ws.max_row, column=top_region_column_num).border = thin_border + + # Insert a new row at the top + ws.insert_rows(1) + + # Insert a new column at the left + ws.insert_cols(1) + + # Adjust the column width to fit the content + for col in ws.columns: + max_length = 0 + for cell in col: + if cell.value: + max_length = max(max_length, len(str(cell.value))) + if max_length > 0: + adjusted_width = max_length + 2 # Add 2 for a little extra padding + column_letter = openpyxl.utils.get_column_letter(col[0].column) + ws.column_dimensions[column_letter].width = adjusted_width + + # Fill cells in first column with white + for cell in ws['A']: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Fill cells in ninth column with white + column = 6 + args.top_regions + for cell in ws[get_column_letter(column)]: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Fill cells in first row with white + for cell in ws[1]: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Fill cells in last row with white + num_rows = top_regions_and_percent_vols_df.shape[0] + 3 + for cell in ws[num_rows]: + cell.fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') + + # Center align the content + for row in ws.iter_rows(min_row=1, min_col=1): + for cell in row: + cell.alignment = Alignment(horizontal='center', vertical='center') + + # Format column C such that the brightness of the fill is proportional to the volume / total volume and the text brightness is inversely proportional to the fill brightness + # total_volume = top_regions_and_percent_vols_df['Volume'].sum() + volumes = top_regions_and_percent_vols_df['Volume'].tolist() + max_volume = max(volumes) # The largest volume + + for row, volume in enumerate(volumes, start=3): # Starting from row 3 (C3) + # Calculate the color based on the volume + fill_color, font_color = get_fill_color(volume, max_volume) + + # Get the cell at column C and the current row + cell = ws.cell(row=row, column=3) # Column 3 corresponds to column C + + # Set the fill color based on the calculated brightness + cell.fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid") + + # Set the font color based on the inverse of the fill color's brightness + cell.font = Font(color=font_color) + + # Apply a thin border style to cells with content + for row in ws.iter_rows(): + for cell in row: + if cell.value: # If the cell has content + cell.border = thin_border + cell.font = Font(name='Arial', size=11) + + # Apply the font to the header row + header_font = Font(name='Arial', bold=True) + for cell in ws['2:2']: + cell.font = header_font + + # Make column B bold + for cell in ws['B']: + cell.font = Font(name='Arial', bold=True) + + # Additional step to ensure cells from column F onwards are black + for row in ws.iter_rows(min_col=6): + for cell in row: + if cell.font: # If the cell already has font settings applied + cell.font = Font(name='Arial', size=cell.font.size, bold=cell.font.bold, color='FF000000') + else: + cell.font = Font(name='Arial', color='FF000000') + + # Iterate through the cells and merge cells with the same value in column 5 + current_region = None + first_row = None + + # Adjusted min_row to 2 and min_col/max_col to merge_column because of the added padding row and column + merge_column = 5 + for row in ws.iter_rows(min_row=2, max_row=ws.max_row - 1, min_col=merge_column, max_col=merge_column): + cell = row[0] # row[0] since we're only looking at one column, and iter_rows yields tuples + if cell.value != current_region: + # If the cell value changes, merge the previous cells if there are more than one with the same value + if first_row and first_row < cell.row - 1: + ws.merge_cells(start_row=first_row, start_column=merge_column, end_row=cell.row - 1, end_column=merge_column) + # Update the current region and reset the first_row to the current cell's row + current_region = cell.value + first_row = cell.row + + # After the loop, check and merge the last set of cells if needed + if first_row and first_row < ws.max_row: + ws.merge_cells(start_row=first_row, start_column=merge_column, end_row=ws.max_row - 1, end_column=merge_column) + + for cell in ws['E']: + cell.alignment = Alignment(horizontal='center', vertical='center') + + # Save the workbook to a file + excel_file_path = valid_clusters_dir / f'{valid_clusters_dir.parent.name}_valid_clusters_table.xlsx' + wb.save(excel_file_path) + print(f"Excel file saved at {excel_file_path}") + print(f"\nBrighter cell fills in the 'Volume' column represent larger volumes (log(10) scaled and normalized to the max volume).") + + # Get the anatomically sorted list of cluster IDs and save it to a .txt file + valid_cluster_ids = top_regions_and_percent_vols_df['Cluster'].tolist() + valid_cluster_ids_str = ' '.join(map(str, valid_cluster_ids)) + '\n' + with open(valid_clusters_dir / 'valid_cluster_IDs_sorted_by_anatomy.txt', 'w') as f: + f.write(valid_cluster_ids_str)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/core/argparse_utils.html b/unravel/docs/_build/html/_modules/unravel/core/argparse_utils.html new file mode 100644 index 00000000..ddaced65 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/core/argparse_utils.html @@ -0,0 +1,518 @@ + + + + + + + + + + unravel.core.argparse_utils — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.core.argparse_utils

+#!/usr/bin/env python3
+
+"""
+This script defines custom classes to enhance the formatting and handling of argparse arguments,
+with a focus on suppressing metavar display and improving help message readability.
+
+Classes:
+    - SuppressMetavar: A custom HelpFormatter class that suppresses the display of metavar for
+                       arguments and customizes the epilog formatting.
+    - SM: A custom argparse.Action class that suppresses the display of metavar and manages
+          argument values.
+
+Usage:
+    Import the classes and use them in an argparse-based script to suppress metavar and format help
+    messages for improved readability.
+
+Example:
+    import argparse
+    from path.to.this.script import SuppressMetavar, SM
+
+    parser = argparse.ArgumentParser(description="A script example.", formatter_class=SuppressMetavar)
+    parser.add_argument('-e', '--example', help='Example argument', action=SM)
+    args = parser.parse_args()
+
+Classes:
+    SuppressMetavar
+        - Inherits from argparse.HelpFormatter to modify the formatting of action invocations and epilog text.
+        - Methods:
+            - _format_action_invocation: Customizes the formatting of argument options.
+            - _fill_text: Formats the epilog text with specified indentation and width.
+
+    SM
+        - Inherits from argparse.Action to suppress metavar display and manage argument values.
+        - Methods:
+            - __init__: Initializes the custom action and sets the metavar to an empty string or tuple.
+            - __call__: Sets the argument values in the namespace, handling both single and multiple values.
+
+Notes:
+    - This script relies on the argparse library for command-line argument parsing.
+    - The SuppressMetavar class is designed to improve the readability of help messages by suppressing
+      metavar display and customizing the formatting of epilog text.
+"""
+
+import argparse
+import textwrap
+
+
+[docs] +class SuppressMetavar(argparse.HelpFormatter): + def _format_action_invocation(self, action): + if not action.option_strings: + metavar, = self._metavar_formatter(action, action.dest)(1) + return metavar + else: + parts = [] + if action.nargs == 0: + parts.extend(action.option_strings) + else: + for option_string in action.option_strings: + parts.append(option_string) + return ', '.join(parts) + + def _fill_text(self, text, width, indent): + # This method formats the epilog. Override it to split the text into lines and format each line individually. + text_lines = text.splitlines() + formatted_lines = [textwrap.fill(line, width, initial_indent=indent, subsequent_indent=indent) for line in text_lines] + return '\n'.join(formatted_lines)
+ + + +#Suppress metavar +
+[docs] +class SM(argparse.Action): + def __init__(self, option_strings, dest, nargs=None, **kwargs): + if nargs is not None: + kwargs.setdefault('metavar', '') + super(SM, self).__init__(option_strings, dest, nargs=nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # If this action is for a single value (not expecting multiple values) + if self.nargs is None or self.nargs == 0: + setattr(namespace, self.dest, values) # Set the single value directly + else: + # If the action expects multiple values, handle it as a list + if isinstance(values, list): + setattr(namespace, self.dest, values) + else: + current_values = getattr(namespace, self.dest, []) + if not isinstance(current_values, list): + current_values = [current_values] + current_values.append(values) + setattr(namespace, self.dest, current_values)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/core/argparse_utils_rich.html b/unravel/docs/_build/html/_modules/unravel/core/argparse_utils_rich.html new file mode 100644 index 00000000..4d53f299 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/core/argparse_utils_rich.html @@ -0,0 +1,520 @@ + + + + + + + + + + unravel.core.argparse_utils_rich — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.core.argparse_utils_rich

+#!/usr/bin/env python3
+
+"""
+This script defines custom classes to enhance the formatting and handling of argparse arguments
+using the Rich library for beautiful terminal output.
+
+Classes:
+    - SuppressMetavar: A custom RichHelpFormatter class that suppresses the display of metavar for
+                       arguments and customizes the epilog formatting.
+    - SM: A custom argparse.Action class that suppresses the display of metavar across all nargs 
+          configurations and manages argument values.
+
+Usage:
+    Import the classes and use them in an argparse-based script to suppress metavar and format help
+    messages with Rich's styled output.
+
+Example:
+    import argparse
+    from rich_argparse import RichHelpFormatter
+    from path.to.this.script import SuppressMetavar, SM
+
+    parser = argparse.ArgumentParser(description="A script example.", formatter_class=SuppressMetavar)
+    parser.add_argument('-e', '--example', help='Example argument', action=SM)
+    args = parser.parse_args()
+
+Classes:
+    SuppressMetavar
+        - Inherits from RichHelpFormatter to modify the formatting of action invocations and epilog text.
+        - Methods:
+            - _format_action_invocation: Customizes the formatting of argument options.
+            - _fill_text: Formats the epilog text with specified indentation and width.
+
+    SM
+        - Inherits from argparse.Action to suppress metavar display and manage argument values.
+        - Methods:
+            - __init__: Initializes the custom action and sets the metavar to an empty string or tuple.
+            - __call__: Sets the argument values in the namespace, handling both single and multiple values.
+
+Notes:
+    - This script relies on the rich and argparse libraries for enhanced help message formatting.
+    - The SuppressMetavar class is specifically designed to work with Rich's RichHelpFormatter for styled terminal output.
+"""
+
+import argparse
+from rich_argparse import RichHelpFormatter
+import textwrap
+
+
+[docs] +class SuppressMetavar(RichHelpFormatter): + def _format_action_invocation(self, action): + if not action.option_strings: + metavar, = self._metavar_formatter(action, action.dest)(1) + return metavar + else: + parts = [] + if action.nargs == 0: + parts.extend(action.option_strings) + else: + for option_string in action.option_strings: + parts.append(option_string) + return ', '.join(parts) + + def _fill_text(self, text, width, indent): + # This method formats the epilog. Override it to split the text into lines and format each line individually. + text_lines = text.splitlines() + formatted_lines = [textwrap.fill(line, width, initial_indent=indent, subsequent_indent=indent) for line in text_lines] + return '\n'.join(formatted_lines)
+ + +# Custom action class to suppress metavar across all nargs configurations +
+[docs] +class SM(argparse.Action): + def __init__(self, option_strings, dest, nargs=None, **kwargs): + # Forcefully suppress metavar display by setting it to an empty string or an appropriate tuple + if nargs is not None: + # Use an empty tuple with a count matching nargs when nargs is a specific count or '+' + kwargs['metavar'] = tuple('' for _ in range(nargs if isinstance(nargs, int) else 1)) + else: + # Default single metavar suppression + kwargs['metavar'] = '' + super(SM, self).__init__(option_strings, dest, nargs=nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # Simply set the value(s) in the namespace + if self.nargs is None or self.nargs == 0: + setattr(namespace, self.dest, values) # Directly set the value + else: + # Handle multiple values as a list + current_values = getattr(namespace, self.dest, []) + if not isinstance(current_values, list): + current_values = [current_values] # Ensure it is a list + current_values.append(values) + setattr(namespace, self.dest, current_values) # Append new values
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/core/config.html b/unravel/docs/_build/html/_modules/unravel/core/config.html new file mode 100644 index 00000000..00ec71d7 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/core/config.html @@ -0,0 +1,508 @@ + + + + + + + + + + unravel.core.config — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.core.config

+#!/usr/bin/env python3
+
+"""
+This script defines classes for reading configuration settings from a file and accessing them
+using attribute-style access. It uses the ConfigParser module to parse configuration files
+and provides convenient access to configuration sections and values.
+
+Classes:
+    - AttrDict: A dictionary subclass that allows attribute access to its keys.
+    - Config: A class to read configuration from a file and allow attribute access using RawConfigParser.
+    - Configuration: A class to hold global configuration settings.
+
+Usage:
+    Import the classes and use them to read and access configuration settings from a file.
+
+Example:
+    from path.to.this.script import Config, Configuration
+
+    config = Config("path/to/config_file.ini")
+    database_config = config.database
+    print(database_config.username)  # Access a configuration value using attribute access
+
+Classes:
+    AttrDict
+        - A dictionary that allows attribute access to its keys.
+        - Methods:
+            - __getattr__: Returns the value associated with the given key.
+
+    Config
+        - Reads configuration from a file and allows attribute access using RawConfigParser.
+        - Methods:
+            - __init__: Initializes the Config object and reads the configuration file.
+            - __getattr__: Returns a dictionary-like object for the specified section, with comments stripped from values.
+            - _strip_comments: Static method that strips inline comments from configuration values.
+
+    Configuration
+        - Holds global configuration settings.
+        - Attributes:
+            - verbose: A boolean flag to control verbosity of the application.
+
+Notes:
+    - The Config class uses the RawConfigParser from the configparser module to parse the configuration file.
+    - The AttrDict class allows for convenient attribute access to dictionary keys.
+    - The Configuration class can be extended to hold additional global settings as needed.
+"""
+
+import configparser
+import re
+
+
+[docs] +class AttrDict(dict): + """A dictionary that allows attribute access.""" + def __getattr__(self, name): + return self[name]
+ + +
+[docs] +class Config: + """A class to read configuration from a file and allow attribute access using RawConfigParser.""" + def __init__(self, config_file): + self.parser = configparser.RawConfigParser() + self.parser.read(config_file) + + def __getattr__(self, section): + if section in self.parser: + section_dict = {k: self._strip_comments(v) for k, v in self.parser[section].items()} + return AttrDict(section_dict) + else: + raise AttributeError(f"No such section: {section}") + + @staticmethod + def _strip_comments(value): + """Strip inline comments from configuration values.""" + return re.sub(r"\s*#.*$", "", value, flags=re.M)
+ + +
+[docs] +class Configuration: + """A class to hold configuration settings.""" + verbose = False
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/core/img_io.html b/unravel/docs/_build/html/_modules/unravel/core/img_io.html new file mode 100644 index 00000000..82a1b824 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/core/img_io.html @@ -0,0 +1,1167 @@ + + + + + + + + + + unravel.core.img_io — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.core.img_io

+#!/usr/bin/env python3
+
+"""
+This module contains functions for loading and saving 3D images.
+
+Main Functions:
+---------------
+- load_3D_img: Load a 3D image from a .czi, .nii.gz, or .tif series and return the ndarray.
+- save_as_nii: Save a numpy array as a .nii.gz image.
+- save_as_tifs: Save a 3D ndarray as a series of tif images.
+
+Helper Functions:
+-----------------
+- extract_resolution
+- load_image_metadata_from_txt
+- save_metadata_to_file
+- metadata
+- return_3D_img
+"""
+
+import re
+import cv2 
+import dask.array as da
+import h5py
+import nibabel as nib
+import numpy as np
+import tifffile
+import zarr
+from aicspylibczi import CziFile
+from concurrent.futures import ThreadPoolExecutor
+from glob import glob
+from lxml import etree
+from pathlib import Path
+from rich import print
+from tifffile import imwrite 
+
+from unravel.core.utils import print_func_name_args_times
+
+
+# Load 3D image (load_3D_img()), get/save metadata, and return ndarray [with metadata]
+
+
+[docs] +def return_3D_img(ndarray, return_metadata=False, return_res=False, xy_res=None, z_res=None, x_dim=None, y_dim=None, z_dim=None): + """ + Return the 3D image ndarray and optionally resolutions (xy_res, z_res) or metadata (xy_res, z_res, x_dim, y_dim, z_dim). + + Parameters + ---------- + ndarray : ndarray + The 3D image array. + return_metadata : bool, optional + Whether to return metadata. Default is False. + return_res : bool, optional + Whether to return resolutions. Default is False. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + x_dim : int, optional + The size of the image in the x-dimension. + y_dim : int, optional + The size of the image in the y-dimension. + z_dim : int, optional + The size of the image in the z-dimension. + + Returns + ------- + ndarray + The 3D image array. + tuple, optional + If return_res is True, returns (ndarray, xy_res, z_res). + tuple, optional + If return_metadata is True, returns (ndarray, xy_res, z_res, x_dim, y_dim, z_dim). + """ + if return_metadata: + return ndarray, xy_res, z_res, x_dim, y_dim, z_dim + elif return_res: + return ndarray, xy_res, z_res + return ndarray
+ + +
+[docs] +def metadata(file_path, ndarray, return_res=False, return_metadata=False, xy_res=None, z_res=None, save_metadata=None): + """ + Extract and handle metadata, including saving to a file if requested. + + Parameters + ---------- + file_path : str + The path to the image file. + ndarray : ndarray + The 3D image array. + return_res : bool, optional + Whether to return resolutions. Default is False. + return_metadata : bool, optional + Whether to return metadata. Default is False. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + save_metadata : str, optional + Path to save metadata file. Default is None. + + Returns + ------- + tuple + Returns (xy_res, z_res, x_dim, y_dim, z_dim). + """ + x_dim, y_dim, z_dim = None, None, None + if return_res or return_metadata: + if xy_res is None and z_res is None: + xy_res, z_res = extract_resolution(file_path) + x_dim, y_dim, z_dim = ndarray.shape + if save_metadata: + save_metadata_to_file(xy_res, z_res, x_dim, y_dim, z_dim, save_metadata=save_metadata) + return xy_res, z_res, x_dim, y_dim, z_dim
+ + +
+[docs] +def extract_resolution(img_path): + """ + Extract resolution from image metadata. + + Parameters + ---------- + img_path : str + The path to the image file. + + Returns + ------- + tuple + Returns (xy_res, z_res) in microns. + """ + xy_res, z_res = None, None + if str(img_path).endswith('.czi'): + czi = CziFile(img_path) + xml_root = czi.meta + scaling_info = xml_root.find(".//Scaling") + xy_res = float(scaling_info.find("./Items/Distance[@Id='X']/Value").text) * 1e6 + z_res = float(scaling_info.find("./Items/Distance[@Id='Z']/Value").text) * 1e6 + elif str(img_path).endswith('.ome.tif') or str(img_path).endswith('.tif'): + with tifffile.TiffFile(img_path) as tif: + meta = tif.pages[0].tags + ome_xml_str = meta['ImageDescription'].value + ome_xml_root = etree.fromstring(ome_xml_str.encode('utf-8')) + default_ns = ome_xml_root.nsmap[None] + pixels_element = ome_xml_root.find(f'.//{{{default_ns}}}Pixels') + xy_res = float(pixels_element.get('PhysicalSizeX')) + z_res = float(pixels_element.get('PhysicalSizeZ')) + elif str(img_path).endswith('.nii.gz'): + img = nib.load(img_path) + affine = img.affine + xy_res = abs(affine[0, 0] * 1e3) # Convert from mm to um + z_res = abs(affine[2, 2] * 1e3) + elif str(img_path).endswith('.h5'): + with h5py.File(h5py, 'r') as f: + full_res_dataset_name = next(iter(f.keys())) # Assumes that full res data is 1st in the dataset list + print(f"\n Loading HDF5 dataset: {full_res_dataset_name}\n") + dataset = f[full_res_dataset_name] + res = dataset.attrs['element_size_um'] # z, y, x voxel sizes in microns (ndarray) + xy_res = res[1] + z_res = res[0] + return xy_res, z_res
+ + +
+[docs] +@print_func_name_args_times() +def load_czi(czi_path, channel=0, desired_axis_order="xyz", return_res=False, return_metadata=False, save_metadata=None, xy_res=None, z_res=None): + """ + Load a .czi image and return the ndarray. + + Parameters + ---------- + czi_path : str + The path to the .czi file. + channel : int, optional + The channel to load. Default is 0. + desired_axis_order : str, optional + The desired order of the image axes. Default is 'xyz'. + return_res : bool, optional + Whether to return resolutions. Default is False. + return_metadata : bool, optional + Whether to return metadata. Default is False. + save_metadata : str, optional + Path to save metadata file. Default is None. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + + Returns + ------- + ndarray + The loaded 3D image array. + tuple, optional + If return_res is True, returns (ndarray, xy_res, z_res). + tuple, optional + If return_metadata is True, returns (ndarray, xy_res, z_res, x_dim, y_dim, z_dim). + """ + czi = CziFile(czi_path) + ndarray = np.squeeze(czi.read_image(C=channel)[0]) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "xyz" else ndarray + xy_res, z_res, x_dim, y_dim, z_dim = metadata(czi_path, ndarray, return_res, return_metadata, xy_res, z_res, save_metadata) + return return_3D_img(ndarray, return_metadata, return_res, xy_res, z_res, x_dim, y_dim, z_dim)
+ + +
+[docs] +@print_func_name_args_times() +def load_tifs(tif_path, desired_axis_order="xyz", return_res=False, return_metadata=False, save_metadata=None, xy_res=None, z_res=None, parallel_loading=True): + """ + Load a series of .tif images and return the ndarray. + + Parameters + ---------- + tif_path : str + The path to the .tif files. + desired_axis_order : str, optional + The desired order of the image axes. Default is 'xyz'. + return_res : bool, optional + Whether to return resolutions. Default is False. + return_metadata : bool, optional + Whether to return metadata. Default is False. + save_metadata : str, optional + Path to save metadata file. Default is None. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + parallel_loading : bool, optional + Whether to load images in parallel. Default is True. + + Returns + ------- + ndarray + The loaded 3D image array. + tuple, optional + If return_res is True, returns (ndarray, xy_res, z_res). + tuple, optional + If return_metadata is True, returns (ndarray, xy_res, z_res, x_dim, y_dim, z_dim). + """ + def load_single_tif(tif_file): + """Load a single .tif file using OpenCV and return the ndarray.""" + img = cv2.imread(str(tif_file), cv2.IMREAD_UNCHANGED) + return img + tif_files = sorted(Path(tif_path).parent.glob("*.tif")) + if parallel_loading: + with ThreadPoolExecutor() as executor: + tifs_stacked = list(executor.map(load_single_tif, tif_files)) + else: + tifs_stacked = [] + for tif_file in tif_files: + tifs_stacked.append(load_single_tif(tif_file)) + ndarray = np.stack(tifs_stacked, axis=0) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "xyz" else ndarray + xy_res, z_res, x_dim, y_dim, z_dim = metadata(tif_files[0], ndarray, return_res, return_metadata, xy_res, z_res, save_metadata) + return return_3D_img(ndarray, return_metadata, return_res, xy_res, z_res, x_dim, y_dim, z_dim)
+ + +
+[docs] +@print_func_name_args_times() +def load_nii(nii_path, desired_axis_order="xyz", return_res=False, return_metadata=False, save_metadata=None, xy_res=None, z_res=None): + """ + Load a .nii.gz image and return the ndarray. + + Parameters + ---------- + nii_path : str + The path to the .nii.gz file. + desired_axis_order : str, optional + The desired order of the image axes. Default is 'xyz'. + return_res : bool, optional + Whether to return resolutions. Default is False. + return_metadata : bool, optional + Whether to return metadata. Default is False. + save_metadata : str, optional + Path to save metadata file. Default is None. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + + Returns + ------- + ndarray + The loaded 3D image array. + tuple, optional + If return_res is True, returns (ndarray, xy_res, z_res). + tuple, optional + If return_metadata is True, returns (ndarray, xy_res, z_res, x_dim, y_dim, z_dim). + """ + nii = nib.load(nii_path) + data_type = nii.header.get_data_dtype() + ndarray = np.asanyarray(nii.dataobj).astype(data_type) + ndarray = np.squeeze(ndarray) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "zyx" else ndarray + xy_res, z_res, x_dim, y_dim, z_dim = metadata(nii_path, ndarray, return_res, return_metadata, save_metadata=save_metadata) + return return_3D_img(ndarray, return_metadata, return_res, xy_res, z_res, x_dim, y_dim, z_dim)
+ + +
+[docs] +@print_func_name_args_times() +def load_nii_subset(nii_path, xmin, xmax, ymin, ymax, zmin, zmax): + """ + Load a spatial subset of a .nii.gz file and return an ndarray. + + Parameters + ---------- + nii_path : str + The path to the .nii.gz file. + xmin, xmax, ymin, ymax, zmin, zmax : int + The spatial coordinates defining the subset. + + Returns + ------- + ndarray + The loaded subset of the 3D image. + """ + proxy_img = nib.load(nii_path) + subset_array = proxy_img.dataobj[xmin:xmax, ymin:ymax, zmin:zmax] + return np.squeeze(subset_array)
+ + +
+[docs] +@print_func_name_args_times() +def load_h5(hdf5_path, desired_axis_order="xyz", return_res=False, return_metadata=False, save_metadata=None, xy_res=None, z_res=None): + """ + Load full resolution image from an HDF5 file (.h5) and return the ndarray. + + Parameters + ---------- + hdf5_path : str + The path to the .h5 file. + desired_axis_order : str, optional + The desired order of the image axes. Default is 'xyz'. + return_res : bool, optional + Whether to return resolutions. Default is False. + return_metadata : bool, optional + Whether to return metadata. Default is False. + save_metadata : str, optional + Path to save metadata file. Default is None. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + + Returns + ------- + ndarray + The loaded 3D image array. + tuple, optional + If return_res is True, returns (ndarray, xy_res, z_res). + tuple, optional + If return_metadata is True, returns (ndarray, xy_res, z_res, x_dim, y_dim, z_dim). + """ + with h5py.File(hdf5_path, 'r') as f: + full_res_dataset_name = next(iter(f.keys())) # Assumes first dataset = full res image + dataset = f[full_res_dataset_name] + print(f"\n Loading {full_res_dataset_name} as ndarray") + ndarray = dataset[:] # Load the full res image into memory (if not enough RAM, chunck data [e.g., w/ dask array]) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "xyz" else ndarray + xy_res, z_res, x_dim, y_dim, z_dim = metadata(hdf5_path, ndarray, return_res, return_metadata, save_metadata=save_metadata) + return return_3D_img(ndarray, return_metadata, return_res, xy_res, z_res, x_dim, y_dim, z_dim)
+ + +
+[docs] +@print_func_name_args_times() +def load_zarr(zarr_path, desired_axis_order="xyz"): + """ + Load a .zarr image and return the ndarray. + + Parameters + ---------- + zarr_path : str + The path to the .zarr file. + desired_axis_order : str, optional + The desired order of the image axes. Default is 'xyz'. + + Returns + ------- + ndarray + The loaded 3D image array. + """ + zarr_dataset = zarr.open(zarr_path, mode='r') + ndarray = np.array(zarr_dataset) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "xyz" else ndarray + return ndarray
+ + +
+[docs] +def resolve_path(upstream_path, path_or_pattern, make_parents=True, is_file=True): + """ + Returns full path or Path(upstream_path, path_or_pattern) and optionally creates parent directories. + + Parameters + ---------- + upstream_path : str + The base path. + path_or_pattern : str + The relative path or glob pattern. + make_parents : bool, optional + Whether to create parent directories if they don't exist. Default is True. + is_file : bool, optional + Whether the path is a file. Default is True. + + Returns + ------- + Path or None + The resolved path or None if not found. + """ + if Path(path_or_pattern).is_absolute(): + if is_file: + Path(path_or_pattern).parent.mkdir(parents=True, exist_ok=True) + else: + Path(path_or_pattern).mkdir(parents=True, exist_ok=True) + return Path(path_or_pattern) + + full_path = Path(upstream_path, path_or_pattern) + if full_path.exists(): + return full_path + + glob_matches = sorted(full_path.parent.glob(full_path.name)) + if glob_matches: + return glob_matches[0] # Return the first match + + # Make parent dirs for future output + if make_parents: + if is_file: + full_path.parent.mkdir(parents=True, exist_ok=True) + else: + full_path.mkdir(parents=True, exist_ok=True) + return full_path + + return None
+ + +
+[docs] +def save_metadata_to_file(xy_res, z_res, x_dim, y_dim, z_dim, save_metadata='parameters/metadata.txt'): + """ + Save metadata to a text file. + + Parameters + ---------- + xy_res : float + The resolution in the xy-plane. + z_res : float + The resolution in the z-plane. + x_dim : int + The size of the image in the x-dimension. + y_dim : int + The size of the image in the y-dimension. + z_dim : int + The size of the image in the z-dimension. + save_metadata : str, optional + Path to save metadata file. Default is 'parameters/metadata.txt'. + """ + save_metadata = Path(save_metadata) + save_metadata.parent.mkdir(parents=True, exist_ok=True) + if not save_metadata.exists(): + with save_metadata.open('w') as f: + f.write(f"Width: {x_dim*xy_res} microns ({x_dim})\n") + f.write(f"Height: {y_dim*xy_res} microns ({y_dim})\n") + f.write(f"Depth: {z_dim*z_res} microns ({z_dim})\n") + f.write(f"Voxel size: {xy_res}x{xy_res}x{z_res} micron^3\n")
+ + +
+[docs] +def load_image_metadata_from_txt(metadata="./parameters/metadata*"): + """ + Load metadata from a text file. + + Parameters + ---------- + metadata : str, optional + The path or pattern to the metadata file. Default is './parameters/metadata*'. + + Returns + ------- + tuple + Returns (xy_res, z_res, x_dim, y_dim, z_dim) or (None, None, None, None, None) if file not found. + """ + file_paths = glob(str(metadata)) + if file_paths: + with open(file_paths[0], 'r') as file: + for line in file: + dim_match = re.compile(r'(Width|Height|Depth):\s+[\d.]+ microns \((\d+)\)').search(line) + if dim_match: + dim = dim_match.group(1) + dim_res = float(dim_match.group(2)) + if dim == 'Width': + x_dim = int(dim_res) + elif dim == 'Height': + y_dim = int(dim_res) + elif dim == 'Depth': + z_dim = int(dim_res) + + voxel_match = re.compile(r'Voxel size: ([\d.]+)x([\d.]+)x([\d.]+) micron\^3').search(line) + if voxel_match: + xy_res = float(voxel_match.group(1)) + z_res = float(voxel_match.group(3)) + else: + return None, None, None, None, None + return xy_res, z_res, x_dim, y_dim, z_dim
+ + +
+[docs] +@print_func_name_args_times() +def load_3D_img(img_path, channel=0, desired_axis_order="xyz", return_res=False, return_metadata=False, xy_res=None, z_res=None, save_metadata=None): + """ + Load a 3D image from various file formats and return the ndarray. + + Parameters + ---------- + img_path : str + The path to the image file. + channel : int, optional + The channel to load. Default is 0. + desired_axis_order : str, optional + The desired order of the image axes. Default is 'xyz'. + return_res : bool, optional + Whether to return resolutions. Default is False. + return_metadata : bool, optional + Whether to return metadata. Default is False. + xy_res : float, optional + The resolution in the xy-plane. + z_res : float, optional + The resolution in the z-plane. + save_metadata : str, optional + Path to save metadata file. Default is None. + + Returns + ------- + ndarray + The loaded 3D image array. + tuple, optional + If return_res is True, returns (ndarray, xy_res, z_res). + tuple, optional + If return_metadata is True, returns (ndarray, xy_res, z_res, x_dim, y_dim, z_dim). + """ + + # If file_path points to dir containing tifs, resolve path to first .tif + img_path = Path(img_path) + + if img_path.is_dir(): + sorted_files = sorted(img_path.glob(f"*.tif")) + if sorted_files: + img_path = next(iter(sorted_files), None) + + if not img_path.exists(): + raise FileNotFoundError(f"No compatible image files found in {img_path}. Supported file types: .czi, .ome.tif, .tif, .nii.gz, .h5, .zarr") + + if str(img_path).endswith('.czi'): + print(f"\n [default]Loading channel {channel} from {img_path} (channel 0 is the first channel)") + else: + print(f"\n [default]Loading {img_path}") + + # Load image based on file type and optionally return resolutions and dimensions + try: + if str(img_path).endswith('.czi'): + return load_czi(img_path, channel, desired_axis_order, return_res, return_metadata, save_metadata, xy_res, z_res) + elif str(img_path).endswith('.ome.tif') or str(img_path).endswith('.tif'): + return load_tifs(img_path, desired_axis_order, return_res, return_metadata, save_metadata, xy_res, z_res, parallel_loading=True) + elif str(img_path).endswith('.nii.gz'): + return load_nii(img_path, desired_axis_order, return_res, return_metadata, save_metadata, xy_res, z_res) + elif str(img_path).endswith('.h5'): + return load_h5(img_path, desired_axis_order, return_res, return_metadata, save_metadata, xy_res, z_res) + elif str(img_path).endswith('.zarr'): + return load_zarr(img_path, desired_axis_order) + else: + raise ValueError(f"Unsupported file type: {img_path.suffix}. Supported file types: .czi, .ome.tif, .tif, .nii.gz, .h5") + except (FileNotFoundError, ValueError) as e: + print(f"\n [red bold]Error: {e}\n") + import sys ; sys.exit()
+ + + +# Save images +
+[docs] +def load_nii_orientation(input_nii_path): + """ + Load a .nii.gz file and return its orientation (affine matrix). + + Parameters + ---------- + input_nii_path : str + The path to the .nii.gz file. + + Returns + ------- + ndarray + The affine matrix of the .nii.gz file. + """ + nii = nib.load(str(input_nii_path)) + return nii.affine
+ + +
+[docs] +@print_func_name_args_times() +def save_as_nii(ndarray, output, xy_res=1000, z_res=1000, data_type=np.float32, reference=None): + """ + Save a numpy array as a .nii.gz image with the option to retain the orientation of an input .nii.gz file. + + Parameters + ---------- + ndarray : ndarray + The numpy array to save. + output : str + Output file path. + xy_res : float, optional + XY resolution in microns. Default is 1000. + z_res : float, optional + Z resolution in microns. Default is 1000. + data_type : data-type, optional + Data type for the NIFTI image. Default is np.float32. + reference : str or ndarray, optional + Either an affine matrix or a path to a .nii.gz file to retain its orientation. Default is None. + + Returns + ------- + None + """ + output = Path(output).resolve() + output.parent.mkdir(parents=True, exist_ok=True) + + # Check if reference is an affine matrix or a path + if reference is not None: + if isinstance(reference, np.ndarray): + affine = reference + elif isinstance(reference, (str, Path)): + affine = load_nii_orientation(reference) + else: + raise ValueError("Reference must be either an affine matrix or a file path.") + else: + # Create the affine matrix with the appropriate resolutions (converting microns to mm) + affine = np.diag([xy_res / 1000, xy_res / 1000, z_res / 1000, 1]) # RAS orientation + + # Create and save the NIFTI image + nifti_img = nib.Nifti1Image(ndarray, affine) + nifti_img.header.set_data_dtype(data_type) + nib.save(nifti_img, str(output)) + print(f"\n Output: [default bold]{output}")
+ + +
+[docs] +@print_func_name_args_times() +def save_as_tifs(ndarray, tif_dir_out, ndarray_axis_order="xyz"): + """ + Save an ndarray as a series of .tif images. + + Parameters + ---------- + ndarray : ndarray + The 3D image array to save. + tif_dir_out : str + The directory to save the .tif files. + ndarray_axis_order : str, optional + The order of the ndarray axes. Default is 'xyz'. + + Returns + ------- + None + """ + # Ensure tif_dir_out is a Path object, not a string + tif_dir_out = Path(tif_dir_out) + tif_dir_out.mkdir(parents=True, exist_ok=True) + + if ndarray_axis_order == "xyz": + ndarray = np.transpose(ndarray, (2, 1, 0)) # Transpose to zyx (tiff expects zyx) + + for i, slice_ in enumerate(ndarray): + slice_file_path = tif_dir_out / f"slice_{i:04d}.tif" + imwrite(str(slice_file_path), slice_) + print(f"\n Output: [default bold]{tif_dir_out}")
+ + +
+[docs] +@print_func_name_args_times() +def save_as_zarr(ndarray, output_path, ndarray_axis_order="xyz"): + """ + Save an ndarray to a .zarr file. + + Parameters + ---------- + ndarray : ndarray + The 3D image array to save. + output_path : str + The path to save the .zarr file. + ndarray_axis_order : str, optional + The order of the ndarray axes. Default is 'xyz'. + + Returns + ------- + None + """ + if ndarray_axis_order == "xyz": + ndarray = np.transpose(ndarray, (2, 1, 0)) + dask_array = da.from_array(ndarray, chunks='auto') + compressor = zarr.Blosc(cname='lz4', clevel=9, shuffle=zarr.Blosc.BITSHUFFLE) + dask_array.to_zarr(output_path, compressor=compressor, overwrite=True) + print(f"\n Output: [default bold]{output_path}")
+ + +
+[docs] +@print_func_name_args_times() +def save_as_h5(ndarray, output_path, ndarray_axis_order="xyz"): + """ + Save an ndarray to an HDF5 file (.h5). + + Parameters + ---------- + ndarray : ndarray + The 3D image array to save. + output_path : str + The path to save the .h5 file. + ndarray_axis_order : str, optional + The order of the ndarray axes. Default is 'xyz'. + + Returns + ------- + None + """ + if ndarray_axis_order == "xyz": + ndarray = np.transpose(ndarray, (2, 1, 0)) + with h5py.File(output_path, 'w') as f: + f.create_dataset('data', data=ndarray, compression="lzf")
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/core/img_tools.html b/unravel/docs/_build/html/_modules/unravel/core/img_tools.html new file mode 100644 index 00000000..030cd797 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/core/img_tools.html @@ -0,0 +1,756 @@ + + + + + + + + + + unravel.core.img_tools — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.core.img_tools

+#!/usr/bin/env python3
+
+""" 
+This module contains functions processing 3D images: 
+    - resample: Resample a 3D ndarray.
+    - reorient_for_raw_to_nii_conv: Reorient an ndarray for registration or warping to atlas space
+    - pixel_classification: Segment tif series with Ilastik.
+    - pad: Pad an ndarray by a specified percentage.
+    - reorient_ndarray: Reorient a 3D ndarray based on the 3 letter orientation code (using the letters RLAPSI).
+    - reorient_ndarray2: Reorient a 3D ndarray based on the 3 letter orientation code (using the letters RLAPSI).
+    - rolling_ball_subtraction_opencv_parallel: Subtract background from a 3D ndarray using OpenCV.
+    - cluster_IDs: Prints cluster IDs for clusters > minextent voxels.
+    - find_bounding_box: Finds the bounding box of all clusters or a specific cluster in a cluster index ndarray and optionally writes to file.
+"""
+
+
+import cv2 
+import numpy as np
+import subprocess
+from concurrent.futures import ThreadPoolExecutor
+from glob import glob
+from pathlib import Path
+from rich import print
+from scipy import ndimage
+from scipy.ndimage import rotate
+
+from unravel.core.utils import print_func_name_args_times
+
+
+
+[docs] +@print_func_name_args_times() +def resample(ndarray, xy_res, z_res, res, zoom_order=1): + """Resample a 3D ndarray + + Parameters: + ndarray: 3D ndarray to resample + xy_res: x/y voxel size in microns (for the original image) + z_res: z voxel size in microns + res: resolution in microns for the resampled image + zoom_order: SciPy zoom order for resampling the native image. Default: 1 (bilinear interpolation)""" + zf_xy = xy_res / res # Zoom factor + zf_z = z_res / res + img_resampled = ndimage.zoom(ndarray, (zf_xy, zf_xy, zf_z), order=zoom_order) + return img_resampled
+ + +
+[docs] +@print_func_name_args_times() +def reorient_for_raw_to_nii_conv(ndarray): + """Reorient resampled ndarray for registration or warping to atlas space + (legacy mode mimics MIRACL's tif to .nii.gz conversion)""" + img_reoriented = np.einsum('zyx->xzy', ndarray) + return np.transpose(img_reoriented, (2, 1, 0))
+ + +
+[docs] +@print_func_name_args_times() +def reverse_reorient_for_raw_to_nii_conv(ndarray): + """After warping to native space, reorients image to match tissue""" + rotated_img = rotate(ndarray, -90, reshape=True, axes=(0, 1)) # Rotate 90 degrees to the right + flipped_img = np.fliplr(rotated_img) # Flip horizontally + return flipped_img
+ + +# @print_func_name_args_times() +# def pixel_classification(tif_dir, ilastik_project, output_dir, ilastik_log=None): +# """Segment tif series with Ilastik.""" +# tif_dir = str(tif_dir) +# tif_list = sorted(glob(f"{tif_dir}/*.tif")) +# ilastik_project = str(ilastik_project) +# output_dir_ = str(output_dir) +# cmd = [ +# 'run_ilastik.sh', +# '--headless', +# '--project', ilastik_project, +# '--export_source', 'Simple Segmentation', +# '--output_format', 'tif', +# '--output_filename_format', f'{output_dir}/{{nickname}}.tif', +# ] + tif_list +# if not Path(output_dir_).exists(): +# if ilastik_log == None: +# subprocess.run(cmd) +# else: +# subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +
+[docs] +@print_func_name_args_times() +def pixel_classification(tif_dir, ilastik_project, output_dir, ilastik_log=None): + """Segment tif series with Ilastik using pixel classification.""" + tif_list = sorted(glob(f"{tif_dir}/*.tif")) + if not tif_list: + print(f"No TIF files found in {tif_dir}.") + return + + cmd = [ + 'run_ilastik.sh', # Make sure this path is correct + '--headless', + '--project', str(ilastik_project), + '--export_source', 'Simple Segmentation', + '--output_format', 'tif', + '--output_filename_format', f'{str(output_dir)}/{{nickname}}.tif' + ] + tif_list + + print("\n Running Ilastik with command:\n", ' '.join(cmd)) + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print("\n Ilastik failed with error:\n", result.stderr) + else: + print("\n Ilastik completed successfully.\n")
+ + +
+[docs] +@print_func_name_args_times() +def pad(ndarray, pad_width=0.15): + """Pads ndarray by 15% of voxels on all sides""" + pad_factor = 1 + 2 * pad_width + pad_width_x = round(((ndarray.shape[0] * pad_factor) - ndarray.shape[0]) / 2) + pad_width_y = round(((ndarray.shape[1] * pad_factor) - ndarray.shape[1]) / 2) + pad_width_z = round(((ndarray.shape[2] * pad_factor) - ndarray.shape[2]) / 2) + return np.pad(ndarray, ((pad_width_x, pad_width_x), (pad_width_y, pad_width_y), (pad_width_z, pad_width_z)), mode='constant')
+ + +
+[docs] +@print_func_name_args_times() +def reorient_ndarray(data, orientation_string): + """Reorient a 3D ndarray based on the 3 letter orientation code (using the letters RLAPSI). Assumes initial orientation is RAS (NIFTI convention).""" + + # Orientation reference for RAS system + ref_orient = "RAS" # R=Right, A=Anterior, S=Superior (conventional orientation of NIFTI images) + + # Ensure valid orientation_string + if not set(orientation_string).issubset(set(ref_orient + "LIP")): + raise ValueError("Invalid orientation code. Must be a 3-letter code consisting of RLAPSI.") + if len(orientation_string) != 3: + raise ValueError("Invalid orientation code. Must be a 3-letter code consisting of RLAPSI.") + + # Compute the permutation indices and flips + permutation = [ref_orient.index(orient) for orient in orientation_string] + flips = [(slice(None, None, -1) if orient in "LIP" else slice(None)) for orient in orientation_string] + + # Reorient the data using numpy's advanced indexing + reoriented_data = data[flips[0], flips[1], flips[2]] + reoriented_data = np.transpose(reoriented_data, permutation) + + return reoriented_data
+ + +
+[docs] +@print_func_name_args_times() +def reorient_ndarray2(ndarray, orientation_string): + """Reorient a 3D ndarray based on the 3 letter orientation code (using the letters RLAPSI). Assumes initial orientation is RAS (NIFTI convention).""" + + # Define the anatomical direction mapping. The first letter is the direction of the first axis, etc. + direction_map = { + 'R': 0, 'L': 0, + 'A': 1, 'P': 1, + 'I': 2, 'S': 2 + } + + # Define the flip direction + flip_map = { + 'R': True, 'L': False, + 'A': False, 'P': True, + 'I': True, 'S': False + } + + # Orientation reference for RAS system + ref_orient = "RAS" + + # Ensure valid orientation_string + if not set(orientation_string).issubset(set(ref_orient + "LIP")): + raise ValueError("Invalid orientation code. Must be a 3-letter code consisting of RLAPSI.") + if len(orientation_string) != 3: + raise ValueError("Invalid orientation code. Must be a 3-letter code consisting of RLAPSI.") + + # Determine new orientation based on the code + new_axes_order = [direction_map[c] for c in orientation_string] + + # Reorder the axes + reoriented_volume = np.transpose(ndarray, axes=new_axes_order) + + # Flip axes as necessary + for idx, c in enumerate(orientation_string): + if flip_map[c]: + reoriented_volume = np.flip(reoriented_volume, axis=idx) + + return reoriented_volume
+ + + +####### Rolling ball background subraction ####### + +
+[docs] +def process_slice(slice, struct_element): + """Subtract background from <slice> using OpenCV.""" + smoothed_slice = cv2.morphologyEx(slice, cv2.MORPH_OPEN, struct_element) + return slice - smoothed_slice
+ + +
+[docs] +@print_func_name_args_times() +def rolling_ball_subtraction_opencv_parallel(ndarray, radius, threads=8): + """Subtract background from <ndarray> using OpenCV. + Uses multiple threads to process slices in parallel. + Radius is the radius of the rolling ball in pixels. + Returns ndarray with background subtracted. + """ + struct_element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*radius+1, 2*radius+1)) # 2D disk + bkg_subtracted_img = np.empty_like(ndarray) # Preallocate the result array + num_cores = min(len(ndarray), threads) # Number of available CPU cores + with ThreadPoolExecutor(max_workers=num_cores) as executor: # Process slices in parallel + # Map the process_slice function to each slice in ndarray and store the result in result. Each process_slice call gets a slice and the struct_element as arguments. + # executor.map() returns an iterator with the results of each process_slice call. The iterator is consumed and the results are stored in result. + # ndarray is a list of slices + # [struct_element]*len(ndarray) is a list of struct_elements of length len(ndarray) + for i, background_subtracted_slice in enumerate(executor.map(process_slice, ndarray, [struct_element]*len(ndarray))): + bkg_subtracted_img[i] = background_subtracted_slice + return bkg_subtracted_img
+ + +print_func_name_args_times() +
+[docs] +def cluster_IDs(ndarray, min_extent=1, print_IDs=False, print_sizes=False): + """Gets unique intensities [and sizes] for regions/clusters > minextent voxels and prints them in a string-separated list. + + Args: + ndarray + min_extent (int, optional): _description_. Defaults to 1. + print_IDs (bool, optional): _description_. Defaults to False. + print_sizes (bool, optional): _description_. Defaults to False. + + Returns: + list of ints: list of unique intensities + """ + + # Get unique intensities and their counts + unique_intensities, counts = np.unique(ndarray[ndarray > 0], return_counts=True) + + # Filter clusters based on size + clusters_above_minextent = [intensity for intensity, count in zip(unique_intensities, counts) if count >= min_extent] + + # Print cluster IDs + for idx, cluster_id in enumerate(clusters_above_minextent): + if print_sizes: + print(f"ID: {int(cluster_id)}, Size: {counts[idx]}") + elif print_IDs: + print(int(cluster_id), end=' ') + if print_IDs: # Removes trailing % + print() + + clusters = [int(cluster_id) for cluster_id in clusters_above_minextent] + + return clusters
+ + +print_func_name_args_times() +
+[docs] +def find_bounding_box(ndarray, cluster_ID=None, output_file_path=None): + """ + Finds the bounding box of all clusters or a specific cluster in a cluster index ndarray and optionally writes to file. + + Parameters: + ndarray: 3D numpy array to search within. + cluster_ID (int): Cluster intensity to find bbox for. If None, return bbox for all clusters. + output_file_path (str): File path to write the bounding box. + """ + + # Initialize views based on whether we are looking for a specific cluster_ID or any cluster + if cluster_ID is not None: + # Find indices where ndarray equals cluster_ID for each dimension + views = [np.where(ndarray == int(cluster_ID))[i] for i in range(3)] + else: + # Find indices where ndarray has any value (greater than 0) for each dimension + views = [np.any(ndarray, axis=i) for i in range(3)] + + # Initialize min and max indices + min_max_indices = [] + + # Find min and max indices for each dimension + for i, view in enumerate(views): + if cluster_ID is not None: + indices = views[i] + else: + indices = np.where(view)[0] + + # Check if there are any indices found + if len(indices) > 0: + min_index = int(min(indices)) + max_index = int(max(indices) + 1) + else: + # Handle empty array case by setting min and max to zero + min_index = 0 + max_index = 0 + + min_max_indices.append((min_index, max_index)) + + # Unpack indices for easier referencing + xmin, xmax, ymin, ymax, zmin, zmax = [idx for dim in min_max_indices for idx in dim] + + # Write to file if specified + if output_file_path: + with open(output_file_path, "w") as file: + file.write(f"{xmin}:{xmax}, {ymin}:{ymax}, {zmin}:{zmax}") + + return xmin, xmax, ymin, ymax, zmin, zmax
+ + +print_func_name_args_times() +
+[docs] +def crop(ndarray, bbox: str): + """Crop an ndarray to the specified bounding box (xmin:xmax, ymin:ymax, zmin:zmax)""" + # Split the bounding box string into coordinates + bbox_coords = bbox.split(',') + xmin, xmax = [int(x) for x in bbox_coords[0].split(':')] + ymin, ymax = [int(y) for y in bbox_coords[1].split(':')] + zmin, zmax = [int(z) for z in bbox_coords[2].split(':')] + + # Crop and return the ndarray + return ndarray[xmin:xmax, ymin:ymax, zmin:zmax]
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/core/utils.html b/unravel/docs/_build/html/_modules/unravel/core/utils.html new file mode 100644 index 00000000..4a631d32 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/core/utils.html @@ -0,0 +1,804 @@ + + + + + + + + + + unravel.core.utils — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.core.utils

+#!/usr/bin/env python3
+
+"""
+Utility functions and decorators for handling configurations, processing files and directories,
+and enhancing command-line scripts with progress bars and detailed function execution info.
+
+Classes:
+    - CustomMofNCompleteColumn: Progress bar column for completed/total items.
+    - CustomTimeElapsedColumn: Progress bar column for elapsed time in green.
+    - CustomTimeRemainingColumn: Progress bar column for remaining time in dark orange.
+    - AverageTimePerIterationColumn: Progress bar column for average time per iteration.
+
+Functions:
+    - load_config: Load settings from a config file.
+    - get_samples: Get a list of sample directories based on provided parameters.
+    - initialize_progress_bar: Initialize a Rich progress bar.
+    - print_cmd_and_times: Decorator to log and print command execution details.
+    - print_func_name_args_times: Decorator to print function execution details.
+    - load_text_from_file: Load text content from a file.
+    - copy_files: Copy specified files from source to target directory.
+
+Usage:
+    Import the functions and decorators to enhance your scripts.
+
+Examples:
+    - from unravel.core.utils import load_config, get_samples, initialize_progress_bar, print_cmd_and_times, print_func_name_args_times, load_text_from_file copy_files
+    - config = load_config("path/to/config.ini")
+    - samples = get_samples(exp_dir_paths=["/path/to/exp1", "/path/to/exp2"])
+    - progress, task_id = initialize_progress_bar(len(samples), task_message="[red]Processing samples...")
+    - if __name__ == '__main__':
+        - install()
+        - args = parse_args()
+        - Configuration.verbose = args.verbose
+        - print_cmd_and_times(main)()
+"""
+
+import functools
+import shutil
+import numpy as np
+import os
+import sys
+import threading
+import time
+from datetime import datetime
+from fnmatch import fnmatch
+from pathlib import Path
+from rich import print
+from rich.console import Console
+from rich.progress import Progress, TextColumn, SpinnerColumn, BarColumn, TimeElapsedColumn, TimeRemainingColumn, MofNCompleteColumn, ProgressColumn
+from rich.text import Text
+
+from unravel.core.config import Configuration, Config
+
+# Configuration loading
+
+[docs] +def load_config(config_path): + """Load settings from the config file and return a Config object.""" + if Path(config_path).exists(): + cfg = Config(config_path) + else: + print(f'\n [red]{config_path} does not exist\n') + import sys ; sys.exit() + return cfg
+ + +# Sample list +
+[docs] +def get_samples(sample_dir_list=None, sample_dir_pattern="sample??", exp_dir_paths=None): + """ + Return a list of full paths to sample directories (dirs) based on the dir list, pattern, and/or experiment dirs. + + This function searches for dirs matching a specific pattern (default "sample??") within the given experiment dirs. + If a sample_dir_list is provided, it uses the full paths from the list or resolves them if necessary. + If an exp_dir_paths list is provided, it searches for sample dirs within each experiment directory. + If both sample_dir_list and exp_dir_paths are provided, paths are added to the list from both sources. + + Parameters: + - sample_dir_list (list of str or None): Explicit list of dirs to include. Can be dir names or absolute paths. + - sample_dir_pattern (str): Pattern to match dirs within experiment dirs. Defaults to "sample??". + - exp_dir_paths (list of str or None): List of paths to experiment dirs where subdirs matching the sample_dir_pattern will be searched for. + + Returns: + - list of pathlib.Path: Full paths to all found sample dirs. + """ + samples = [] + + # Ensure sample_dir_list is a list + if isinstance(sample_dir_list, str): + sample_dir_list = [sample_dir_list] # Convert string to list + + # Add full paths of dirs from sample_dir_list that exist + if sample_dir_list: + for dir_name in sample_dir_list: + dir_path = Path(dir_name) + dir_path = dir_path if dir_path.is_absolute() else dir_path.resolve() + if dir_path.is_dir(): + samples.append(dir_path) + + # Search for sample folders within each experiment directory in exp_dir_paths and add their full paths + if exp_dir_paths: + for exp_dir in exp_dir_paths: + exp_path = Path(exp_dir).resolve() + if exp_path.is_dir(): + found_samples = [ + d.resolve() for d in exp_path.iterdir() + if d.is_dir() and fnmatch(d.name, sample_dir_pattern) + ] + samples.extend(found_samples) + + # If no dirs have been added yet, search the current working directory for dirs matching the pattern + if not samples: + cwd_samples = [ + d.resolve() for d in Path.cwd().iterdir() + if d.is_dir() and fnmatch(d.name, sample_dir_pattern) + ] + samples.extend(cwd_samples) + + # Use the current working directory as the fallback if no samples found + if not samples: + samples.append(Path.cwd()) + + return samples
+ + + +# Progress bar functions +
+[docs] +class CustomMofNCompleteColumn(MofNCompleteColumn): +
+[docs] + def render(self, task) -> Text: + completed = str(task.completed) + total = str(task.total) + return Text(f"{completed}/{total}", style="bright_cyan")
+
+ + +
+[docs] +class CustomTimeElapsedColumn(TimeElapsedColumn): +
+[docs] + def render(self, task) -> Text: + time_elapsed = super().render(task) + time_elapsed.stylize("green") + return time_elapsed
+
+ + +
+[docs] +class CustomTimeRemainingColumn(TimeRemainingColumn): +
+[docs] + def render(self, task) -> Text: + time_elapsed = super().render(task) + time_elapsed.stylize("dark_orange") + return time_elapsed
+
+ + +
+[docs] +class AverageTimePerIterationColumn(ProgressColumn): +
+[docs] + def render(self, task: "Task") -> Text: + speed = task.speed or 0 + if speed > 0: + avg_time = f"{1 / speed:.2f}s/iter" + else: + avg_time = "." + return Text(avg_time, style="red1")
+
+ + +
+[docs] +def initialize_progress_bar(num_of_items_to_iterate, task_message="[red]Processing..."): + progress = Progress( + TextColumn("[progress.description]{task.description}"), + SpinnerColumn(style="bright_magenta"), + BarColumn(complete_style="purple3", finished_style="purple"), + TextColumn("[bright_blue]{task.percentage:>3.0f}%[progress.percentage]"), + CustomMofNCompleteColumn(), + CustomTimeElapsedColumn(), + TextColumn("[gold1]eta:"), + CustomTimeRemainingColumn(), + AverageTimePerIterationColumn() + ) + task_id = progress.add_task(task_message, total=num_of_items_to_iterate) + return progress, task_id
+ + + +# Main function decorator + + + + + +# Function decorator + +
+[docs] +def get_dir_name_from_args(args, kwargs): + """ + This function checks args and kwargs for a file or directory path + and returns a string based on the name of the file or directory. + """ + for arg in args: + if isinstance(arg, (str, Path)) and Path(arg).exists(): + return Path(arg).resolve().name + + for kwarg in kwargs.values(): + if isinstance(kwarg, (str, Path)) and Path(kwarg).exists(): + return Path(kwarg).resolve().name + + return Path.cwd().name
+ + +# Create a thread-local storage for indentation level +thread_local_data = threading.local() +thread_local_data.indentation_level = 0 + + + + +
+[docs] +@print_func_name_args_times() +def load_text_from_file(file_path): + try: + with open(file_path, 'r') as file: + return file.read() + except Exception as e: + print(f"[red]Error reading file: {e}[/]") + return None
+ + +
+[docs] +@print_func_name_args_times() +def copy_files(source_dir, target_dir, filename, sample_path=None, verbose=False): + """Copy the specified slices to the target directory. + + Args: + - source_dir (Path): Path to the source directory containing the .tif files. + - target_dir (Path): Path to the target directory where the selected slices will be copied. + - filename (str): Name of the file to copy. + - sample_path (Path): Path to the sample directory (provide to prepend to the filename). + - verbose (bool): Increase verbosity.""" + + src_file = Path(source_dir, filename) + + if src_file.exists(): + if sample_path is not None: + dest_file = target_dir / f'{sample_path.name}_{filename}' + else: + dest_file = target_dir / filename + shutil.copy(src_file, dest_file) + if verbose: + print(f"Copied {src_file} to {dest_file}") + else: + if verbose: + print(f"File {src_file} does not exist and was not copied.")
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/h5_to_tifs.html b/unravel/docs/_build/html/_modules/unravel/image_io/h5_to_tifs.html new file mode 100644 index 00000000..b55e567e --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/h5_to_tifs.html @@ -0,0 +1,574 @@ + + + + + + + + + + unravel.image_io.h5_to_tifs — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.h5_to_tifs

+#!/usr/bin/env python3
+
+"""
+Use ``io_h5_to_tifs`` from UNRAVEL to load a h5/hdf5 image and save it as tifs.
+
+Usage:
+------
+    io_h5_to_tifs -i path/image.h5 -t autofl
+
+Inputs:
+    - image.h5 either from -i path/image.h5 or largest <asterisk>.h5 in cwd
+    - This assumes that the first dataset in the hdf5 file has the highest resolution.
+
+Outputs:
+    - ./<tif_dir_out>/slice_<asterisk>.tif series
+    - ./parameters/metadata (text file)
+
+Next command:
+    - ``reg_prep`` for registration
+"""
+
+import argparse
+import glob
+import os
+import h5py
+import numpy as np
+from pathlib import Path
+from rich import print
+from tifffile import imwrite 
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image.h5', action=SM) + parser.add_argument('-t', '--tif_dir', help='Name of output folder for outputting tifs', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def find_largest_h5_file(): + """ Find and return the path to the largest .h5 file in the current directory """ + largest_file = None + max_size = -1 + + for file in glob.glob('*.h5'): + size = os.path.getsize(file) + if size > max_size: + max_size = size + largest_file = file + + return largest_file
+ + +
+[docs] +def load_h5(hdf5_path, desired_axis_order="xyz", return_res=False): + """Load full res image from an HDF5 file (.h5) and return ndarray + Default: axis_order=xyz (other option: axis_order="zyx") + Default: returns: ndarray + If return_res=True returns: ndarray, xy_res, z_res (resolution in um)""" + with h5py.File(hdf5_path, 'r') as f: + full_res_dataset_name = next(iter(f.keys())) + dataset = f[full_res_dataset_name] + print(f"\n Loading {full_res_dataset_name} as ndarray") + ndarray = dataset[:] # Load the full res image into memory (if not enough RAM, chunck data [e.g., w/ dask array]) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "xyz" else ndarray + print(f' {ndarray.shape=}') + + if return_res: + xy_res, z_res = metadata_from_h5(hdf5_path) + return ndarray, xy_res, z_res + else: + return ndarray
+ + +
+[docs] +def metadata_from_h5(hdf5_path): + """Returns tuple with xy_voxel_size and z_voxel_size in microns from full res HDF5 image""" + with h5py.File(hdf5_path, 'r') as f: + # Extract full res HDF5 dataset + full_res_dataset_name = next(iter(f.keys())) # Assumes that full res data is 1st in the dataset list + dataset = f[full_res_dataset_name] # Slice the list of datasets + print(f" {dataset}") + + # Extract x, y, and z voxel sizes + res = dataset.attrs['element_size_um'] # z, y, x voxel sizes in microns (ndarray) + xy_res = res[1] + z_res = res[0] + print(f" {xy_res=}") + print(f" {z_res=}\n") + return xy_res, z_res
+ + +
+[docs] +def save_as_tifs(ndarray, tif_dir_out, ndarray_axis_order="xyz"): + """Save <ndarray> as tifs in <Path(tif_dir_out)>""" + tif_dir_out = Path(tif_dir_out) + tif_dir_out.mkdir(parents=True, exist_ok=True) + + if ndarray_axis_order == "xyz": + ndarray = np.transpose(ndarray, (2, 1, 0)) # Transpose to zyx (tiff expects zyx) + for i, slice_ in enumerate(ndarray): + slice_file_path = tif_dir_out / f"slice_{i:04d}.tif" + imwrite(str(slice_file_path), slice_) + print(f" Output: [default bold]{tif_dir_out}\n")
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.input: + h5_path = args.input + else: + h5_path = find_largest_h5_file() + if h5_path: + print(f"\n The largest .h5 file is: {h5_path}") + else: + print("\n [red1]No .h5 files found.\n") + + # Load h5 image (highest res dataset) as ndarray and extract voxel sizes in microns + img, xy_res, z_res = load_h5(h5_path, desired_axis_order="xyz", return_res=True) + + # Make parameters directory in the sample?? folder + os.makedirs("parameters", exist_ok=True) + + # Save metadata to text file so resolution can be obtained by other commands/modules + metadata_txt_path = Path(".", "parameters", "metadata") + with open(metadata_txt_path, 'w') as file: + file.write(f'Voxel size: {xy_res}x{xy_res}x{z_res} micron^3') + + # Save as tifs + tifs_output_path = Path(".", args.tif_dir) + save_as_tifs(img, tifs_output_path)
+ + + +if __name__ == '__main__': + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/img_to_npy.html b/unravel/docs/_build/html/_modules/unravel/image_io/img_to_npy.html new file mode 100644 index 00000000..2f15da99 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/img_to_npy.html @@ -0,0 +1,483 @@ + + + + + + + + + + unravel.image_io.img_to_npy — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.img_to_npy

+#!/usr/bin/env python3
+
+"""
+Use ``io_img_to_npy`` from UNRAVEL to convert a 3D image to an ndarray and save it as a .npy file.
+
+Usage: 
+------
+    io_img_to_npy -i path/to/image.czi -o path/to/image.npy
+"""
+
+import argparse
+import numpy as np
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.utils import print_cmd_and_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', required=True, help='Input image file path (.czi, .nii.gz, .tif)', action=SM) + parser.add_argument('-o', '--output', required=True, help='Output HDF5 file path', action=SM) + parser.add_argument('-ao', '--axis_order', help='Axis order for the image (default: zyx)', default='zyx', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def main(): + args = parse_args() + + img = load_3D_img(args.input, desired_axis_order=args.axis_order) + + if args.output: + output = args.output + elif args.input.endswith('.czi'): + output = args.input.replace('.czi', '.npy') + elif args.input.endswith('.nii.gz'): + output = args.input.replace('.nii.gz', '.npy') + elif args.input.endswith('.tif'): + output = args.input.replace('.tif', '.npy') + elif args.input.endswith('.zarr'): + output = args.input.replace('.zarr', '.npy') + + # Save the ndarray to a binary file in NumPy `.npy` format + np.save(output, img)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/io_img.html b/unravel/docs/_build/html/_modules/unravel/image_io/io_img.html new file mode 100644 index 00000000..98402430 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/io_img.html @@ -0,0 +1,506 @@ + + + + + + + + + + unravel.image_io.io_img — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.io_img

+#!/usr/bin/env python3
+
+"""
+Use ``io_img`` from UNRAVEL to load a 3D image, [get metadata], and save as the specified image type.
+
+Usage: 
+------
+    io_img -i path/to/image.czi -o path/to/tif_dir
+
+Input image types:
+    .czi, .nii.gz, .ome.tif series, .tif series, .h5, .zarr
+
+Output image types: 
+    .nii.gz, .tif series, .zarr
+"""
+
+import argparse
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_h5, save_as_nii, save_as_tifs, save_as_zarr
+from unravel.core.utils import print_cmd_and_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image .czi, path/img.nii.gz, or path/tif_dir', required=True, action=SM) + parser.add_argument('-x', '--xy_res', help='xy resolution in um', required=True, type=float, action=SM) + parser.add_argument('-z', '--z_res', help='z resolution in um', required=True, type=float, action=SM) + parser.add_argument('-c', '--channel', help='.czi channel number. Default: 0 for autofluo', default=0, type=int, action=SM) + parser.add_argument('-o', '--output', help='Output path. Default: None', default=None, action=SM) + parser.add_argument('-d', '--dtype', help='Data type for .nii.gz. Default: uint16', default='uint16', action=SM) + parser.add_argument('-r', '--reference', help='Reference image for .nii.gz metadata. Default: None', default=None, action=SM) + parser.add_argument('-ao', '--axis_order', help='Default: xyz. (other option: zyx)', default='xyz', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Test if other scripts in image_io are redundant and can be removed. If not, consolidate them into this script. + + +
+[docs] +def main(): + args = parse_args() + + + # Load image and metadata + if args.xy_res is None or args.z_res is None: + img, xy_res, z_res = load_3D_img(args.input, return_res=True) + else: + img = load_3D_img(args.input) + xy_res, z_res = args.xy_res, args.z_res + + # Print metadata + if args.verbose: + print(f"\n Type: {type(img)}") + print(f" Image shape: {img.shape}") + print(f" Image dtype: {img.dtype}") + print(f" xy resolution: {xy_res} um") + print(f" z resolution: {z_res} um") + + # Save image + if args.output.endswith('.nii.gz'): + save_as_nii(img, args.output, xy_res, z_res, data_type=args.dtype, reference=args.reference) + elif args.output.endswith('.zarr'): + save_as_zarr(img, args.output, ndarray_axis_order=args.axis_order) + elif args.output.endswith('.h5'): + save_as_h5(img, args.output, ndarray_axis_order=args.axis_order) + else: + save_as_tifs(img, args.output, ndarray_axis_order=args.axis_order)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/io_nii.html b/unravel/docs/_build/html/_modules/unravel/image_io/io_nii.html new file mode 100644 index 00000000..83326dc4 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/io_nii.html @@ -0,0 +1,586 @@ + + + + + + + + + + unravel.image_io.io_nii — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.io_nii

+#!/usr/bin/env python3
+
+"""
+Use ``io_nii`` from UNRAVEL to convert the data type of a .nii.gz image and optionally scale the data.
+
+Usage:
+------
+    io_nii -i path/img.nii.gz -d float32
+
+Usage for z-score scaling (if 8 bit is needed):
+-----------------------------------------------
+    io_nii -i path/img.nii.gz -d uint8 -z
+
+Possible numpy data types: 
+    - Unsigned Integer: uint8, uint16, uint32, uint64
+    - Signed Integer: int8, int16, int32, int64
+    - Floating Point: float32, float64
+
+With --scale, the min intensity becomes dtype min and max intensity becomes dtype max. Every other intensity is scaled accordingly.
+With --binary, the image is binarized (0 or 1).
+With --zscore, the range of z-scored data from -3 to 3 is converted to 0 to 255.
+With --fixed_scale, the data is scaled using the provided min and max values.
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='Convert the data type of a .nii.gz image', formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.nii.gz', required=True, action=SM) + parser.add_argument('-d', '--data_type', help='Data type of output. For example: uint16 (numpy conventions)', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/new_img.nii.gz. Default: path/img_dtype.nii.gz', action=SM) + parser.add_argument('-f', '--fixed_scale', help='Scale data using fixed min and max values. Supply as "min,max"', default=None) + parser.add_argument('-s', '--scale', help='Scale the data to the range of the new data type', action='store_true', default=False) + parser.add_argument('-b', '--binary', help='Convert to binary image.', action='store_true', default=False) + parser.add_argument('-z', '--zscore', help='Convert the range of z-scored data (use uint8 data type).', action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def convert_dtype(ndarray, data_type, scale_mode='none', fixed_scale_range=None, zscore_range=(-3, 3), target_range=(0, 255)): + """ + Convert the data type of an ndarray and optionally scale the data. + + Parameters: + - ndarray: Input ndarray. + - data_type: Target data type. + - scale_mode: 'none', 'standard', or 'zscore'. Determines the scaling approach. + - fixed_scale_range: Tuple indicating the fixed range for scaling if scale_mode is 'fixed'. + - zscore_range: Tuple indicating the z-score range for scaling if scale_mode is 'zscore'. + - target_range: Tuple indicating the target range for the data type conversion. + + Returns: + - Converted ndarray with the specified data type and scaling. + """ + if scale_mode != 'none': + if scale_mode == 'standard': + print("Applying standard scaling...") + data_min, data_max = ndarray.min(), ndarray.max() + ndarray = (ndarray - data_min) / (data_max - data_min) * (target_range[1] - target_range[0]) + target_range[0] + elif scale_mode == 'fixed' and fixed_scale_range: + min_val, max_val = fixed_scale_range + print(f"Applying fixed range scaling from {min_val} to {max_val}...") + ndarray = (ndarray - min_val) / (max_val - min_val) * (target_range[1] - target_range[0]) + target_range[0] + ndarray = np.clip(ndarray, target_range[0], target_range[1]) + elif scale_mode == 'zscore': + print("Applying z-score based scaling (converting range from -3 to 3 to 0 to 255)...") + scale = (target_range[1] - target_range[0]) / (zscore_range[1] - zscore_range[0]) + offset = target_range[0] - zscore_range[0] * scale + ndarray = ndarray * scale + offset + ndarray = np.clip(ndarray, target_range[0], target_range[1]) + + # Clip the data to the target range if the data type is integer + if np.issubdtype(np.dtype(data_type), np.integer): + dtype_info = np.iinfo(data_type) if np.issubdtype(np.dtype(data_type), np.integer) else np.finfo(data_type) + ndarray = np.clip(ndarray, dtype_info.min, dtype_info.max) + + return ndarray.astype(np.dtype(data_type))
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load the .nii.gz file + nii_path = args.input if args.input.endswith('.nii.gz') else f'{args.input}.nii.gz' + nii = nib.load(nii_path) + + # Convert the data to a numpy array + img = nii.get_fdata(dtype=np.float32) + + # Convert the ndarray to the input data type + img = img.astype(args.data_type) + + # Optionally binarize the image + if args.binary: + img = np.where(img > 0, 1, 0) + + # Determine the scaling mode + scale_mode = 'standard' if args.scale else 'fixed' if args.fixed_scale else 'zscore' if args.zscore else 'none' + + # Determine target range based on specified data type and scaling mode + if args.zscore: + if args.data_type in ['float32', 'float64']: + # For floating-point types with z-score scaling, use the z-score range itself or a modified version + target_range = (-3, 3) + else: + # For uint8, map z-scores to the full range of the type + target_range = (0, 255) + else: + if np.issubdtype(np.dtype(args.data_type), np.integer): + dtype_info = np.iinfo(args.data_type) + else: + dtype_info = np.finfo(args.data_type) + target_range = (dtype_info.min, dtype_info.max) + + # Prepare fixed scale range if specified + fixed_scale_range = None + if args.fixed_scale: + fixed_scale_range = tuple(float(x) for x in args.fixed_scale.split(',')) + + # Convert the data type and optionally scale the data + new_img = convert_dtype(img, args.data_type, scale_mode=scale_mode, fixed_scale_range=fixed_scale_range, target_range=target_range) + + # Update the header's datatype + new_nii = nib.Nifti1Image(new_img, nii.affine, nii.header) + new_nii.header.set_data_dtype(np.dtype(args.data_type)) + + # Save the new .nii.gz file + if args.binary: + output_path = args.output if args.output else nii_path.replace('.nii.gz', f'_bin_{args.data_type}.nii.gz') + elif args.scale: + output_path = args.output if args.output else nii_path.replace('.nii.gz', f'_std_scaled_{args.data_type}.nii.gz') + elif scale_mode == 'zscore': + output_path = args.output if args.output else nii_path.replace('.nii.gz', f'_z_range_scaled_{args.data_type}.nii.gz') + else: + output_path = args.output if args.output else nii_path.replace('.nii.gz', f'_{args.data_type}.nii.gz') + nib.save(new_nii, output_path)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/metadata.html b/unravel/docs/_build/html/_modules/unravel/image_io/metadata.html new file mode 100644 index 00000000..d85d15af --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/metadata.html @@ -0,0 +1,540 @@ + + + + + + + + + + unravel.image_io.metadata — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.metadata

+#!/usr/bin/env python3
+
+"""
+Use ``io_metadata`` from UNRAVEL to save x/y and z voxel sizes in microns as well as image dimensions to a metadata file in each sample directory.
+
+Run this command from an experiment, sample?? folder, or provide -e/--exp_paths and -d/--dirs arguments to specify the experiment and sample directories.
+
+Usage for when metadata is extractable:
+---------------------------------------
+    io_metadata -i rel_path/full_res_img (can use glob patterns)
+
+Usage for when metadata is not extractable:
+-------------------------------------------
+    io_metadata -i tif_dir -x 3.5232 -z 6  # Use if metadata not extractable
+
+Inputs:
+    - .czi, .nii.gz, .h5, or TIF series (path should be relative to ./sample??)
+
+Outputs:
+    - ./parameters/metadata.txt (path should be relative to ./sample??)
+
+Next command:
+    - ``reg_prep`` for registration
+"""
+
+import argparse
+from pathlib import Path
+import cv2
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, resolve_path, save_metadata_to_file
+from unravel.core.utils import get_samples, initialize_progress_bar, print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='path/full_res_img (path relative to ./sample??)', required=True, action=SM) + parser.add_argument('-m', '--metad_path', help='path/metadata.txt. Default: parameters/metadata.txt', default="parameters/metadata.txt", action=SM) + parser.add_argument('-x', '--xy_res', help='xy resolution in um', type=float, default=None, action=SM) + parser.add_argument('-z', '--z_res', help='z resolution in um', type=float, default=None, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + + + + +
+[docs] +def get_dims_from_tifs(tifs_path): + # Get dims quickly from full res tifs (Using a generator without converting to a list to be memory efficient) + tifs = Path(tifs_path).resolve().glob("*.tif") # Generator + tif_file = next(tifs, None) # First item in generator + tif_img = cv2.imread(str(tif_file), cv2.IMREAD_UNCHANGED) # Load first tif + x_dim, y_dim, z_dim = (tif_img.shape[1], tif_img.shape[0], sum(1 for _ in tifs) + 1) # For z count tifs + 1 (next() uses 1 generator item) + return x_dim, y_dim, z_dim
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample_path in samples: + + # Resolve path to image + img_path = resolve_path(sample_path, path_or_pattern=args.input) + + # Resolve path to metadata file + metadata_path = resolve_path(sample_path, path_or_pattern=args.metad_path, make_parents=True) + + if metadata_path.exists(): + print(f'\n\n{metadata_path} exists. Skipping...') + print_metadata(metadata_path) + else: + # Load image and save metadata to file + if img_path.exists(): + if img_path.is_dir and args.xy_res is not None and args.z_res is not None: + x_dim, y_dim, z_dim = get_dims_from_tifs(img_path) + save_metadata_to_file(args.xy_res, args.z_res, x_dim, y_dim, z_dim, save_metadata=metadata_path) + else: + load_3D_img(img_path, desired_axis_order="xyz", xy_res=args.xy_res, z_res=args.z_res, return_metadata=True, save_metadata=metadata_path) + print(f'\n\n{metadata_path}:') + print_metadata(metadata_path) + else: + print(f" [red1]No match found for {args.input} in {sample_path}. Skipping...") + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/nii_hd.html b/unravel/docs/_build/html/_modules/unravel/image_io/nii_hd.html new file mode 100644 index 00000000..812de7c0 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/nii_hd.html @@ -0,0 +1,477 @@ + + + + + + + + + + unravel.image_io.nii_hd — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.nii_hd

+#!/usr/bin/env python3
+
+"""
+Use ``io_nii_hd`` from UNRAVEL to load a .nii.gz and print its header using nibabel.
+
+Usage:
+------
+    io_nii_hd -i path/img.nii.gz
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.nii.gz', required=True, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + np.set_printoptions(precision=4, suppress=True) + + nii = nib.load(args.input) + print(nii.header) + + current_axcodes_tuple = nib.orientations.aff2axcodes(nii.affine) + current_axcodes = ''.join(current_axcodes_tuple) + np.set_printoptions(precision=4, suppress=True) + print(f'\nOrientation: [default bold]{current_axcodes}[/]+\n')
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/nii_info.html b/unravel/docs/_build/html/_modules/unravel/image_io/nii_info.html new file mode 100644 index 00000000..f5bf5273 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/nii_info.html @@ -0,0 +1,493 @@ + + + + + + + + + + unravel.image_io.nii_info — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.nii_info

+#!/usr/bin/env python3
+
+"""
+Use ``io_nii_info`` from UNRAVEL to load an .nii.gz image and print its data type, shape, voxel sizes, and affine matrix using nibabel.
+
+Usage:
+------
+    io_nii_info -i path/img.nii.gz
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.nii.gz', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def nii_axis_codes(nii): + """Get and return axes codes (three letter orientation like RAS) from an nibabel NIfTI image""" + axcodes_tuple = nib.orientations.aff2axcodes(nii.affine) + axcodes = ''.join(axcodes_tuple) + return axcodes
+ + +
+[docs] +def main(): + args = parse_args() + + nii = nib.load(args.input) + + np.set_printoptions(precision=2, suppress=True) + + # Print data type + data_type = nii.get_data_dtype() + print(f'\nData type:\n[default bold]{data_type}') + + # Print dimensions + print(f'\nShape (x, y, z):\n{nii.shape}') + + # Print the voxel sizes + voxel_sizes = nii.header.get_zooms() + voxel_sizes = tuple(np.array(voxel_sizes) * 1000) + print(f'\nVoxel sizes (in microns):\n{voxel_sizes}') + + + # Print orientation and affine + axcodes = nii_axis_codes(nii) + np.set_printoptions(precision=4, suppress=True) + print(f'\nAffine matrix ([default bold]{axcodes}[/]):\n{nii.affine}\n')
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/nii_to_tifs.html b/unravel/docs/_build/html/_modules/unravel/image_io/nii_to_tifs.html new file mode 100644 index 00000000..2e7d797a --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/nii_to_tifs.html @@ -0,0 +1,485 @@ + + + + + + + + + + unravel.image_io.nii_to_tifs — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.nii_to_tifs

+#!/usr/bin/env python3
+
+"""
+Use ``io_nii_to_tifs`` from UNRAVEL to convert an image.nii.gz to tif series in an output_dir.
+
+Usage:
+------
+    io_nii_to_tifs -i path/image.nii.gz -o path/output_dir
+
+"""
+
+import argparse
+import os
+import nibabel as nib
+import numpy as np
+import tifffile as tif
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='image.nii.gz', action=SM) + parser.add_argument('-o', '--output_dir', help='Name of output folder', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def nii_to_tifs(nii_path, output_dir): + # Load the NIfTI file + nii_image = nib.load(nii_path) + data = nii_image.get_fdata(dtype=np.float32) + + # Ensure the output directory exists + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Iterate through the slices of the image and save each as a .tif + for i in range(data.shape[2]): + slice_ = data[:, :, i] + slice_ = np.rot90(slice_, 1) # '1' indicates one 90-degree rotation counter-clockwise + slice_ = np.flipud(slice_) # Flip vertically + tif.imwrite(os.path.join(output_dir, f'slice_{i:04d}.tif'), slice_) #not opening in FIJI as one stack
+ + +
+[docs] +def main(): + args = parse_args() + + nii_to_tifs(args.input, args.output_dir)
+ + +if __name__ == '__main__': + main() + +#Daniel Rijsketic 08/25/23 (Heifets lab) +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/nii_to_zarr.html b/unravel/docs/_build/html/_modules/unravel/image_io/nii_to_zarr.html new file mode 100644 index 00000000..d1e62c89 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/nii_to_zarr.html @@ -0,0 +1,506 @@ + + + + + + + + + + unravel.image_io.nii_to_zarr — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.nii_to_zarr

+#!/usr/bin/env python3
+
+"""
+Use ``io_nii_to_zarr`` from UNRAVEL to convert an image.nii.gz to an image.zarr
+
+Usage:
+------
+    io_nii_to_zarr -i path/img.nii.gz -o path/img.zarr
+"""
+
+import argparse
+import dask.array as da
+import nibabel as nib
+import numpy as np
+import zarr
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image.nii.gz', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/image.zarr', default=None, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def nii_to_ndarray(img_path): + nii_img = nib.load(img_path) + ndarray = np.asanyarray(nii_img.dataobj) # Preserves dtype + d_type = ndarray.dtype + return ndarray, d_type
+ + +@print_func_name_args_times() +def save_as_zarr(ndarray, output_path): + dask_array = da.from_array(ndarray, chunks='auto') + compressor = zarr.Blosc(cname='lz4', clevel=9, shuffle=zarr.Blosc.BITSHUFFLE) + dask_array.to_zarr(output_path, compressor=compressor, overwrite=True) + +
+[docs] +@print_func_name_args_times() +def save_as_zarr(ndarray, output_path, d_type): + ndarray = ndarray.astype(d_type) + dask_array = da.from_array(ndarray, chunks='auto') + compressor = zarr.Blosc(cname='lz4', clevel=9, shuffle=zarr.Blosc.BITSHUFFLE) + dask_array.to_zarr(output_path, compressor=compressor, overwrite=True)
+ + + +
+[docs] +def main(): + args = parse_args() + + img, d_type = nii_to_ndarray(args.input) + + if args.output: + output_path = args.output + else: + output_path = str(args.input).replace(".nii.gz", ".zarr") + + save_as_zarr(img, output_path, d_type)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/reorient_nii.html b/unravel/docs/_build/html/_modules/unravel/image_io/reorient_nii.html new file mode 100644 index 00000000..e048fe75 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/reorient_nii.html @@ -0,0 +1,657 @@ + + + + + + + + + + unravel.image_io.reorient_nii — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.reorient_nii

+#!/usr/bin/env python3
+
+"""
+Use ``io_reorient_nii`` from UNRAVEL to set the orientation of a .nii.gz or its affine matrix.
+
+Usage:
+------
+    io_reorient_nii -i image.nii.gz -t PIR -a -z
+
+Output:
+    - The new .nii.gz file with the new orientation (e.g., image_PIR.nii.gz or image_PIR_applied.nii.gz)
+
+The axis codes are:
+    R: Right / L: Left 
+    A: Anterior / P: Posterior
+    S: Superior / I: Inferior
+
+The orientation code is a 3-letter code that indicates the direction of the axes in the image.
+
+For the RAS+ orientation (default for NIfTI): 
+    - The right side is at the positive direction of the x-axis
+    - The anterior side is at the positive direction of the y-axis
+    - The superior side is at the positive direction of the z-axis
+
+The orientation code also indicates the orientation of the axes in the affine matrix.
+
+Example affine for RAS+ orientation:
+    [[1  0  0  0]
+    [ 0  1  0  0]
+    [ 0  0  1  0]
+    [ 0  0  0  1]]
+
+    - The 1st letter is R since the 1st diagonal value is positive (the right side is at the positive direction of the x-axis)
+    - The 2nd letter is A since the 2nd diagonal value is positive (the anterior side is at the positive direction of the y-axis)
+    - The 3rd letter is S since the 3rd diagonal value is positive (the superior side is at the positive direction of the z-axis)
+
+Example affine for LPS+ orientation:
+    [[-1  0  0  0]
+    [  0 -1  0  0]
+    [  0  0  1  0]
+    [  0  0  0  1]]
+
+        -For LPS, the 1st letter is L since the 1st diagonal value is negative.
+        -The 2nd letter is P since the 2nd diagonal value is negative.
+        -The 3rd letter is S since the 3rd diagonal value is positive.
+
+Example affine for PIR+ orientation (default for CCFv3):
+    [[ 0  0  1  0]
+    [ -1  0  0  0]
+    [  0 -1  0  0]
+    [  0  0  0  1]]
+
+For PIR:
+    First letter determination: 
+        -The 1st column has a non-zero value at the 2nd row, so the 1st letter is either A or P (2nd letter of the default 'RAS' orientation code).
+        -Since the valud is negative, the 1st letter is P
+    Second letter determination:
+        -The 2nd column has a non-zero value at the 3rd row, so the 2nd letter is either S or I (3rd letter of the default 'RAS' orientation code).
+        -Since the value is negative, the 2nd letter is I
+    Third letter determination:
+        -The 3rd column has a non-zero value at the 1st row, so the 3rd letter is either R or L (1st letter of the default 'RAS' orientation code).
+        -Since the value is positive, the 3rd letter is R
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from nibabel.orientations import axcodes2ornt, ornt_transform, io_orientation, aff2axcodes, apply_orientation
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.nii.gz', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/img.nii.gz', required=True, action=SM) + parser.add_argument('-t', '--target_ort', help='Target orientation axis codes (e.g., RAS)', required=True, action=SM) + parser.add_argument('-z', '--zero_origin', help='Provide flag to zero the origin of the affine matrix.', action='store_true', default=False) + parser.add_argument('-a', '--apply', help='Provide flag to apply the new orientation to the ndarray data.', action='store_true', default=False) + parser.add_argument('-fc', '--form_code', help='Set the sform and qform codes for spatial coordinate type (1 = scanner; 2 = aligned)', type=int, default=None) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def transform_nii_affine(nii, target_ort, zero_origin=False): + """Transform the affine matrix of a NIfTI image to a target orientation and return the new affine matrix + + Args: + nii (nibabel.nifti1.Nifti1Image): NIfTI image + target_ort (str): Target orientation axis codes (e.g., RAS) + zero_origin (bool): Zero the origin of the affine matrix. Default: False + """ + + # Get the current axis codes + current_axcodes_tuple = aff2axcodes(nii.affine) + current_axcodes = ''.join(current_axcodes_tuple) + np.set_printoptions(precision=4, suppress=True) + print(f'\nCurrent affine matrix ({current_axcodes}): \n{nii.affine}') + + # Get the current orientation + current_orientation = axcodes2ornt(current_axcodes_tuple) + + # Get the index of the target orientation + current_ort_first_col_index = int(current_orientation[0][0]) + current_ort_second_col_index = int(current_orientation[1][0]) + current_ort_third_col_index = int(current_orientation[2][0]) + + # Convert the current affine to the target orientation + target_orientation = axcodes2ornt(target_ort) + + # Get the sign of the target orientation + sign_of_first_target_direction = target_orientation[0][1] + sign_of_second_target_direction = target_orientation[1][1] + sign_of_third_target_direction = target_orientation[2][1] + + # Get the index of the target orientation + new_ort_first_col_index = int(target_orientation[0][0]) + new_ort_second_col_index = int(target_orientation[1][0]) + new_ort_third_col_index = int(target_orientation[2][0]) + + # Make an affine matrix for the target orientation + new_affine = np.zeros((4,4)) + new_affine[:,3] = 1 + + # Set the columns of the new affine matrix to the target orientation + new_affine[new_ort_first_col_index, 0] = sign_of_first_target_direction * np.abs(nii.affine[current_ort_first_col_index, 0]) + new_affine[new_ort_second_col_index, 1] = sign_of_second_target_direction * np.abs(nii.affine[current_ort_second_col_index, 1]) + new_affine[new_ort_third_col_index, 2] = sign_of_third_target_direction * np.abs(nii.affine[current_ort_third_col_index, 2]) + + # Set the origin of the new affine matrix to the origin of the current affine matrix + new_affine[0:3,3] = nii.affine[0:3,3] + + # Zero the origin of the affine matrix + if zero_origin: + for i in range(3): + new_affine[i,3] = 0 + + # Get the axis codes of the new affine + new_axcodes_tuple = nib.orientations.aff2axcodes(new_affine) + new_axcodes = ''.join(new_axcodes_tuple) + print(f'\nNew affine matrix ({new_axcodes}): \n{new_affine}') + + return new_affine
+ + +
+[docs] +def reorient_nii(nii, target_ort, zero_origin=False, apply=False, form_code=None): + """Reorient a NIfTI image or its affine matrix to a target orientation. + + Args: + nii_path (str): Path to the NIfTI image + target_ort (str): Target orientation axis codes (e.g., RAS) + zero_origin (bool): Zero the origin of the affine matrix. Default: False + apply (bool): Apply the new orientation to the ndarray data. Default: False + form_code (int): Set the sform and qform codes for spatial coordinate type (1 = scanner; 2 = aligned). Default: None (get from the input NIfTI image) + + Returns: + If apply True: new_nii (nibabel.nifti1.Nifti1Image): NIfTI image with the new orientation + If apply False: new_affine (np.ndarray): New affine matrix + + """ + + # Optionally apply the orientation change to the image data + if apply: + print('Applying orientation change to the image data...') + img = nii.get_fdata(dtype=np.float32) + current_orientation = io_orientation(nii.affine) + target_orientation = axcodes2ornt(target_ort) + orientation_change = ornt_transform(current_orientation, target_orientation) + img = apply_orientation(img, orientation_change) + else: + img = nii.get_fdata(dtype=np.float32) + + + # Check data type + data_type = nii.header.get_data_dtype() + + # For integer data types, round the values to the nearest integer + if np.issubdtype(data_type, np.integer): + img = np.round(img).astype(data_type) + + # Get the new affine matrix + new_affine = transform_nii_affine(nii, target_ort, zero_origin=zero_origin) + + # Make the new NIfTI image + new_nii = nib.Nifti1Image(img, new_affine) + new_nii.header.set_data_dtype(data_type) + + # Set the header information + new_nii.header['xyzt_units'] = 10 # mm, s + new_nii.header['regular'] = b'r' + + if form_code: + new_nii.header.set_qform(new_affine, code=form_code) + new_nii.header.set_sform(new_affine, code=form_code) + else: + new_nii.header.set_qform(new_affine, code=int(nii.header['qform_code'])) + new_nii.header.set_sform(new_affine, code=int(nii.header['sform_code'])) + + return new_nii
+ + +
+[docs] +def main(): + args = parse_args() + + nii = nib.load(args.input) + new_nii = reorient_nii(nii, args.target_ort, zero_origin=args.zero_origin, apply=args.apply, form_code=args.form_code) + + # Save the new .nii.gz file + if args.output: + nib.save(new_nii, args.output) + else: + if args.apply: + nib.save(new_nii, args.input.replace('.nii.gz', f'_{args.target_ort}_applied.nii.gz')) + else: + nib.save(new_nii, args.input.replace('.nii.gz', f'_{args.target_ort}.nii.gz'))
+ + + +if __name__ == '__main__': + install() + args = parse_args() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/tif_to_tifs.html b/unravel/docs/_build/html/_modules/unravel/image_io/tif_to_tifs.html new file mode 100644 index 00000000..3bad746f --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/tif_to_tifs.html @@ -0,0 +1,588 @@ + + + + + + + + + + unravel.image_io.tif_to_tifs — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.tif_to_tifs

+#!/usr/bin/env python3
+
+"""
+Use ``io_tif_to_tifs`` from UNRAVEL to load a 3D .tif image and save it as tifs.
+
+Usage:
+------
+    io_tif_to_tifs -i <path/image.tif> -t 488
+
+Inputs: 
+    - image.tif # either from -i path/image.tif or largest <asterisk>.tif in cwd
+
+Outputs:
+    - ./<tif_dir_out>/slice_<asterisk>.tif series
+    - ./parameters/metadata (text file)
+
+Next command: 
+    ``reg_prep`` for registration
+"""
+
+import argparse
+import glob
+import os
+import numpy as np
+from pathlib import Path
+from rich import print
+from tifffile import imwrite
+import tifffile 
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='Loads 3D .tif image, saves as tifs. Also, saves xy and z voxel size in microns', formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image.tif', action=SM) + parser.add_argument('-t', '--tif_dir', help='Name of output folder for outputting tifs', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def find_largest_tif_file(): + """ Find and return the path to the largest .tif file in the current directory """ + largest_file = None + max_size = -1 + + for file in glob.glob('*.tif'): + size = os.path.getsize(file) + if size > max_size: + max_size = size + largest_file = file + + return largest_file
+ + +
+[docs] +def load_3D_tif(tif_path, desired_axis_order="xyz", return_res=False): + """Load full res image from a 3D .tif and return ndarray + Default: axis_order=xyz (other option: axis_order="zyx") + Default: returns: ndarray + If return_res=True returns: ndarray, xy_res, z_res (resolution in um)""" + with tifffile.TiffFile(tif_path) as tif: + print(f"\n Loading {tif_path} as ndarray") + ndarray = tif.asarray() # Load image into memory (if not enough RAM, chunck data [e.g., w/ dask array]) + ndarray = np.transpose(ndarray, (2, 1, 0)) if desired_axis_order == "xyz" else ndarray + print(f' {ndarray.shape=}') + + if return_res: + xy_res, z_res = metadata_from_3D_tif(tif_path) + return ndarray, xy_res, z_res + else: + return ndarray
+ + +
+[docs] +def metadata_from_3D_tif(tif_path): + """Returns tuple with xy_voxel_size and z_voxel_size in microns from a 3D .tif""" + + with tifffile.TiffFile(tif_path) as tif: + # Get the first page of the tif file where the metadata is usually stored + first_page = tif.pages[0] + + # Access the tags dictionary directly + tags_dict = first_page.tags + + # Look for the XResolution tag (tag number 282) # 'XResolution': (numerator, denominator) + if 282 in tags_dict: + res_numerator, res_denominator = tags_dict[282].value + + # Calculate the resolution in pixels per micron + resolution = res_numerator / res_denominator + + # Calculate x/y voxel size in microns + xy_res = 1 / resolution + else: + raise ValueError("XResolution tag not found in file.") + + # Extract z-step size in microns + imagej_metadata = tif.imagej_metadata + if imagej_metadata and 'spacing' in imagej_metadata: + z_res = imagej_metadata['spacing'] + else: + raise ValueError("Z-spacing information not found in ImageJ metadata.") + + return xy_res, z_res
+ + +
+[docs] +def save_as_tifs(ndarray, tif_dir_out, ndarray_axis_order="xyz"): + """Save <ndarray> as tifs in <Path(tif_dir_out)>""" + tif_dir_out = Path(tif_dir_out) + tif_dir_out.mkdir(parents=True, exist_ok=True) + + if ndarray_axis_order == "xyz": + ndarray = np.transpose(ndarray, (2, 1, 0)) # Transpose to zyx (tiff expects zyx) + for i, slice_ in enumerate(ndarray): + slice_file_path = tif_dir_out / f"slice_{i:04d}.tif" + imwrite(str(slice_file_path), slice_) + print(f" Output: [default bold]{tif_dir_out}\n")
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.input: + tif_path = args.input + else: + print("\n [red1]Please provide a path/image.tif for the -t option\n") + import sys ; sys.exit() + + # Load .tif image (highest res dataset) as ndarray and extract voxel sizes in microns + img, xy_res, z_res = load_3D_tif(tif_path, desired_axis_order="xyz", return_res=True) + + # Make parameters directory in the sample?? folder + os.makedirs("parameters", exist_ok=True) + + # Save metadata to text file so resolution can be obtained by other commands/modules + metadata_txt_path = Path(".", "parameters", "metadata") + with open(metadata_txt_path, 'w') as file: + file.write(f"Voxel size: {xy_res:.4f}x{xy_res:.4f}x{z_res:.4f} µm^3") + + # Save as tifs + if args.tif_dir is None: + print(" [red1]The tif_dir argument was not provided. Please specify the directory.") + import sys ; sys.exit() + else: + tifs_output_path = Path(".", args.tif_dir) + + save_as_tifs(img, tifs_output_path)
+ + + +if __name__ == '__main__': + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_io/zarr_to_nii.html b/unravel/docs/_build/html/_modules/unravel/image_io/zarr_to_nii.html new file mode 100644 index 00000000..b39fc943 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_io/zarr_to_nii.html @@ -0,0 +1,504 @@ + + + + + + + + + + unravel.image_io.zarr_to_nii — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_io.zarr_to_nii

+#!/usr/bin/env python3
+
+"""
+Use ``io_zarr_to_nii`` from UNRAVEL to convert an image.zarr to an image.nii.gz.
+
+Usage:
+------
+    io_zarr_to_nii -i path/img.zarr -o path/img.nii.gz
+
+Notes:
+    - Outputs RAS orientation
+    - Scaling not preserved
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+import zarr
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image.zarr', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/image.nii.gz', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def zarr_to_ndarray(img_path): + zarr_dataset = zarr.open(img_path, mode='r') + return np.array(zarr_dataset)
+ + +
+[docs] +def define_zarr_to_nii_output(output_path): + if args.output: + return args.output + else: + return str(args.input).replace(".zarr", ".nii.gz")
+ + +
+[docs] +@print_func_name_args_times() +def save_as_nii(ndarray, output_path): + affine = np.eye(4) + nifti_image = nib.Nifti1Image(ndarray, affine) + nib.save(nifti_image, output_path)
+ + + +
+[docs] +def main(): + args = parse_args() + + img = zarr_to_ndarray(args.input) + output_path = define_zarr_to_nii_output(args.output) + save_as_nii(img, output_path)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/DoG.html b/unravel/docs/_build/html/_modules/unravel/image_tools/DoG.html new file mode 100644 index 00000000..393b8b23 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/DoG.html @@ -0,0 +1,520 @@ + + + + + + + + + + unravel.image_tools.DoG — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.DoG

+#!/usr/bin/env python3
+
+"""
+Use ``img_DoG`` from UNRAVEL to apply Difference of Gaussians to a single image.
+
+Usage: 
+------
+    img_DoG -i input.tif -g1 1.0 -g2 2.0
+
+Difference of Gaussians:
+    - Sigma1 and sigma2 are the standard deviations for the first and second Gaussian blurs
+    - Simga2 (the larger blur) should be ~ 1.0 to 1.5 times the radius of these features of interest
+        - E.g., if nuclei have a radius of ~1.5 to 2.5 pixels, sigma2 might be 1.5 to 3.0
+    - Sigma1 (the smaller blur) should be smaller than the size of the features you want to keep, ideally around the size of the noise
+        - E.g., if noise is ~1 pixel in size, sigma1 might be 0.5 to 1
+    - The ratio of simga2 to sigma1 should ideally be at least 1.5 to 2. This helps ensure that the blurring difference is significant enough to highlight the features of interest.
+
+Note: 
+    - This command is intended to test the DoG method on a single image.
+    - 2D DoG is not implemented in vstats_prep. 
+    - DoG could be added to vstats_prep in the future if needed. 
+    - 3D spatial averaging and 2D rolling ball background subtraction are used in vstats_prep instead.
+"""
+
+import argparse
+import cv2
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='Path to the input TIFF file.', required=True, action=SM) + parser.add_argument('-o', '--output', help='Path to save the output TIFF file.', default=None, action=SM) + parser.add_argument('-g1', '--sigma1', help='Sigma for the first Gaussian blur in DoG (targets noise)', default=None, required=True, type=float) + parser.add_argument('-g2', '--sigma2', help='Sigma for the second Gaussian blur in DoG (targets signal).', default=None, required=True, type=float) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Add support for other image types and 3D images. + +
+[docs] +def load_tif(tif_path): + '''Load a single tif file using OpenCV and return ndarray.''' + img = cv2.imread(tif_path, cv2.IMREAD_UNCHANGED) + if img is None: + raise FileNotFoundError(f'Could not load the TIFF file from {tif_path}') + return img
+ + +
+[docs] +def difference_of_gaussians(img, sigma1, sigma2): + '''Subtract one blurred version of the image from another to highlight edges.''' + blur1 = cv2.GaussianBlur(img, (0, 0), sigma1) + blur2 = cv2.GaussianBlur(img, (0, 0), sigma2) + dog_img = cv2.subtract(blur1, blur2) + return dog_img
+ + +
+[docs] +def save_tif(img, output_path): + '''Save an image as a tif file.''' + cv2.imwrite(output_path, img)
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load the image + img = load_tif(args.input) + + # Apply difference of Gaussians if sigmas are provided + img = difference_of_gaussians(img, args.sigma1, args.sigma2) + print(f'Applied Difference of Gaussians with sigmas {args.sigma1} and {args.sigma2}.') + + # Save the processed image + output_path = args.output if args.output is not None else args.input.replace('.tif', f'_DoG{args.sigma2}-{args.sigma1}.tif') + save_tif(img, output_path)
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/atlas/relabel_nii.html b/unravel/docs/_build/html/_modules/unravel/image_tools/atlas/relabel_nii.html new file mode 100644 index 00000000..13d16a62 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/atlas/relabel_nii.html @@ -0,0 +1,508 @@ + + + + + + + + + + unravel.image_tools.atlas.relabel_nii — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.atlas.relabel_nii

+#!/usr/bin/env python3
+
+"""
+Use ``atlas_relabel`` from UNRAVEL to convert intensities (e.g., atlas label IDs) based on a CSV.
+
+Usage: 
+------
+    atlas_relabel -i path/old_image.nii.gz -o path/new_image.nii.gz -ic path/input.csv -oc volume_summary -odt uint16
+"""
+
+import argparse
+import numpy as np
+import nibabel as nib
+import pandas as pd
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/old_image.nii.gz', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/new_image.nii.gz', required=True, action=SM) + parser.add_argument('-ic', '--csv_input', help='path/input.csv w/ old IDs in column 1 and new IDs in column 2', required=True, action=SM) + parser.add_argument('-oc', '--csv_output', help='Optionally provide prefix to output label volume summaries (e.g., volume_summary)', default=None, action=SM) + parser.add_argument('-odt', '--data_type', help='Output data type. Default: uint16', default="uint16", action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load the specified columns from the CSV with CCFv3 info + if Path(args.csv_input).exists() and args.csv_input.endswith('.csv'): + df = pd.read_csv(args.csv_input) + else: + raise FileNotFoundError(f'CSV file not found: {args.csv_input}') + + # Get column names + columns = df.columns + + # Convert values in columns to integers + df[columns] = df[columns].astype(int) + + # Load the NIfTI image + nii = nib.load(args.input) + img = nii.get_fdata(dtye=np.float32) + + # Initialize an empty ndarray with the same shape as img and data type uint16 + if args.data_type: + new_img_array = np.zeros(img.shape, dtype=args.data_type) + else: + new_img_array = np.zeros(img.shape, dtype=np.uint16) + + # Replace voxel values in the new image array with the new labels + for old_label, new_label in zip(df[columns[0]], df[columns[1]]): + mask = img == old_label + new_img_array[mask] = new_label + + # Convert the ndarray to an NIfTI image and save + new_nii = nib.Nifti1Image(new_img_array, nii.affine, nii.header) + nib.save(new_nii, args.output) + + # Summarize the volume for each label before and after the replacement + if args.csv_output: + old_labels, counts_old_labels = np.unique(img, return_counts=True) + new_labels, counts_new_labels = np.unique(new_img_array, return_counts=True) + volume_summary_old_labels = pd.DataFrame({columns[0]: old_labels, 'voxel_count': counts_old_labels}) + volume_summary_new_labels = pd.DataFrame({columns[1]: new_labels, 'voxel_count': counts_new_labels}) + volume_summary_old_labels.to_csv(f'{args.csv_output}_old_labels.csv', index=False) + volume_summary_new_labels.to_csv(f'{args.csv_output}_new_labels.csv', index=False)
+ + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/atlas/wireframe.html b/unravel/docs/_build/html/_modules/unravel/image_tools/atlas/wireframe.html new file mode 100644 index 00000000..3975705b --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/atlas/wireframe.html @@ -0,0 +1,575 @@ + + + + + + + + + + unravel.image_tools.atlas.wireframe — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.atlas.wireframe

+#!/usr/bin/env python3
+
+"""
+Use ``atlas_wireframe`` from UNRAVEL to generate a thin wireframe image from an atlas NIfTI file.
+
+Usage: 
+------
+    atlas_wireframe -i path.atlas.nii.gz 
+
+Outlines are generated outside the regions and not inside smaller regions. 
+For regions at the surface of the brain, the outlines are internalized.
+
+Outputs: 
+    - path/atlas_img_W.nii.gz # Wireframe image
+    - path/atlas_img_W_IDs.nii.gz # Wireframe image with region IDs
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from concurrent.futures import ThreadPoolExecutor
+import pandas as pd
+from scipy.ndimage import binary_dilation, binary_erosion
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='Generate a thin wireframe image from an atlas NIfTI file.', formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/atlas_img.nii.gz', required=True, action=SM) + parser.add_argument('-wo', '--wire_output', help='Wireframe image output path. Default: path/atlas_img_W.nii.gz', action=SM) + parser.add_argument('-id', '--id_output', help='Wireframe image with atlas IDs output path. Default: path/atlas_img_W_IDs.nii.gz', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def process_intensity(args): + """ + Process a single intensity: generate its binary mask, dilate, and find boundaries. + + Args: + args (tuple): A tuple containing the ndarray and the intensity value. + + Returns a tuple with: + binary_mask (np.ndarray): A binary mask for the given intensity. + boundary (np.ndarray): A binary boundary mask for the given intensity + """ + atlas_ndarray, intensity = args + if intensity == 0: # Skip background + # Return an empty mask and boundary + return np.zeros(atlas_ndarray.shape, dtype=bool), np.zeros(atlas_ndarray.shape, dtype=bool) + + binary_mask = atlas_ndarray == intensity # Create a binary mask for the current region + dilated = binary_dilation(binary_mask) # Dilate the mask + boundary = dilated != binary_mask # Outline the region (with the line outside the region) + return binary_mask, boundary
+ + +
+[docs] +def generate_wireframe(atlas_ndarray, unique_intensities): + """Generate a wireframe image of an atlas NIfTI file where outlines are outside the regions and not inside smaller regions. + + Args: + atlas_ndarray (np.ndarray): A 3D numpy array of an atlas NIfTI file. + unique_intensities (np.ndarray): A list of unique intensity values in the atlas ndarray (sorted from smallest to largest regions) + + Returns: + wireframe_image (np.ndarray): A binary wireframe image (1 = wireframe, 0 = background; uint8) + wireframe_image_IDs (np.ndarray): A wireframe image with region IDs (uint16) + """ + + # Process intensities and boundaries with parallel execution + with ThreadPoolExecutor() as executor: + args = [(atlas_ndarray, intensity) for intensity in unique_intensities] + results = list(executor.map(process_intensity, args)) # List of (binary_mask, boundary) tuples + + wireframe = np.zeros(atlas_ndarray.shape, dtype=bool) # Initialize empty wireframe + processed_regions_mask = np.zeros(atlas_ndarray.shape, dtype=bool) # Initialize empty mask + + for binary_mask, boundary in results: + # Add binary mask to the processed regions mask + processed_regions_mask = np.logical_or(processed_regions_mask, binary_mask) + + # Add the boundary in areas outside of the processed regions mask (excludes boundaries inside smaller regions) + wireframe = np.logical_or(wireframe, np.logical_and(boundary, np.logical_not(processed_regions_mask))) + + # Internalize outlines at the surfaces of the brain + brain_mask = atlas_ndarray > 0 + brain_mask_eroded = binary_erosion(brain_mask) + brain_outline = brain_mask_eroded != brain_mask + wireframe = wireframe * brain_mask # Zero out the wireframe outside the brain + wireframe = np.logical_or(wireframe, brain_outline) # Add the brain outline to the wireframe + + # Convert boolean wireframe to binary image (1 = wireframe, 0 = background) + wireframe_img = wireframe.astype(np.uint16) + + # Add in Allen brain atlas region IDs (useful for coloring w/ a LUT) + wireframe_img_IDs = wireframe_img * atlas_ndarray + return wireframe_img.astype(np.uint8), wireframe_img_IDs.astype(np.uint16)
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load the NIfTI file + atlas_nii = nib.load(args.input) + atlas_ndarray = atlas_nii.get_fdata(dtype=np.float32) + + # Generate a binary mask for each unique intensity value + unique_intensities, voxel_counts = np.unique(atlas_ndarray, return_counts=True) + + # Convert to int + unique_intensities = np.array([int(i) for i in unique_intensities]) + + # Create df with unique intensities and counts + df = pd.DataFrame({'intensity': unique_intensities, 'voxel_count': voxel_counts}) + df = df.sort_values('voxel_count', ascending=True) + + # Sort the unique_intensities list based on the size of their corresponding regions (smallest to largest) + unique_intensities = df['intensity'].values + + # Generate the wireframe image + wireframe_img, wireframe_img_IDs = generate_wireframe(atlas_ndarray, unique_intensities) + + # Save the binary wireframe image + if args.wire_output: + wire_output = args.wire_output + else: + wire_output = str(args.input).replace('.nii.gz', '_W.nii.gz') + nib.save(nib.Nifti1Image(wireframe_img, atlas_nii.affine, atlas_nii.header), wire_output) + + # Save the wireframe image with region IDs + if args.id_output: + id_output = args.id_output + else: + id_output = str(args.input).replace('.nii.gz', '_W_IDs.nii.gz') + nib.save(nib.Nifti1Image(wireframe_img_IDs, atlas_nii.affine, atlas_nii.header), id_output)
+ + + +if __name__ == '__main__': + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/avg.html b/unravel/docs/_build/html/_modules/unravel/image_tools/avg.html new file mode 100644 index 00000000..15141718 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/avg.html @@ -0,0 +1,500 @@ + + + + + + + + + + unravel.image_tools.avg — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.avg

+#!/usr/bin/env python3
+
+"""
+Use ``img_avg`` from UNRAVEL to average NIfTI images.
+
+Usage:
+------
+    img_avg -i "<asterisk>.nii.gz" -o avg.nii.gz
+"""
+
+import argparse
+import numpy as np
+import nibabel as nib
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--inputs', help='Input file(s) or pattern(s) to process. Default is "*.nii.gz".', nargs='*', default=['*.nii.gz'], action=SM) + parser.add_argument('-o', '--output', help='Output file name. Default is "avg.nii.gz".', default='avg.nii.gz', action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def main(): + args = parse_args() + + # Resolve file patterns to actual file paths + all_files = [] + for pattern in args.inputs: + all_files.extend(Path().glob(pattern)) + + print(f'\nAveraging: {str(all_files)}\n') + + if not all_files: + print("No NIfTI files found. Exiting.") + return + + # Initialize sum array and affine matrix + sum_image = None + affine = None + + # Process each file + for file_path in all_files: + nii = nib.load(str(file_path)) + if sum_image is None: + sum_image = np.asanyarray(nii.dataobj, dtype=np.float64).squeeze() # Use float64 to avoid overflow + affine = nii.affine + header = nii.header + data_type = nii.header.get_data_dtype() + else: + sum_image += np.asanyarray(nii.dataobj, dtype=np.float64).squeeze() + + + # Calculate the average + average_image = sum_image / len(all_files) + + # Save the averaged image + averaged_nii = nib.Nifti1Image(average_image, affine, header) + averaged_nii.set_data_dtype(data_type) + nib.save(averaged_nii, args.output) + print("Saved averaged image to avg.nii.gz\n")
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/bbox.html b/unravel/docs/_build/html/_modules/unravel/image_tools/bbox.html new file mode 100644 index 00000000..7ff76404 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/bbox.html @@ -0,0 +1,499 @@ + + + + + + + + + + unravel.image_tools.bbox — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.bbox

+#!/usr/bin/env python3
+
+"""
+Use ``img_bbox`` from UNRAVEL to load an image (.czi, .nii.gz, or tif series) and save bounding boxes as txt files.
+
+Usage:
+------
+    img_bbox -i path/img -o path/bounding_boxes
+"""
+
+import argparse
+from pathlib import Path
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.img_tools import find_bounding_box, cluster_IDs
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='', formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.czi, path/img.nii.gz, or path/tif_dir', action=SM, required=True) + parser.add_argument('-o', '--output', help='path to output dir. Default: bounding_boxes', action=SM) + parser.add_argument('-ob', '--outer_bbox', help='path/outer_bbox.txt (bbox for voxels > 0)', action=SM) + parser.add_argument('-c', '--cluster', help='Cluster intensity to get bbox and crop', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def main(): + args = parse_args() + + img = load_3D_img(args.input) + + # Make output dir + if args.output: + output_path = Path(args.output).resolve() + else: + output_path = Path(args.input).parent.resolve() / 'bounding_boxes' + output_path.mkdir(parents=True, exist_ok=True) + + # Save outer bbox as txt + if args.outer_bbox: + xmin, xmax, ymin, ymax, zmin, zmax = find_bounding_box(img) + output = output_path / Path(args.input.replace('.nii.gz', f'_outer_bbox.txt')).name + with open(args.outer_bbox, 'w') as f: + f.write(f"{xmin}:{xmax}, {ymin}:{ymax}, {zmin}:{zmax}") + + # Save cluster bboxes as txt + if args.cluster: + clusters = [int(args.cluster)] + else: + clusters = cluster_IDs(img, min_extent=1) + + for cluster in clusters: + xmin, xmax, ymin, ymax, zmin, zmax = find_bounding_box(img, cluster_ID=cluster) + output = output_path / Path(args.input.replace('.nii.gz', f'_cluster{cluster}_bbox.txt')).name + with open(output, 'w') as f: + f.write(f"{xmin}:{xmax}, {ymin}:{ymax}, {zmin}:{zmax}")
+ + + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/extend.html b/unravel/docs/_build/html/_modules/unravel/image_tools/extend.html new file mode 100644 index 00000000..ed2b713d --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/extend.html @@ -0,0 +1,546 @@ + + + + + + + + + + unravel.image_tools.extend — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.extend

+#!/usr/bin/env python3
+
+"""
+Use ``img_extend`` from UNRAVEL to load a 3D image, extend one side, and save it as tifs
+
+Usage:
+    img_extend -i ochann -o ochann_extended -e 100 -s back -v
+"""
+
+import argparse
+from pathlib import Path
+import numpy as np
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_tifs
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times, initialize_progress_bar, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-p', '--pattern', help='Pattern for folders to process. If no matches, use current dir. Default: sample??', default='sample??', action=SM) + parser.add_argument('--dirs', help='List of folders to process. Overrides --pattern', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='path/image or path/image_dir', default=None, action=SM) + parser.add_argument('-o', '--out_dir_name', help="Output folder name.", required=True, action=SM) + parser.add_argument('-s', '--side', help="Side to extend. Options: 'front', 'back', 'left', 'right', 'top', 'bottom'. Default: 'front'", default='front', action=SM) + parser.add_argument('-e', '--extension', help="Number of voxels to extend", type=int, action=SM) + parser.add_argument('-v', '--verbose', help='Enable verbose mode', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def extend_one_side_3d_array(ndarray, side, extension): + """Extend a 3D ndarray on one side ('front', 'back', 'left', 'right', 'top', 'bottom') by X voxels""" + # TODO: Add option(s) to extend or crop multiple sides + + if side not in ['front', 'back', 'left', 'right', 'top', 'bottom']: + raise ValueError("Side must be 'front', 'back', 'left', 'right', 'top', or 'bottom'") + + original_shape = ndarray.shape + extended_shape = list(original_shape) + + if side in ['front', 'back']: + extended_shape[2] += extension + elif side in ['left', 'right']: + extended_shape[0] += extension + elif side in ['top', 'bottom']: + extended_shape[1] += extension + + extended_array = np.zeros(extended_shape, dtype=ndarray.dtype) + + if side == 'front': + extended_array[:, :, extension:] = ndarray + elif side == 'back': + extended_array[:, :, :original_shape[2]] = ndarray + elif side == 'left': + extended_array[extension:, :, :] = ndarray + elif side == 'right': + extended_array[:original_shape[0], :, :] = ndarray + elif side == 'top': + extended_array[:, extension:, :] = ndarray + elif side == 'bottom': + extended_array[:, :original_shape[1], :] = ndarray + + return extended_array
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern) + + if samples == ['.']: + samples[0] = Path.cwd().name + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to tif directory + cwd = Path(".").resolve() + + sample_path = Path(sample).resolve() if sample != cwd.name else Path().resolve() + + if args.input: + input_path = Path(args.input).resolve() + else: + input_path = Path(sample_path, args.chann_name).resolve() + + # Load image + img = load_3D_img(input_path, return_res=False) + + # Extend image + extended_img = extend_one_side_3d_array(img, args.side, args.extension) + + # Define output path + output_dir = Path(sample_path, args.out_dir_name).resolve() + + # Save extended image + save_as_tifs(extended_img, output_dir, "xyz") + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/max.html b/unravel/docs/_build/html/_modules/unravel/image_tools/max.html new file mode 100644 index 00000000..3bf1c869 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/max.html @@ -0,0 +1,481 @@ + + + + + + + + + + unravel.image_tools.max — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.max

+#!/usr/bin/env python3
+
+"""
+Use ``img_max`` from UNRAVEL to load an image.nii.gz and print its max intensity value.
+
+Usage: 
+------
+    img_max -i path/image.nii.gz
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image.nii.gz', default=None, action=SM) + return parser.parse_args()
+ + + +
+[docs] +def find_max_intensity(file_path): + """Find the maximum intensity value in the NIfTI image file.""" + # Load the .nii.gz file + nii_img = nib.load(file_path) + + # Get the data from the file + data = nii_img.get_fdata(dtype=np.float32) + + # Find the maximum intensity value in the data + max_intensity = int(data.max()) + + return max_intensity
+ + + +
+[docs] +def main(): + args = parse_args() + max_intensity = find_max_intensity(args.input) + print(max_intensity)
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/pad.html b/unravel/docs/_build/html/_modules/unravel/image_tools/pad.html new file mode 100644 index 00000000..2038f1f2 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/pad.html @@ -0,0 +1,493 @@ + + + + + + + + + + unravel.image_tools.pad — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.pad

+#!/usr/bin/env python3
+
+"""
+Use ``img_pad`` from UNRAVEL to add 15 percent of padding to an image.nii.gz and save it.
+
+Usage:
+------
+    img_pad -i reg_inputs/autofl_50um.nii.gz
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from rich.traceback import install
+
+from unravel.image_io.nii_info import nii_axis_codes
+from unravel.image_io.reorient_nii import reorient_nii
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.img_tools import pad
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='Adds 15 percent of padding to an image and saves it', formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.nii.gz', required=True, action=SM) + parser.add_argument('-ort', '--ort_code', help='3 letter orientation code of fixed image if not set in fixed_img (e.g., RAS)', action=SM) + parser.add_argument('-r', '--ref_nii', help='Reference image for setting the orientation code', action=SM) + parser.add_argument('-o', '--output', help='path/img.nii.gz. Default: None (saves as path/img_pad.nii.gz) ', default=None, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + nii = nib.load(args.input) + + data_type = nii.header.get_data_dtype() + img = np.asanyarray(nii.dataobj, dtype=data_type).squeeze() + + # Pad the image + img = pad(img, pad_width=0.15) + + # Save the padded image + fixed_img_padded_nii = nib.Nifti1Image(img, nii.affine, nii.header) + fixed_img_padded_nii.set_data_dtype(data_type) + + # Set the orientation of the image (use if not already set correctly in the header; check with ``io_reorient_nii``) + if args.ort_code: + fixed_img_padded_nii = reorient_nii(fixed_img_padded_nii, args.ort_code, zero_origin=True, apply=False, form_code=1) + else: + ref_nii = nib.load(args.ref_nii) + ort_code = nii_axis_codes(ref_nii) + fixed_img_padded_nii = reorient_nii(fixed_img_padded_nii, ort_code, zero_origin=True, apply=False, form_code=1) + + if args.output is None: + padded_img_path = args.input.replace('.nii.gz', '_pad.nii.gz') + else: + padded_img_path = args.output + nib.save(fixed_img_padded_nii, padded_img_path)
+ + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/rb.html b/unravel/docs/_build/html/_modules/unravel/image_tools/rb.html new file mode 100644 index 00000000..bfed397a --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/rb.html @@ -0,0 +1,514 @@ + + + + + + + + + + unravel.image_tools.rb — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.rb

+#!/usr/bin/env python3
+
+"""
+Use ``img_rb`` from UNRAVEL to perform rolling ball background subtraction on a TIFF file.
+
+Usage:
+------
+    img_rb -i input.tif -rb 4 
+
+Rolling ball subtraction:
+    - Radius should be ~ 1.0 to 2.0 times the size of the features of interest
+    - Larger radii will remove more background, but may also remove some of the features of interest
+    - Smaller radii will remove less background, but may leave some background noise
+
+To do: 
+    - Add support for other image types and 3D images
+"""
+
+import argparse
+import cv2
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='Path to the input TIFF file.', required=True, action=SM) + parser.add_argument('-o', '--output', help='Path to save the output TIFF file.', default=None, action=SM) + parser.add_argument('-rb', '--rb_radius', help='Radius of rolling ball in pixels.', default=None, type=int, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Add support for other image types and 3D images. + +
+[docs] +def load_tif(tif_path): + '''Load a single tif file using OpenCV and return ndarray.''' + img = cv2.imread(tif_path, cv2.IMREAD_UNCHANGED) + if img is None: + raise FileNotFoundError(f'Could not load the TIFF file from {tif_path}') + return img
+ + +
+[docs] +def rolling_ball_subtraction(img, radius): + '''Subtract background from image using a rolling ball algorithm.''' + kernel_size = 2 * radius + 1 + struct_element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) # 2D disk + background = cv2.morphologyEx(img, cv2.MORPH_OPEN, struct_element) + subtracted_img = cv2.subtract(img, background) + return subtracted_img
+ + +
+[docs] +def save_tif(img, output_path): + '''Save an image as a tif file.''' + cv2.imwrite(output_path, img)
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load the image + img = load_tif(args.input) + + # Apply rolling ball subtraction + img = rolling_ball_subtraction(img, args.rb_radius) + print(f'Applied rolling ball subtraction with radius {args.rb_radius}.') + + # Save the processed image + output_path = args.output if args.output is not None else args.input.replace('.tif', f'_rb{args.rb_radius}.tif') + save_tif(img, output_path)
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/spatial_averaging.html b/unravel/docs/_build/html/_modules/unravel/image_tools/spatial_averaging.html new file mode 100644 index 00000000..1e7add08 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/spatial_averaging.html @@ -0,0 +1,595 @@ + + + + + + + + + + unravel.image_tools.spatial_averaging — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.spatial_averaging

+#!/usr/bin/env python3
+
+"""
+Use ``img_spatial_avg`` from UNRAVEL to load an image and apply 3D spatial averaging.
+
+Usage:
+------
+    img_spatial_avg -i <tif_dir> -o spatial_avg.zarr -d 2 -v 
+    
+Input image types:
+    - .czi, .nii.gz, .ome.tif series, .tif series, .h5, .zarr
+
+3D spatial averaging:
+    - Apply a 3D spatial averaging filter to a 3D numpy array.
+    - Default kernel size is 3x3x3, for the current voxel and its 26 neighbors.
+    - The output array is the same size as the input array.
+    - The edges of the output array are padded with zeros.
+    - The output array is the same data type as the input array.
+    - The input array must be 3D.
+    - The xy and z resolutions are required for saving the output as .nii.gz.
+    - The output is saved as .nii.gz, .tif series, or .zarr.
+
+2D spatial averaging:
+    - Apply a 2D spatial averaging filter to each slice of a 3D numpy array.
+    - Default kernel size is 3x3, for the current pixel and its 8 neighbors.
+    - The output array is the same size as the input array.
+    - The edges of the output array are padded with zeros.
+    - The output array is the same data type as the input array.
+    - The input array must be 3D.
+    - The xy and z resolutions are required for saving the output as .nii.gz.
+    - The output is saved as .nii.gz, .tif series, or .zarr.
+
+Outputs: 
+    - .nii.gz, .tif series, or .zarr depending on the output path extension.
+"""
+
+import argparse
+import cv2
+import numpy as np
+from concurrent.futures import ThreadPoolExecutor
+from rich.traceback import install
+from scipy.ndimage import uniform_filter
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_nii, save_as_tifs, save_as_zarr
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/image .czi, path/img.nii.gz, or path/tif_dir', required=True, action=SM) + parser.add_argument('-o', '--output', help='Output path. Default: None', required=True, action=SM) + parser.add_argument('-d', '--dimensions', help='2D or 3D spatial averaging. (2 or 3)', required=True, type=int, action=SM) + parser.add_argument('-k', '--kernel_size', help='Size of the kernel for spatial averaging. Default: 3', default=3, type=int, action=SM) + parser.add_argument('-c', '--channel', help='.czi channel number. Default: 0 for autofluo', default=0, type=int, action=SM) + parser.add_argument('-x', '--xy_res', help='xy resolution in um', default=None, type=float, action=SM) + parser.add_argument('-z', '--z_res', help='z resolution in um', default=None, type=float, action=SM) + parser.add_argument('-dt', '--dtype', help='Output data type. Default: uint16', default='uint16', action=SM) + parser.add_argument('-r', '--reference', help='Reference image for .nii.gz metadata. Default: None', default=None, action=SM) + parser.add_argument('-ao', '--axis_order', help='Default: xyz. (other option: zyx)', default='xyz', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def spatial_average_3D(arr, kernel_size=3): + """ + Apply a 3D spatial averaging filter to a 3D numpy array. + + Parameters: + - arr (np.ndarray): The input 3D array. + - kernel_size (int): The size of the cubic kernel. Default is 3, for the current voxel and its 26 neighbors. + + Returns: + - np.ndarray: The array after applying the spatial averaging. + """ + if arr.ndim != 3: + raise ValueError("Input array must be 3D.") + + return uniform_filter(arr, size=kernel_size, mode='constant', cval=0.0)
+ + +
+[docs] +def apply_2D_mean_filter(slice, kernel_size=(3, 3)): + """Apply a 2D mean filter to a single slice.""" + kernel = np.ones(kernel_size, np.float32) / (kernel_size[0] * kernel_size[1]) + return cv2.filter2D(slice, -1, kernel)
+ + +
+[docs] +@print_func_name_args_times() +def spatial_average_2D(volume, filter_func, kernel_size=(3, 3), threads=8): + """ + Apply a specified 2D filter function to each slice of a 3D volume in parallel. + + Parameters: + - volume (np.ndarray): The input 3D array. + - filter_func (callable): The filter function to apply to each slice. + - kernel_size (tuple): The dimensions of the kernel to be used in the filter. + - threads (int): The number of parallel threads to use. + + Returns: + - np.ndarray: The volume processed with the filter applied to each slice. + """ + processed_volume = np.empty_like(volume) + num_cores = min(len(volume), threads) # Limit the number of cores to the number of slices or specified threads + + with ThreadPoolExecutor(max_workers=num_cores) as executor: + # Each slice is processed independently and the result is stored in the corresponding index + results = executor.map(filter_func, volume, [kernel_size] * len(volume)) + for i, processed_slice in enumerate(results): + processed_volume[i] = processed_slice + + return processed_volume
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load image and metadata + if args.xy_res is None or args.z_res is None: + img, xy_res, z_res = load_3D_img(args.input, return_res=True) + else: + img = load_3D_img(args.input) + xy_res, z_res = args.xy_res, args.z_res + + # Apply spatial averaging + if args.dimensions == 3: + img = spatial_average_3D(img, kernel_size=args.kernel_size) + elif args.dimensions == 2: + img = spatial_average_2D(img, apply_2D_mean_filter, kernel_size=(args.kernel_size, args.kernel_size)) + else: + raise ValueError("Dimensions must be 2 or 3.") + + # Set the data type for the output + if args.dtype == 'uint8': + img = img.astype(np.uint8) + elif args.dtype == 'uint16': + img = img.astype(np.uint16) + elif args.dtype == 'float32': + img = img.astype(np.float32) + else: + raise ValueError("Data type must be uint8, uint16, or float32.") + + # Save image + if args.output.endswith('.nii.gz'): + save_as_nii(img, args.output, xy_res, z_res, data_type=args.dtype, reference=args.reference) + elif args.output.endswith('.tif'): + save_as_tifs(img, args.output, ndarray_axis_order=args.axis_order) + elif args.output.endswith('.zarr'): + save_as_zarr(img, args.output, ndarray_axis_order=args.axis_order)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/transpose_axes.html b/unravel/docs/_build/html/_modules/unravel/image_tools/transpose_axes.html new file mode 100644 index 00000000..b1ad853b --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/transpose_axes.html @@ -0,0 +1,498 @@ + + + + + + + + + + unravel.image_tools.transpose_axes — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.transpose_axes

+#!/usr/bin/env python3
+
+"""
+Use ``img_transpose`` from UNRAVEL to run ndarray.transpose(axis_1, axis_2, axis_3).
+
+Usage: 
+------
+    img_transpose -i path/img
+"""
+
+import argparse
+from pathlib import Path
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_nii
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/img.czi, path/img.nii.gz, or path/tif_dir', action=SM) + parser.add_argument('-xa', '-x_axis', help='Enter 0, 1, or 2. Default: 0', default=0, type=int, action=SM) + parser.add_argument('-ya', '-y_axis', help='Default: 1', default=1, type=int, action=SM) + parser.add_argument('-za', '-z_axis', help='Default: 2', default=2, type=int, action=SM) + parser.add_argument('-o', '--output', help='path/img.nii.gz', action=SM) + parser.add_argument('-c', '--channel', help='.czi channel number. Default: 0 for autofluo', default=0, type=int, action=SM) + parser.add_argument('-ao', '--axis_order', help='Axis order for loading image. Default: xyz. (other option: zyx)', default='xyz', action=SM) + parser.add_argument('-rr', '--return_res', help='Default: True. If false, enter a float for xy_res and z_res (in um) in prompts', action='store_true', default=True) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +# TODO: Test script. Add support for other output formats. + +print_func_name_args_times() +
+[docs] +def transpose_img(ndarray, axis_1, axis_2, axis_3): + """Transposes axes of ndarray""" + return ndarray.transpose(axis_1, axis_2, axis_3)
+ + +
+[docs] +def main(): + args = parse_args() + + if args.return_res: + img, xy_res, z_res = load_3D_img(args.input, args.channel, desired_axis_order=args.axis_order, return_res=args.return_res) + else: + img = load_3D_img(args.input, args.channel, desired_axis_order=args.axis_order, return_res=args.return_res) + xy_res = float(input("Enter xy_res: ")) + z_res = float(input("Enter z_res: ")) + + tranposed_img = transpose_img(img, args.x_axis, args.y_axis, args.z_axis) + + if args.output: + save_as_nii(tranposed_img, args.output, xy_res, z_res) + else: + output = str(Path(args.input).resolve()).replace(".nii.gz", f"_transposed.nii.gz") + save_as_nii(tranposed_img, output, xy_res, z_res)
+ + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/image_tools/unique_intensities.html b/unravel/docs/_build/html/_modules/unravel/image_tools/unique_intensities.html new file mode 100644 index 00000000..279a5b64 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/image_tools/unique_intensities.html @@ -0,0 +1,507 @@ + + + + + + + + + + unravel.image_tools.unique_intensities — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.image_tools.unique_intensities

+#!/usr/bin/env python3
+
+"""
+Use ``img_unique`` from UNRAVEL to print a list of unique intensities greater than 0.
+
+Usage for printing all non-zero intensities:
+--------------------------------------------
+    img_unique -i path/input_img.nii.gz
+
+Usage for printing the number of voxels for each intensity that is present:
+---------------------------------------------------------------------------
+    img_unique -i path/input_img.nii.gz
+
+Usage for checking which clusters are present if the min cluster size was 100 voxels:
+-------------------------------------------------------------------------------------
+    img_unique -i path/input_img.nii.gz -m 100
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+# from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.img_tools import cluster_IDs
+from unravel.core.utils import print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='path/input_img.nii.gz', required=True, action=SM) + parser.add_argument('-m', '--min_extent', help='Min cluster size in voxels (Default: 1)', default=1, action=SM, type=int) + parser.add_argument('-s', '--print_sizes', help='Print cluster IDs and sizes. Default: False', default=False, action='store_true') + # parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def uniq_intensities(input, min_extent=1, print_sizes=False): + """Loads a 3D image and prints non-zero unique intensity values in a space-separated list. + + Args: + input (_type_): _description_ + min_extent (int, optional): _description_. Defaults to 1. + print_sizes (bool, optional): _description_. Defaults to False. + + Returns: + list of ints: list of unique intensities + """ + if str(input).endswith(".nii.gz"): + nii = nib.load(input) + img = np.asanyarray(nii.dataobj, dtype=np.uint16).squeeze() + else: + img = load_3D_img(input) + + uniq_intensities = cluster_IDs(img, min_extent=min_extent, print_IDs=True, print_sizes=print_sizes) + + return uniq_intensities
+ + +
+[docs] +def main(): + args = parse_args() + + # Print unique intensities in image + uniq_intensities(args.input, args.min_extent, args.print_sizes)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + # Configuration.verbose = args.verbose + # print_cmd_and_times(main)() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/region_stats/rstats.html b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats.html new file mode 100644 index 00000000..e18f0c9a --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats.html @@ -0,0 +1,710 @@ + + + + + + + + + + unravel.region_stats.rstats — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.region_stats.rstats

+#!/usr/bin/env python3
+
+"""
+Use ``rstats`` from UNRAVEL to quantify cell densities for all regions in an atlas.
+
+Usage if the atlas is already in native space from ``warp_to_native``:
+----------------------------------------------------------------------
+    rstats -s rel_path/segmentation_image.nii.gz -a rel_path/native_atlas_split.nii.gz -c Saline --dirs sample14 sample36 
+
+Usage if the native atlas is not available; it is not saved (faster):
+---------------------------------------------------------------------
+    rstats -s rel_path/segmentation_image.nii.gz -m path/atlas_split.nii.gz -c Saline --dirs sample14 sample36
+
+Outputs:
+    - CSV file with cell counts, region volumes, or cell densities for each region
+
+Notes 
+    - Regarding --type, also use 'counts' or 'cell_desities' for object counts or object densities
+
+Prereqs: 
+    - ``reg_prep``, ``reg``, and ``seg_ilastik``
+
+Next steps:
+    - Use ``rstats_summary`` to summarize the results
+"""
+
+import argparse
+import cc3d
+import numpy as np
+import os
+import pandas as pd
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, load_image_metadata_from_txt
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times, initialize_progress_bar, get_samples
+from unravel.warp.to_native import to_native
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-t', '--type', help='Type of measurement (options: counts, volumes, cell_densities [default])', default='cell_densities', action=SM) + parser.add_argument('-c', '--condition', help='One word name for group (prepended to sample ID for rstats_summary)', required=True, action=SM) + parser.add_argument('-s', '--seg_img_path', help='rel_path/segmentation_image.nii.gz (can be glob pattern)', required=True, action=SM) + parser.add_argument('-a', '--atlas_path', help='rel_path/native_atlas_split.nii.gz (only use this option if this file exists; left label IDs increased by 20,000)', default=None, action=SM) + parser.add_argument('-m', '--moving_img', help='path/atlas_image.nii.gz to warp from atlas space', default=None, action=SM) + parser.add_argument('-md', '--metadata', help='path/metadata.txt. Default: ./parameters/metadata.txt', default="./parameters/metadata.txt", action=SM) + parser.add_argument('-cc', '--connect', help='Connected component connectivity (6, 18, or 26). Default: 6', type=int, default=6, action=SM) + parser.add_argument('-ro', '--reg_outputs', help="Name of folder w/ outputs from registration. Default: reg_outputs", default="reg_outputs", action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Fixed input for registration (reg). Default: autofl_50um_masked_fixed_reg_input.nii.gz', default="autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of registration inputs in microns. Default: 50', default='50',type=int, action=SM) + parser.add_argument('-mi', '--miracl', help='Mode for compatibility (accounts for tif to nii reorienting)', action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Add option to get regional label densities + +
+[docs] +def get_atlas_region_at_coords(atlas, x, y, z): + """"Get the ndarray atlas region intensity at the given coordinates""" + return atlas[int(x), int(y), int(z)]
+ + +
+[docs] +@print_func_name_args_times() +def count_cells_in_regions(sample_path, seg_img, atlas_img, connectivity, condition): + """Count the number of cells in each region based on atlas region intensities""" + + # Check that the image and atlas have the same shape + if seg_img.shape != atlas_img.shape: + raise ValueError(f" [red1]Image and atlas have different shapes: {seg_img.shape} != {atlas_img.shape}") + + # If the data is big-endian, convert it to little-endian + if seg_img.dtype.byteorder == '>': + seg_img = seg_img.byteswap().newbyteorder() + seg_img = seg_img.astype(np.uint8) + + labels_out, n = cc3d.connected_components(seg_img, connectivity=connectivity, out_dtype=np.uint32, return_N=True) + + print(f"\n Total cell count: {n}\n") + + # Get cell coordinates from the labeled image + print(" Getting cell coordinates") + stats = cc3d.statistics(labels_out) + + # Convert the dictionary to a dataframe + print(" Converting to dataframe") + centroids = stats['centroids'] + + # Drop the first row, which is the background + centroids = np.delete(centroids, 0, axis=0) + + # Convert the centroids ndarray to a dataframe + centroids_df = pd.DataFrame(centroids, columns=['x', 'y', 'z']) + + # Get the region ID for each cell + centroids_df['Region_ID'] = centroids_df.apply(lambda row: get_atlas_region_at_coords(atlas_img, row['x'], row['y'], row['z']), axis=1) + + # Count how many centroids are in each region + print(" Counting cells in each region") + region_counts_series = centroids_df['Region_ID'].value_counts() + + # Get the sample name from the sample directory + sample_name = sample_path.name + + # Add column header to the region counts + region_counts_series = region_counts_series.rename_axis('Region_ID').reset_index(name=f'{condition}_{sample_name}') + + # Load csv with region IDs, sides, ID_paths, names, and abbreviations + region_info_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'gubra__regionID_side_IDpath_region_abbr.csv') + + # Merge the region counts into the region information dataframe + region_counts_df = region_info_df.merge(region_counts_series, on='Region_ID', how='left') + + # After merging, fill NaN values with 0 for regions without any cells + region_counts_df[f'{condition}_{sample_name}'].fillna(0, inplace=True) + region_counts_df[f'{condition}_{sample_name}'] = region_counts_df[f'{condition}_{sample_name}'].astype(int) + + # Save the region counts as a CSV file + os.makedirs(sample_path / "regional_stats", exist_ok=True) + output_filename = f"{condition}_{sample_name}_regional_cell_counts.csv" if condition else f"{sample_name}_regional_cell_counts.csv" + output_path = sample_path / "regional_stats" / output_filename + region_counts_df.to_csv(output_path, index=False) + + # Sort the dataframe by counts and print the top 10 with count > 0 + region_counts_df.sort_values(by=f'{condition}_{sample_name}', ascending=False, inplace=True) + print(f"\n{region_counts_df[region_counts_df[f'{condition}_{sample_name}'] > 0].head(10)}\n") + + region_ids = region_info_df['Region_ID'] + + print(f" Saving region counts to {output_path}\n") + + return region_counts_df, region_ids, atlas_img
+ + +
+[docs] +def calculate_regional_volumes(sample_path, atlas, region_ids, xy_res, z_res, condition): + """Calculate volumes for given regions in an atlas image.""" + + print("\n Calculating regional volumes\n") + + # Calculate the voxel volume in cubic millimeters + voxel_volume = (xy_res * xy_res * z_res) / 1000**3 + + # Use bincount to get counts for all intensities + voxel_counts = np.bincount(atlas.flatten()) + + # Ensure that region_ids are within the range of voxel_counts length + region_ids = [rid for rid in region_ids if rid < len(voxel_counts)] + + # Map the counts to Region_IDs and calculate volumes + regional_volumes = {region_id: voxel_counts[region_id] * voxel_volume for region_id in region_ids} + + # Merge the regional volumes into the region information dataframe + region_info_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'gubra__regionID_side_IDpath_region_abbr.csv') + sample_name = sample_path.name + region_info_df[f'{condition}_{sample_name}'] = region_info_df['Region_ID'].map(regional_volumes) + regional_volumes_df = region_info_df.fillna(0) + + # Save regional volumes as a CSV file + output_filename = f"{condition}_{sample_name}_regional_volumes.csv" if condition else f"{sample_name}_regional_volumes.csv" + output_path = sample_path / "regional_stats" / output_filename + regional_volumes_df.to_csv(output_path, index=False) + print(f" Saving regional volumes to {output_path}\n") + + return regional_volumes_df
+ + +# Function to calculate regional cell densities +
+[docs] +def calculate_regional_cell_densities(sample_path, regional_counts_df, regional_volumes_df, condition): + """Calculate cell densities for each region in the atlas.""" + + print("\n Calculating regional cell densities\n") + + # Merge the regional counts and volumes into a single dataframe + sample_name = sample_path.name + regional_counts_df[f'{condition}_{sample_name}_density'] = regional_counts_df[f'{condition}_{sample_name}'] / regional_volumes_df[f'{condition}_{sample_name}'] + regional_densities_df = regional_counts_df.fillna(0) + + # Save regional cell densities as a CSV file + output_filename = f"{condition}_{sample_name}_regional_cell_densities.csv" if condition else f"{sample_name}_regional_cell_densities.csv" + output_path = sample_path / "regional_stats" / output_filename + regional_densities_df.sort_values(by='Region_ID', ascending=True, inplace=True) + + # Drop the count column + regional_densities_df.drop(f'{condition}_{sample_name}', axis=1, inplace=True) + + # Rename the density column + regional_densities_df.rename(columns={f'{condition}_{sample_name}_density': f'{condition}_{sample_name}'}, inplace=True) + + regional_densities_df.to_csv(output_path, index=False) + print(f" Saving regional cell densities to {output_path}\n")
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define output + output_dir = sample_path / "regional_stats" + output_dir.mkdir(exist_ok=True, parents=True) + output_filename = f"{args.condition}_{sample_path.name}_regional_{args.type}.csv" if args.condition else f"{sample_path.name}_regional_{args.type}.csv" + output = output_dir / output_filename + if output.exists(): + print(f"\n\n {output.name} already exists for {sample_path.name}. Skipping.\n") + continue + + # Load the segmentation image + if args.type == 'counts' or args.type == 'cell_densities': + seg_img_path = next(sample_path.glob(str(args.seg_img_path)), None) + if seg_img_path is None: + print(f"No files match the pattern {args.seg_img_path} in {sample_path}") + continue + seg_img = load_3D_img(seg_img_path) + + # Load or generate the native atlas image + if args.atlas_path is not None and Path(sample_path, args.atlas_path).exists(): + atlas_path = sample_path / args.atlas_path + atlas_img = load_3D_img(atlas_path) + elif args.moving_img is not None and Path(sample_path, args.moving_img).exists(): + fixed_reg_input = sample_path / args.reg_outputs / args.fixed_reg_in + if not fixed_reg_input.exists(): + fixed_reg_input = sample_path / args.reg_outputs / "autofl_50um_fixed_reg_input.nii.gz" + atlas_img = to_native(sample_path, args.reg_outputs, fixed_reg_input, args.moving_img, args.metadata, args.reg_res, args.miracl, int(0), 'multiLabel', output=None) + else: + print(" [red1]Atlas image not found. Please provide a path to the atlas image or the moving image") + import sys ; sys.exit() + + # Count cells in regions + if args.type == 'counts' or args.type == 'cell_densities': + regional_counts_df, region_ids, atlas = count_cells_in_regions(sample_path, seg_img, atlas_img, args.connect, args.condition) + + # Calculate regional volumes + if args.type == 'volumes' or args.type == 'cell_densities': + # Load resolutions and dimensions of full res image for scaling + metadata_path = sample_path / args.metadata + xy_res, z_res, _, _, _ = load_image_metadata_from_txt(metadata_path) + if xy_res is None: + print(" [red1]./sample??/parameters/metadata.txt is missing. Generate w/ io_metadata") + import sys ; sys.exit() + + # Calculate regional volumes + region_info_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'gubra__regionID_side_IDpath_region_abbr.csv') + region_ids = region_info_df['Region_ID'] + regional_volumes_df = calculate_regional_volumes(sample_path, atlas, region_ids, xy_res, z_res, args.condition) + + # Calculate regional cell densities + if args.type == 'cell_densities': + calculate_regional_cell_densities(sample_path, regional_counts_df, regional_volumes_df, args.condition) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF.html b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF.html new file mode 100644 index 00000000..2f1eefe3 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF.html @@ -0,0 +1,551 @@ + + + + + + + + + + unravel.region_stats.rstats_mean_IF — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.region_stats.rstats_mean_IF

+#!/usr/bin/env python3
+
+"""
+Use ``rstats_mean_IF`` from UNRAVEL to measure mean intensity of immunofluorescence staining in brain regions in atlas space.
+
+Usage:
+------
+    rstats_mean_IF -i '<asterisk>.nii.gz' -a path/atlas
+
+Outputs: 
+    - ./rstats_mean_IF/image_name.csv for each image
+
+Next: 
+    - cd rstats_mean_IF
+    - ``rstats_mean_IF_summary``
+"""
+
+import argparse
+import os
+from pathlib import Path 
+import nibabel as nib
+import numpy as np
+import csv
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.image_tools.unique_intensities import uniq_intensities
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help="Pattern for NIfTI images to process (e.g., '*.nii.gz')", required=True, action=SM) + parser.add_argument('-a', '--atlas', help='Path/atlas.nii.gz', required=True, action=SM) + parser.add_argument('-r', '--regions', nargs='*', type=int, help='Space-separated list of region intensities to process') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def calculate_mean_intensity(atlas, image, regions=None): + """Calculates mean intensity for each region in the atlas.""" + + print("\n Calculating mean immunofluorescence intensity for each region in the atlas...\n") + + # Filter out background + valid_mask = atlas > 0 + valid_atlas = atlas[valid_mask].astype(int) + valid_image = image[valid_mask] + + # Use bincount to get sums for each region + sums = np.bincount(valid_atlas, weights=valid_image) + counts = np.bincount(valid_atlas) + + # Suppress the runtime warning and handle potential division by zero + with np.errstate(divide='ignore', invalid='ignore'): + mean_intensities = sums / counts + + mean_intensities = np.nan_to_num(mean_intensities) + + # Convert to dictionary (ignore background) + mean_intensities_dict = {i: mean_intensities[i] for i in range(1, len(mean_intensities))} + + # Filter the dictionary if regions are provided + if regions: + mean_intensities_dict = {region: mean_intensities_dict[region] for region in regions if region in mean_intensities_dict} + + # Optional: Print results for the filtered regions + for region, mean_intensity in mean_intensities_dict.items(): + print(f" Region: {region}\tMean intensity: {mean_intensity}") + + return mean_intensities_dict
+ + +
+[docs] +def write_to_csv(data, output_file): + """Writes the data to a CSV file.""" + with open(output_file, 'w', newline='') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["Region_Intensity", "Mean_IF_Intensity"]) + for key, value in data.items(): + writer.writerow([key, value])
+ + +
+[docs] +def main(): + args = parse_args() + + + # Either use the provided list of region IDs or create it using unique intensities + if args.regions: + region_intensities = args.regions + else: + print(f'\nProcessing these region IDs from {args.atlas}') + region_intensities = uniq_intensities(args.atlas) + print() + + atlas_nii = nib.load(args.atlas) + atlas = atlas_nii.get_fdata(dtype=np.float32) + + output_folder = Path('rstats_mean_IF') + output_folder.mkdir(parents=True, exist_ok=True) + + files = Path().cwd().glob(args.input) + for file in files: + if str(file).endswith('.nii.gz'): + + nii = nib.load(file) + img = nii.get_fdata(dtype=np.float32) + + # Calculate mean intensity + mean_intensities = calculate_mean_intensity(atlas, img, region_intensities) + + output_filename = str(file.name).replace('.nii.gz', '.csv') + output = output_folder / output_filename + + write_to_csv(mean_intensities, output) + + print('CSVs with regional mean IF intensities output to ./rstats_mean_IF/')
+ + +if __name__ == "__main__": + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.html b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.html new file mode 100644 index 00000000..9e001585 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.html @@ -0,0 +1,618 @@ + + + + + + + + + + unravel.region_stats.rstats_mean_IF_in_segmented_voxels — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.region_stats.rstats_mean_IF_in_segmented_voxels

+#!/usr/bin/env python3
+
+"""
+Use ``rstats_mean_IF_in_seg`` from UNRAVEL to measure mean intensity of immunofluorescence (IF) staining in brain regions in segmented voxels.
+
+Run from experiment folder containing sample?? folders.
+
+Usage
+-----
+    rstats_mean_IF_in_seg -i <asterisk>.czi -s seg_dir/sample??_seg_dir.nii.gz -a path/atlas.nii.gz
+
+Note:
+    This uses full resolution images (i.e., the raw IF image and a segmentation from ``seg_ilastik``)
+
+Default output:
+    - ./sample??/seg_dir/sample??_seg_dir_regional_mean_IF_in_seg.csv
+
+Next steps:
+    ``utils_agg_files`` -i seg_dir/sample??_seg_dir_regional_mean_IF_in_seg.csv
+    ``rstats_mean_IF_summary``
+"""
+
+import argparse
+import csv
+import nibabel as nib
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times, initialize_progress_bar, get_samples
+from unravel.warp.to_native import to_native
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='', formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='path/fluo_image or path/fluo_img_dir relative to sample?? folder', required=True, action=SM) + parser.add_argument('-c', '--chann_idx', help='.czi channel index. Default: 1', default=1, type=int, action=SM) + parser.add_argument('-s', '--seg', help='rel_path/seg_img.nii.gz. 1st glob match processed', required=True, action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz to warp to native space', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/name.csv relative to ./sample??/', default=None, action=SM) + parser.add_argument('--region_ids', help='Optional: Space-separated list of region intensities to process. Default: Process all regions', default=None, nargs='*', type=int) + + # Optional to_native() args + parser.add_argument('-n', '--native_atlas', help='Load/save native atlasfrom/to rel_path/native_image.zarr (fast) or rel_path/native_image.nii.gz if provided', default=None, action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Fixed input for registration (``reg``). Default: autofl_50um_masked_fixed_reg_input.nii.gz', default="autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-inp', '--interpol', help='Interpolator for ants.apply_transforms (nearestNeighbor [default], multiLabel [slow])', default="nearestNeighbor", action=SM) + parser.add_argument('-ro', '--reg_outputs', help="Name of folder w/ outputs from ``reg`` (e.g., transforms). Default: reg_outputs", default="reg_outputs", action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of registration inputs in microns. Default: 50', default='50',type=int, action=SM) + parser.add_argument('-md', '--metadata', help='path/metadata.txt. Default: parameters/metadata.txt', default="parameters/metadata.txt", action=SM) + parser.add_argument('-zo', '--zoom_order', help='SciPy zoom order for scaling to full res. Default: 0 (nearest-neighbor)', default='0',type=int, action=SM) + parser.add_argument('-mi', '--miracl', help='Mode for compatibility (accounts for tif to nii reorienting)', action='store_true', default=False) + + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def calculate_mean_intensity(IF_img, ABA_seg, args): + """Calculates mean intensity for each region in the atlas. + + Parameters: + IF_img (np.ndarray): 3D image of immunofluorescence staining. + ABA_seg (np.ndarray): 3D image of segmented brain regions. + args (argparse.Namespace): Command line arguments. + + Returns: + mean_intensities_dict: {region_id: mean_IF_in_seg} + """ + + print("\n Calculating mean immunofluorescence intensity for each region in the atlas...\n") + + # Ensure both images have the same dimensions + if IF_img.shape != ABA_seg.shape: + raise ValueError("The dimensions of IF_img and ABA_seg do not match.") + + # Flatten the images to 1D arrays for bincount + IF_img_flat = IF_img.flatten() + ABA_seg_flat = ABA_seg.flatten() + + # Use bincount to get fluo intensity sums for each region + sums = np.bincount(ABA_seg_flat, weights=IF_img_flat) # Sum of intensities in each region (excluding background) + counts = np.bincount(ABA_seg_flat) # Number of voxels in each region (excluding background) + + # Suppress the runtime warning and handle potential division by zero + with np.errstate(divide='ignore', invalid='ignore'): + mean_intensities = sums / counts + + mean_intensities = np.nan_to_num(mean_intensities) + + # Convert to dictionary + mean_intensities_dict = {i: mean_intensities[i] for i in range(1, len(mean_intensities))} + + # Filter the dictionary if regions are provided + if args.region_ids: + mean_intensities_dict = {region: mean_intensities_dict[region] for region in args.region_ids if region in mean_intensities_dict} + + # Print results + for region, mean_intensity in mean_intensities_dict.items(): + if mean_intensity > 0 and args.verbose: + print(f" Region: {region}\tMean intensity in segmented voxels: {mean_intensity}") + + return mean_intensities_dict
+ + + +
+[docs] +def write_to_csv(data, output_path): + """Writes the data to a CSV file.""" + with open(output_path, 'w', newline='') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["Region_Intensity", "Mean_IF_Intensity"]) + for key, value in data.items(): + writer.writerow([key, value])
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Load or make the native atlas image + native_atlas_path = next(sample_path.glob(str(args.native_atlas)), None) + if args.native_atlas and native_atlas_path.exists(): + native_atlas = load_3D_img(native_atlas_path) + else: + fixed_reg_input = Path(sample_path, args.reg_outputs, args.fixed_reg_in) + if not fixed_reg_input.exists(): + fixed_reg_input = sample_path / args.reg_outputs / "autofl_50um_fixed_reg_input.nii.gz" + native_atlas = to_native(sample_path, args.reg_outputs, fixed_reg_input, args.atlas, args.metadata, args.reg_res, args.miracl, args.zoom_order, args.interpol, output=native_atlas_path) + + # Load the segmentation image + seg_path = next(sample_path.glob(str(args.seg)), None) + if seg_path is None: + print(f"\n [red bold]No files match the pattern {args.seg} in {sample_path}\n") + continue + seg_nii = nib.load(seg_path) + seg_img = np.asanyarray(seg_nii.dataobj, dtype=np.bool_).squeeze() + + # Multiply the images to convert the seg image to atlas intenties + ABA_seg = native_atlas * seg_img + + # Load the IF image + IF_img_path = next(sample_path.glob(str(args.input)), None) + if IF_img_path is None: + print(f"No files match the pattern {args.input} in {sample_path}") + continue + IF_img = load_3D_img(IF_img_path, args.chann_idx, "xyz") + + # Calculate mean intensity + mean_intensities = calculate_mean_intensity(IF_img, ABA_seg, args) + + # Write to CSV + if args.output: + output_path = sample_path / args.output + output_path.parent.mkdir(parents=True, exist_ok=True) + write_to_csv(mean_intensities, output_path) + else: + output_str = str(seg_path).replace('.nii.gz', '_regional_mean_IF_in_seg.csv') + write_to_csv(mean_intensities, output_str) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF_summary.html b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF_summary.html new file mode 100644 index 00000000..77e4a308 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_mean_IF_summary.html @@ -0,0 +1,774 @@ + + + + + + + + + + unravel.region_stats.rstats_mean_IF_summary — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.region_stats.rstats_mean_IF_summary

+#!/usr/bin/env python3
+
+"""
+Use ``rstats_mean_IF_summary`` from UNRAVEL to output plots of mean IF intensities for each region intensity ID.
+
+Usage for t-tests:
+------------------
+    rstats_mean_IF_summary --order Control Treatment --labels Control Treatment -t ttest
+
+Usage for Tukey's tests w/ reordering and renaming of conditions:
+-----------------------------------------------------------------
+    rstats_mean_IF_summary --order group3 group2 group1 --labels Group_3 Group_2 Group_1
+
+Usage with a custom atlas:
+--------------------------
+    atlas=path/custom_atlas.nii.gz ; rstats_mean_IF_summary --region_ids $(img_unique -i $atlas) --order group2 group1 --labels Group_2 Group_1 -t ttest
+
+Note:
+    - The first word of the csv inputs is used for the the group names (e.g. Control from Control_sample01_cFos_rb4_atlas_space_z.csv)
+
+Inputs: 
+    - <asterisk>.csv in the working dir with these columns: 'Region_Intensity', 'Mean_IF_Intensity'
+
+Prereqs:
+    - Generate CSV inputs withs ``rstats_IF_mean`` or ``rstats_IF_mean_in_seg``
+    - After ``rstats_IF_mean_in_seg``, aggregate CSV inputs with ``utils_agg_files``
+    - If needed, add conditions to input CSV file names: utils_prepend -sk $SAMPLE_KEY -f
+
+The look up table (LUT) csv has these columns: 
+    'Region_ID', 'Side', 'Name', 'Abbr'
+"""
+
+import argparse
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+import os
+import pandas as pd
+import seaborn as sns
+import textwrap
+from rich import print
+from rich.traceback import install
+from pathlib import Path
+from scipy.stats import ttest_ind, dunnett
+from statsmodels.stats.multicomp import pairwise_tukeyhsd
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('--region_ids', nargs='*', type=int, help='List of region intensity IDs (Default: process all regions from the lut CSV)', action=SM) + parser.add_argument('-l', '--lut', help='LUT csv name (in unravel/core/csvs/). Default: gubra__region_ID_side_name_abbr.csv', default="gubra__region_ID_side_name_abbr.csv", action=SM) + parser.add_argument('--order', nargs='*', help='Group Order for plotting (must match 1st word of CSVs)', action=SM) + parser.add_argument('--labels', nargs='*', help='Group Labels in same order', action=SM) + parser.add_argument('-t', '--test', help='Choose between "tukey", "dunnett", and "ttest" post-hoc tests. (Default: tukey)', default='tukey', choices=['tukey', 'dunnett', 'ttest'], action=SM) + parser.add_argument('-alt', "--alternate", help="Number of tails and direction for Dunnett's test {'two-sided', 'less' (means < ctrl), 'greater'}. Default: two-sided", default='two-sided', action=SM) + parser.add_argument('-s', '--show_plot', help='Show plot if flag is provided', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Also output csv to summarise t-test/Tukey/Dunnett results like in ``cluster_stats``. Make symbols transparent. Add option to pass in symbol colors for each group. Add ABA coloring to plots. +# TODO: CSVs are loaded for each region. It would be more efficient to load them once for processing all regions. + +# Set Arial as the font +mpl.rcParams['font.family'] = 'Arial' + +
+[docs] +def load_data(region_id): + data = [] + + # Load all CSVs in the directory + for filename in os.listdir(): + if filename.endswith('.csv'): + group_name = filename.split("_")[0] + df = pd.read_csv(filename) + + # Filter by the region ID + mean_intensity = df[df["Region_Intensity"] == region_id]["Mean_IF_Intensity"].values + + if len(mean_intensity) > 0: + data.append({ + 'group': group_name, + 'mean_intensity': mean_intensity[0] + }) + + if data: + return pd.DataFrame(data) + else: + raise ValueError(f" [red1]No data found for region ID {region_id}")
+ + +
+[docs] +def get_max_region_id_from_csvs(): + """Retrieve the maximum Region_Intensity from all input CSVs.""" + max_region_id = -1 + for filename in os.listdir(): + if filename.endswith('.csv'): + df = pd.read_csv(filename) + max_id_in_file = df["Region_Intensity"].max() + if max_id_in_file > max_region_id: + max_region_id = max_id_in_file + return max_region_id
+ + +
+[docs] +def get_region_details(region_id, csv_path): + region_df = pd.read_csv(csv_path) + region_row = region_df[region_df["Region_ID"] == region_id].iloc[0] + return region_row["Name"], region_row["Abbr"]
+ + +
+[docs] +def get_all_region_ids(csv_path): + """Retrieve all region IDs from the provided CSV.""" + region_df = pd.read_csv(csv_path) + return region_df["Region_ID"].tolist()
+ + +
+[docs] +def filter_region_ids(region_ids, max_region_id): + """Filter region IDs to be within the maximum region ID from the CSVs.""" + return [region_id for region_id in region_ids if region_id <= max_region_id]
+ + +
+[docs] +def remove_zero_intensity_regions(region_ids): + """Remove regions with Mean_IF_Intensity of 0 across all input CSVs.""" + valid_region_ids = [] + for region_id in region_ids: + all_zero = True + for filename in os.listdir(): + if filename.endswith('.csv'): + df = pd.read_csv(filename) + mean_intensity = df[df["Region_Intensity"] == region_id]["Mean_IF_Intensity"].values + if len(mean_intensity) > 0 and mean_intensity[0] != 0: + all_zero = False + break + if not all_zero: + valid_region_ids.append(region_id) + return valid_region_ids
+ + +
+[docs] +def perform_t_tests(df, order): + """Perform t-tests between groups in the DataFrame.""" + comparisons = [] + for i in range(len(order)): + for j in range(i + 1, len(order)): + group1, group2 = order[i], order[j] + data1 = df[df['group'] == group1]['mean_intensity'] + data2 = df[df['group'] == group2]['mean_intensity'] + t_stat, p_value = ttest_ind(data1, data2) + comparisons.append({ + 'group1': group1, + 'group2': group2, + 'p-adj': p_value + }) + return pd.DataFrame(comparisons)
+ + +
+[docs] +def plot_data(region_id, order=None, labels=None, csv_path=None, test_type='tukey', show_plot=False, alt='two-sided'): + df = load_data(region_id) + + if 'group' not in df.columns: + raise KeyError(f" [red1]'group' column not found in the DataFrame for {region_id}. Ensure the CSV files contain the correct data.") + + region_name, region_abbr = get_region_details(region_id, csv_path) + + # Define a list of potential colors + predefined_colors = [ + '#2D67C8', # blue + '#D32525', # red + '#27AF2E', # green + '#FFD700', # gold + '#FF6347', # tomato + '#8A2BE2', # blueviolet + # ... add more colors if needed + ] + + # Check if order is provided and slice the color list accordingly + if order: + selected_colors = predefined_colors[:len(order)] + group_colors = dict(zip(order, selected_colors)) + else: + groups_in_df = df['group'].unique().tolist() + selected_colors = predefined_colors[:len(groups_in_df)] + group_colors = dict(zip(groups_in_df, selected_colors)) + + # If group order and labels are provided, update the DataFrame + if order and labels: + df['group'] = df['group'].astype(pd.CategoricalDtype(categories=order, ordered=True)) + df = df.sort_values('group') + labels_mapping = dict(zip(order, labels)) + df['group_label'] = df['group'].map(labels_mapping) + else: + df['group_label'] = df['group'] + + # Bar plot + plt.figure(figsize=(4, 4)) + ax = sns.barplot(x='group_label', y='mean_intensity', data=df, color='white', errorbar=('se'), capsize=0.1, linewidth=2, edgecolor='black') + + # Formatting + ax.set_ylabel('Mean IF Intensity', weight='bold') + ax.set_xticks(np.arange(len(df['group_label'].unique()))) + ax.set_xticklabels(ax.get_xticklabels(), weight='bold') + ax.tick_params(axis='both', which='major', width=2) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['bottom'].set_linewidth(2) + ax.spines['left'].set_linewidth(2) + + # Swarm plot + sns.swarmplot(x='group_label', y='mean_intensity', hue='group', data=df, palette=group_colors, size=8, linewidth=1, edgecolor='black') + + # Remove the legend created by hue + if ax.legend_: + ax.legend_.remove() + + # Perform the chosen post-hoc test + if test_type == 'tukey': + test_results = pairwise_tukeyhsd(df['mean_intensity'], df['group']).summary() + test_df = pd.DataFrame(test_results.data[1:], columns=test_results.data[0]) + elif test_type == 'dunnett': + # Assuming control is the first group in the order (change as needed) + control_data = df[df['group'] == order[0]]['mean_intensity'].values + experimental_data = [df[df['group'] == group]['mean_intensity'].values for group in order[1:]] + test_stats = dunnett(*experimental_data, control=control_data, alternative=alt) + # Convert the result to a DataFrame similar to the Tukey output for easier handling + test_df = pd.DataFrame({ + 'group1': [order[0]] * len(test_stats.pvalue), + 'group2': order[1:], + 'p-adj': test_stats.pvalue + }) + test_df['reject'] = test_df['p-adj'] < 0.05 + elif test_type == 'ttest': + test_df = perform_t_tests(df, order) + test_df['reject'] = test_df['p-adj'] < 0.05 + + significant_comparisons = test_df[test_df['reject'] == True] + y_max = df['mean_intensity'].max() + y_min = df['mean_intensity'].min() + height_diff = (y_max - y_min) * 0.1 + y_pos = y_max + 0.5 * height_diff + + groups = df['group'].unique() + + for _, row in significant_comparisons.iterrows(): + group1, group2 = row['group1'], row['group2'] + x1 = np.where(groups == group1)[0][0] + x2 = np.where(groups == group2)[0][0] + + plt.plot([x1, x1, x2, x2], [y_pos, y_pos + height_diff, y_pos + height_diff, y_pos], lw=1.5, c='black') + + if row['p-adj'] < 0.0001: + sig = '****' + elif row['p-adj'] < 0.001: + sig = '***' + elif row['p-adj'] < 0.01: + sig = '**' + else: + sig = '*' + + plt.text((x1+x2)*.5, y_pos + 0.8*height_diff, sig, horizontalalignment='center', size='xx-large', color='black', weight='bold') + y_pos += 3 * height_diff + + # Calculate y-axis limits + y_max = df['mean_intensity'].max() + y_min = df['mean_intensity'].min() + height_diff = (y_max - y_min) * 0.1 + y_pos = y_max + 0.5 * height_diff + + # Ensure the y-axis starts from the minimum value, allowing for negative values + plt.ylim(y_min - 2 * height_diff, y_pos + 2 * height_diff) + + # plt.ylim(0, y_pos + 2*height_diff) + ax.set_xlabel(None) + + # Save the plot + output_folder = Path('regional_mean_IF_summary') + output_folder.mkdir(parents=True, exist_ok=True) + + title = f"{region_name} ({region_abbr})" + wrapped_title = textwrap.fill(title, 42) # wraps at x characters. Adjust as needed. + plt.title(wrapped_title) + plt.tight_layout() + region_abbr = region_abbr.replace("/", "-") # Replace problematic characters for file paths + + is_significant = not significant_comparisons.empty + file_prefix = '_' if is_significant else '' + file_name = f"{file_prefix}region_{region_id}_{region_abbr}.pdf" + plt.savefig(output_folder / file_name) + + plt.close() + + if show_plot: + plt.show()
+ + + +
+[docs] +def main(): + args = parse_args() + + if (args.order and not args.labels) or (not args.order and args.labels): + raise ValueError("Both --order and --labels must be provided together.") + + if args.order and args.labels and len(args.order) != len(args.labels): + raise ValueError("The number of entries in --order and --labels must match.") + + # Print CSVs in the working dir + print(f'\n[bold]CSVs in the working dir to process (the first word defines the groups): \n') + for filename in os.listdir(): + if filename.endswith('.csv'): + print(f' {filename}') + print() + + # If region IDs are provided using -r, use them; otherwise, get all region IDs from the CSV + lut = Path(__file__).parent.parent / 'core' / 'csvs' / args.lut + region_ids_to_process = args.region_ids if args.region_ids else get_all_region_ids(lut) + + # Filter region IDs based on max Region_Intensity in input CSVs + max_region_id = get_max_region_id_from_csvs() + region_ids_to_process = filter_region_ids(region_ids_to_process, max_region_id) + + # Remove regions with Mean_IF_Intensity of 0 across all input CSVs + region_ids_to_process = remove_zero_intensity_regions(region_ids_to_process) + + # Process each region ID + for region_id in region_ids_to_process: + plot_data(region_id, args.order, args.labels, csv_path=lut, test_type=args.test, show_plot=args.show_plot, alt=args.alternate)
+ + + +if __name__ == "__main__": + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_summary.html b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_summary.html new file mode 100644 index 00000000..b30bd628 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/region_stats/rstats_summary.html @@ -0,0 +1,886 @@ + + + + + + + + + + unravel.region_stats.rstats_summary — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.region_stats.rstats_summary

+#!/usr/bin/env python3
+
+"""
+Use ``rstats_summary`` from UNRAVEL to plot cell densensities for each region and summarize results.\n CSV columns: Region_ID,Side,Name,Abbr,Saline_sample06,Saline_sample07,...,MDMA_sample01,...,Meth_sample23,...
+
+Usage:
+------
+    rstats_summary --groups Saline MDMA Meth -d 10000 -hemi r
+
+To do: 
+    Add module for Dunnett's tests (don't use this option for now)
+
+Outputs:
+    Plots and a summary CSV to the current directory.    
+
+Example hex code list (flank arg w/ double quotes): ['#2D67C8', '#27AF2E', '#D32525', '#7F25D3']
+"""
+
+import argparse
+import ast
+import os
+from pathlib import Path
+import re
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+import seaborn as sns
+import textwrap
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+from scipy.stats import ttest_ind
+from statsmodels.stats.multicomp import pairwise_tukeyhsd
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.utils import initialize_progress_bar
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('--groups', nargs='*', help='Group prefixes (e.g., saline meth cbsMeth)', action=SM) + parser.add_argument('-t', '--test_type', help="Type of statistical test to use: 'tukey' (default), 'dunnett', or 't-test'", choices=['tukey', 'dunnett', 't-test'], default='tukey', action=SM) + parser.add_argument('-hemi', help="Hemisphere(s) to process (r, l or both)", choices=['r', 'l', 'both'], required=True, action=SM) + parser.add_argument('-c', '--ctrl_group', help="Control group name for t-test or Dunnett's tests", action=SM) + parser.add_argument('-d', '--divide', type=float, help='Divide the cell densities by the specified value for plotting (default is None)', default=None, action=SM) + parser.add_argument('-y', '--ylabel', help='Y-axis label (Default: cell_density)', default='cell_density', action=SM) + parser.add_argument('-b', '--bar_color', help="ABA (default), #hex_code, Seaborn palette, or #hex_code list matching # of groups", default='ABA', action=SM) + parser.add_argument('-s', '--symbol_color', help="ABA, #hex_code, Seaborn palette (Default: light:white), or #hex_code list matching # of groups", default='light:white', action=SM) + parser.add_argument('-o', '--output', help='Output directory for plots (Default: <args.test_type>_plots)', action=SM) + parser.add_argument('-alt', "--alternate", help="Number of tails and direction for t-tests or Dunnett's tests ('two-sided' [default], 'less' [group1 < group2], or 'greater')", default='two-sided', action=SM) + parser.add_argument('-e', "--extension", help="File extension for plots. Choices: pdf (default), svg, eps, tiff, png)", default='pdf', choices=['pdf', 'svg', 'eps', 'tiff', 'png'], action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Dunnett's test. LH/RH averaging via summing counts and volumes before dividing counts by volumes (rather than averaging densities directly). Set up label density quantification. + +
+[docs] +def get_region_details(region_id, df): + # Adjust to account for the unique region IDs. + region_row = df[(df["Region_ID"] == region_id) | (df["Region_ID"] == region_id + 20000)].iloc[0] + return region_row["Region"], region_row["Abbr"]
+ + +
+[docs] +def parse_color_argument(color_arg, num_groups, region_id): + if isinstance(color_arg, str): + if color_arg.startswith('[') and color_arg.endswith(']'): + # It's a string representation of a list, so evaluate it safely + color_list = ast.literal_eval(color_arg) + if len(color_list) != num_groups: + raise ValueError(f"The number of colors provided ({len(color_list)}) does not match the number of groups ({num_groups}).") + return color_list + elif color_arg.startswith('#'): + # It's a single hex color, use it for all groups + return [color_arg] * num_groups + elif color_arg == 'ABA': + # Determine the RGB color for bars based on the region_id + combined_region_id = region_id if region_id < 20000 else region_id - 20000 + results_df = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'regional_summary.csv') #(Region_ID,ID_Path,Region,Abbr,General_Region,R,G,B) + region_rgb = results_df[results_df['Region_ID'] == combined_region_id][['R', 'G', 'B']] + rgb = tuple(region_rgb.iloc[0].values) + rgb_normalized = tuple([x / 255.0 for x in rgb]) + ABA_color = sns.color_palette([rgb_normalized] * num_groups) + return ABA_color + else: + # It's a named seaborn palette + return sns.color_palette(color_arg, num_groups) + else: + # It's already a list (this would be the case for default values or if the input method changes) + return color_arg
+ + +
+[docs] +def summarize_significance(test_df, id): + """Summarize the results of the statistical tests. + + Args: + - test_df (DataFrame): the DataFrame containing the test results (w/ columns: group1, group2, p-value, meandiff) + - id (int): the region or cluster ID + + Returns: + - summary_df (DataFrame): the DataFrame containing the summarized results + """ + summary_rows = [] + for _, row in test_df.iterrows(): + group1, group2 = row['group1'], row['group2'] + # Determine significance level + sig = '' + if row['p-value'] < 0.0001: + sig = '****' + elif row['p-value'] < 0.001: + sig = '***' + elif row['p-value'] < 0.01: + sig = '**' + elif row['p-value'] < 0.05: + sig = '*' + # Determine which group has a higher mean + meandiff = row['meandiff'] + higher_group = group2 if meandiff > 0 else group1 + summary_rows.append({ + 'Region_ID': id, + 'Comparison': f'{group1} vs {group2}', + 'p-value': row['p-value'], + 'Higher_Mean_Group': higher_group, + 'Significance': sig + }) + return pd.DataFrame(summary_rows)
+ + +
+[docs] +def process_and_plot_data(df, region_id, region_name, region_abbr, side, out_dir, group_columns, args): + + # Reshaping the data for plotting + reshaped_data = [] + for prefix in args.groups: + for value in df[group_columns[prefix]].values.ravel(): + reshaped_data.append({'group': prefix, 'density': value}) + reshaped_df = pd.DataFrame(reshaped_data) + + # Plotting + mpl.rcParams['font.family'] = 'Arial' + plt.figure(figsize=(4, 4)) + + groups = reshaped_df['group'].unique() + num_groups = len(groups) + + # Parse the color arguments + bar_color = parse_color_argument(args.bar_color, num_groups, region_id) + symbol_color = parse_color_argument(args.symbol_color, num_groups, region_id) + + # Coloring the bars and symbols + ax = sns.barplot(x='group', y='density', data=reshaped_df, errorbar=('se'), capsize=0.1, palette=bar_color, linewidth=2, edgecolor='black') + sns.stripplot(x='group', y='density', hue='group', data=reshaped_df, palette=symbol_color, alpha=0.5, size=8, linewidth=0.75, edgecolor='black') + + # Calculate y_max and y_min based on the actual plot + y_max = ax.get_ylim()[1] + y_min = ax.get_ylim()[0] + height_diff = (y_max - y_min) * 0.05 # Adjust the height difference as needed + y_pos = y_max * 1.05 # Start just above the highest bar + + # Check which test to perform + if args.test_type == 't-test': + # Perform t-test for each group against the control group + control_data = df[group_columns[args.ctrl_group]].values.ravel() + test_results = [] + for prefix in args.groups: + if prefix != args.ctrl_group: + other_group_data = df[group_columns[prefix]].values.ravel() + t_stat, p_value = ttest_ind(other_group_data, control_data, equal_var=True, alternative=args.alternate) # Switched to equal_var=True and alternative=args.alternate + meandiff = np.mean(other_group_data) - np.mean(control_data) + # if args.alternate == 'less' and meandiff < 0: + # p_value /= 2 # For one-tailed test, halve the p-value if the alternative is 'less' + # t_stat = -t_stat # Flip the sign for 'less' + # elif args.alternate == 'greater' and meandiff > 0: + # p_value /= 2 # For one-tailed test, halve the p-value if the alternative is 'greater' + # elif args.alternate == 'two-sided': + # pass # No change in p value needed for two-sided test + # else: # Effect direction not consistent with hypothesis + # p_value = 1 + test_results.append({ + 'group1': args.ctrl_group, + 'group2': prefix, + 't-stat': t_stat, + 'p-value': p_value, + 'meandiff': np.mean(other_group_data) - np.mean(control_data) + }) + + test_results_df = pd.DataFrame(test_results) + significant_comparisons = test_results_df[test_results_df['p-value'] < 0.05] + + # elif args.test_type == 'dunnett': + + # # Extract the data for the control group and the other groups + # data = [df[group_columns[prefix]].values.ravel() for prefix in args.groups if prefix != args.ctrl_group] + # control_data = df[group_columns[args.ctrl_group]].values.ravel() + + # # The * operator unpacks the list so that each array is a separate argument, as required by dunnett + # dunnett_results = dunnett(*data, control=control_data, alternative=args.alternate) + + # group2_data = [df[group_columns[prefix]].values.ravel() for prefix in args.groups if prefix != args.ctrl_group] + + # # Convert the result to a DataFrame + # test_results_df = pd.DataFrame({ + # 'group1': [args.ctrl_group] * len(dunnett_results.pvalue), + # 'group2': [prefix for prefix in args.groups if prefix != args.ctrl_group], + # 'p-value': dunnett_results.pvalue, + # 'meandiff': np.mean(group2_data, axis=1) - np.mean(control_data) # Calculate the mean difference between each group and the control group + # }) + # significant_comparisons = test_results_df[test_results_df['p-value'] < 0.05] + + elif args.test_type == 'tukey': + + # Conduct Tukey's HSD test + densities = np.array([value for prefix in args.groups for value in df[group_columns[prefix]].values.ravel()]) # Flatten the data + groups = np.array([prefix for prefix in args.groups for _ in range(len(df[group_columns[prefix]].values.ravel()))]) + tukey_results = pairwise_tukeyhsd(densities, groups, alpha=0.05) + + # Extract significant comparisons from Tukey's results + test_results_df = pd.DataFrame(data=tukey_results.summary().data[1:], columns=tukey_results.summary().data[0]) + test_results_df.rename(columns={'p-adj': 'p-value'}, inplace=True) + significant_comparisons = test_results_df[test_results_df['p-value'] < 0.05] + + # Loop for plotting comparison bars and asterisks + for _, row in significant_comparisons.iterrows(): + group1, group2 = row['group1'], row['group2'] + x1 = np.where(groups == group1)[0][0] + x2 = np.where(groups == group2)[0][0] + + # Plotting comparison lines + plt.plot([x1, x1, x2, x2], [y_pos, y_pos + height_diff, y_pos + height_diff, y_pos], lw=1.5, c='black') + + # Plotting asterisks based on p-value + if row['p-value'] < 0.0001: + sig = '****' + elif row['p-value'] < 0.001: + sig = '***' + elif row['p-value'] < 0.01: + sig = '**' + else: + sig = '*' + plt.text((x1 + x2) * .5, y_pos + 1 * height_diff, sig, horizontalalignment='center', size='xx-large', color='black', weight='bold') + + y_pos += 3 * height_diff # Increment y_pos for the next comparison bar + + # Remove the legend only if it exists + if ax.get_legend(): + ax.get_legend().remove() + + # Format the plot + if args.ylabel == 'cell_density': + ax.set_ylabel(r'Cells*10$^{4} $/mm$^{3}$', weight='bold') + else: + ax.set_ylabel(args.ylabel, weight='bold') + ax.set_xticklabels(ax.get_xticklabels(), weight='bold') + ax.tick_params(axis='both', which='major', width=2) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['bottom'].set_linewidth(2) + ax.spines['left'].set_linewidth(2) + plt.ylim(0, y_pos) # Adjust y-axis limit to accommodate comparison bars + ax.set_xlabel('group') ### was None + + # Check if there are any significant comparisons (for prepending '_sig__' to the filename) + has_significant_results = True if significant_comparisons.shape[0] > 0 else False + + # Extract the general region for the filename (output file name prefix for sorting by region) + regional_summary = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'regional_summary.csv') #(Region_ID,ID_Path,Region,Abbr,General_Region,R,G,B) + region_id = region_id if region_id < 20000 else region_id - 20000 # Adjust if left hemi + general_region = regional_summary.loc[regional_summary['Region_ID'] == region_id, 'General_Region'].values[0] + + # Format the filename with '_sig__' prefix if there are significant results + prefix = '_sig__' if has_significant_results else '' + filename = f"{prefix}{general_region}__{region_id}_{region_abbr}_{side}".replace("/", "-") # Replace problematic characters + + # Save the plot for each side or pooled data + title = f"{region_name} ({region_abbr}, {side})" + wrapped_title = textwrap.fill(title, 42) + plt.title(wrapped_title, pad = 20).set_position([.5, 1.05]) + plt.tight_layout() + plt.savefig(f"{out_dir}/{filename}.{args.extension}") + plt.close() + + return test_results_df
+ + + +
+[docs] +def main(): + args = parse_args() + + # Find all CSV files in the current directory matching *cell_densities.csv + file_list = [file for file in os.listdir('.') if file.endswith('cell_densities.csv')] + print(f"\nAggregating data from *cell_densities.csv: {file_list}\n") + + # Check if files are found + if not file_list: + print(" [red1]No files found matching the pattern '*cell_densities.csv'.") + return + + # Aggregate the data for each sample + aggregated_df = pd.read_csv(file_list[0]).iloc[:, 0:5] + for file_name in file_list: + df = pd.read_csv(file_name).iloc[:, -1:] + # Rename the column prefix to match the --groups argument + for prefix in args.groups: + if prefix.lower() in df.columns[0].lower(): + old_prefix = df.columns[0].split("_")[0] + new_column_name = df.columns[0].replace(old_prefix, prefix) + df.rename(columns={df.columns[0]: new_column_name}, inplace=True) + + # Append the aggregated data to the dataframe + aggregated_df = pd.concat([aggregated_df, df], axis=1) + + # Sort all columns that are not part of the first five by group prefix + group_columns = sorted(aggregated_df.columns[5:], key=lambda x: args.groups.index(x.split('_')[0])) + + # Sort each group's columns numerically and combine them + sorted_group_columns = [] + for prefix in args.groups: + prefixed_group_columns = [col for col in group_columns if col.startswith(f"{prefix}_")] + sorted_group_columns += sorted(prefixed_group_columns, key=lambda x: int(re.search(r'\d+', x).group())) + + # Combine the first five columns with the sorted group columns + sorted_columns = aggregated_df.columns[:5].tolist() + sorted_group_columns + + # Now sorted_columns contains all columns, sorted by group and numerically within each group + df = aggregated_df[sorted_columns] + + # Save the aggregated data as a CSV + df.to_csv('regional_cell_densities_all.csv', index=False) + + # Normalization if needed + if args.divide: + df.iloc[:, 5:] = df.iloc[:, 5:].div(args.divide) + + # Prepare output directories + if args.alternate == 'two-sided': + suffix = '' + else: + suffix = f"_{args.alternate}" # Add suffix to indicate the alternative hypothesis + + # Make output directories + if args.output: + if args.hemi == 'both': + out_dirs = {side: f"{args.output}_{side}{suffix}" for side in ["L", "R", "pooled"]} + elif args.hemi == 'r': + out_dirs = {side: f"{args.output}_{side}{suffix}" for side in ["R"]} + elif args.hemi == 'l': + out_dirs = {side: f"{args.output}_{side}{suffix}" for side in ["L"]} + else: + print("--hemi should be l, r, or both") + import sys ; sys.exit() + else: + if args.hemi == 'both': + out_dirs = {side: f"{args.test_type}_plots_{side}{suffix}" for side in ["L", "R", "pooled"]} + elif args.hemi == 'r': + out_dirs = {side: f"{args.test_type}_plots_{side}{suffix}" for side in ["R"]} + elif args.hemi == 'l': + out_dirs = {side: f"{args.test_type}_plots_{side}{suffix}" for side in ["L"]} + else: + print("--hemi should be l, r, or both") + import sys ; sys.exit() + + for out_dir in out_dirs.values(): + os.makedirs(out_dir, exist_ok=True) + + group_columns = {} + for prefix in args.groups: + group_columns[prefix] = [col for col in df.columns if col.startswith(f"{prefix}_")] + + if args.hemi == 'both': + # Averaging data across hemispheres and plotting pooled data (DR) + print(f"\nPlotting and summarizing pooled data for each region...\n") + rh_df = df[df['Region_ID'] < 20000] + lh_df = df[df['Region_ID'] > 20000] + + # Initialize an empty dataframe to store all summaries + all_summaries_pooled = pd.DataFrame() + + # Drop first 4 columns + rh_df = rh_df.iloc[:, 5:] + lh_df = lh_df.iloc[:, 5:] + + # Reset indices to ensure alignment + rh_df.reset_index(drop=True, inplace=True) + lh_df.reset_index(drop=True, inplace=True) + + # Initialize pooled_df with common columns + pooled_df = df[['Region_ID', 'Side', 'ID_Path', 'Region', 'Abbr']][df['Region_ID'] < 20000].reset_index(drop=True) + pooled_df['Side'] = 'Pooled' # Set the 'Side' to 'Pooled' + + # Average the cell densities for left and right hemispheres + for col in lh_df.columns: + pooled_df[col] = (lh_df[col] + rh_df[col]) / 2 + + # Averaging data across hemispheres and plotting pooled data + unique_region_ids = df[df["Side"] == "R"]["Region_ID"].unique() + progress, task_id = initialize_progress_bar(len(unique_region_ids), "[red]Processing regions (pooled)...") + with Live(progress): + for region_id in unique_region_ids: + region_name, region_abbr = get_region_details(region_id, df) + out_dir = out_dirs["pooled"] + comparisons_summary = process_and_plot_data(pooled_df[pooled_df["Region_ID"] == region_id], region_id, region_name, region_abbr, "Pooled", out_dir, group_columns, args) + summary_df = summarize_significance(comparisons_summary, region_id) + all_summaries_pooled = pd.concat([all_summaries_pooled, summary_df], ignore_index=True) + progress.update(task_id, advance=1) + + # Merge with the original regional_summary.csv and write to a new CSV + regional_summary = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'regional_summary.csv') + final_summary_pooled = pd.merge(regional_summary, all_summaries_pooled, on='Region_ID', how='left') + final_summary_pooled.to_csv(Path(out_dir) / '__significance_summary_pooled.csv', index=False) + + # Perform analysis and plotting for each hemisphere + if args.hemi == 'r': + sides_to_process = ["R"] + elif args.hemi == 'l': + sides_to_process = ["L"] + else: + sides_to_process = ["L", "R"] + + for side in sides_to_process: + print(f"\nPlotting and summarizing data for {side} hemisphere...\n") + + # Initialize an empty dataframe to store all summaries + all_summaries = pd.DataFrame() + side_df = df[df['Side'] == side] + unique_region_ids = side_df["Region_ID"].unique() # Get unique region IDs for the current side + progress, task_id = initialize_progress_bar(len(unique_region_ids), f"[red]Processing regions ({side})...") + with Live(progress): + for region_id in unique_region_ids: + region_name, region_abbr = get_region_details(region_id, side_df) + out_dir = out_dirs[side] + comparisons_summary = process_and_plot_data(side_df[side_df["Region_ID"] == region_id], region_id, region_name, region_abbr, side, out_dir, group_columns, args) + summary_df = summarize_significance(comparisons_summary, region_id) + all_summaries = pd.concat([all_summaries, summary_df], ignore_index=True) + progress.update(task_id, advance=1) + + # Merge with the original regional_summary.csv and write to a new CSV + regional_summary = pd.read_csv(Path(__file__).parent.parent / 'core' / 'csvs' / 'regional_summary.csv') + + # Adjust Region_ID for left hemisphere + if side == "L": + all_summaries["Region_ID"] = all_summaries["Region_ID"] - 20000 + + final_summary = pd.merge(regional_summary, all_summaries, on='Region_ID', how='left') + final_summary.to_csv(Path(out_dir) / f'__significance_summary_{side}.csv', index=False)
+ + + +if __name__ == '__main__': + install() + main() + +# Effect size calculations described in the supplemental information: https://pubmed.ncbi.nlm.nih.gov/37248402/ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/register/affine_initializer.html b/unravel/docs/_build/html/_modules/unravel/register/affine_initializer.html new file mode 100644 index 00000000..23de57d1 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/register/affine_initializer.html @@ -0,0 +1,531 @@ + + + + + + + + + + unravel.register.affine_initializer — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.register.affine_initializer

+#!/usr/bin/env python3
+
+"""
+Run ``reg_affine_initializer`` from UNRAVEL as a seperate process to kill it after a time out. This also allows for suppressing error messages.
+
+Usage:
+------
+    reg_affine_initializer -f reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz -m /usr/local/unravel/atlases/gubra/gubra_template_25um.nii.gz -o reg_outputs/ANTsPy_init_tform.nii.gz -t 10
+
+Python usage:
+-------------
+>>> import subprocess
+>>> import os
+>>> command = ['python', 'reg_affine_initializer', '-f', 'reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz', '-m', '/usr/local/unravel/atlases/gubra/gubra_template_25um.nii.gz', '-o', 'reg_outputs/ANTsPy_init_tform.nii.gz', '-t', '10' ]
+>>> with open(os.devnull, 'w') as devnull:
+>>>    subprocess.run(command, stderr=devnull)
+"""
+
+import argparse
+import os
+import ants
+from contextlib import redirect_stderr
+from multiprocessing import Process, Queue
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-f', '--fixed_img', help='path/fixed_image.nii.gz (e.g., autofl_50um_masked_fixed_reg_input.nii.gz)', required=True, action=SM) + parser.add_argument('-m', '--moving_img', help='path/moving_image.nii.gz (e.g., template)', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/init_tform_py.nii.gz', required=True, action=SM) + parser.add_argument('-t', '--time_out', help='Duration in seconds to allow this command/module to run. Default: 10', default=10, type=int, action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +def affine_initializer_wrapper(fixed_image_path, moving_image_path, reg_outputs_path, queue): + + # Load the fixed and moving images + fixed_image = ants.image_read(str(fixed_image_path)) + moving_image = ants.image_read(str(moving_image_path)) + + # Suppress stderr + with open(os.devnull, 'w') as f, redirect_stderr(f): + # Perform affine initialization + txfn = ants.affine_initializer( + fixed_image=fixed_image, + moving_image=moving_image, + search_factor=1, # Degree of increments on the sphere to search + radian_fraction=1, # Defines the arc to search over + use_principal_axis=False, # Determines whether to initialize by principal axis + local_search_iterations=500, # Number of iterations for local optimization at each search point + txfn=reg_outputs_path # Path to save the transformation matrix + ) + # Use a queue to pass the result back to the main process + queue.put(txfn)
+ + +
+[docs] +def run_with_timeout(fixed_image, moving_image, reg_outputs_path, timeout): + + # Queue for inter-process communication + queue = Queue() + + # Create and start the process + p = Process(target=affine_initializer_wrapper, args=(fixed_image, moving_image, reg_outputs_path, queue)) + p.start() + + # Wait for the process to complete or timeout + p.join(timeout) + + if p.is_alive(): + # If the process is still alive after the timeout, terminate it + p.terminate() + p.join() + # print(f"Process timed out after {timeout} seconds and was terminated.") + return None + else: + # If the process completed within the timeout, get the result + return queue.get()
+ + + +
+[docs] +def main(): + args = parse_args() + + # Run the affine initializer with a specified timeout (in seconds) + run_with_timeout(args.fixed_img, args.moving_img, args.output, timeout=args.time_out) + if not Path(args.output).exists(): + print("The affine initializer did not complete successfully w/ 10 second timeout. Lengthen the timeout period of ``reg_affine_initializer`` (.e.g, 180 seconds)")
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/register/reg.html b/unravel/docs/_build/html/_modules/unravel/register/reg.html new file mode 100644 index 00000000..2c68da9d --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/register/reg.html @@ -0,0 +1,668 @@ + + + + + + + + + + unravel.register.reg — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.register.reg

+#!/usr/bin/env python3
+
+"""
+Use ``reg`` from UNRAVEL to register an average template brain/atlas to a resampled autofl brain. 
+
+Usage:
+------
+    reg -m <path/template.nii.gz> -bc -pad -sm 0.4 -ort <3 letter orientation code>
+
+ort_code letter options: 
+    - A/P=Anterior/Posterior
+    - L/R=Left/Right
+    - S/I=Superior/Interior
+    - The side of the brain at the positive direction of the x, y, and z axes determines the 3 letters (axis order xyz)
+
+Prereqs: 
+    ``reg_prep``, [``seg_copy_tifs``], & [``seg_brain_mask``]
+
+Next steps: 
+    ``reg_check``
+"""
+
+import argparse
+import os
+import subprocess
+import ants
+import nibabel as nib
+from ants import n4_bias_field_correction, registration
+from pathlib import Path
+import numpy as np
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+from scipy.ndimage import gaussian_filter
+
+from unravel.image_io.reorient_nii import reorient_nii
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.img_io import resolve_path
+from unravel.core.img_tools import pad
+from unravel.core.utils import print_func_name_args_times, print_cmd_and_times, initialize_progress_bar, get_samples
+from unravel.warp.warp import warp
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + + # Required arguments: + parser.add_argument('-m', '--moving_img', help='path/moving_img.nii.gz (e.g., average template optimally matching tissue)', required=True, action=SM) + + # Optional arguments: + parser.add_argument('-f', '--fixed_img', help='reg_inputs/autofl_50um_masked.nii.gz (from ``reg_prep``)', default="reg_inputs/autofl_50um_masked.nii.gz", action=SM) + parser.add_argument('-o', '--output', help='Warped moving image aligned with the fixed image. Default: <moving_img>__warped_moving_img.nii.gz', default=None, action=SM) + parser.add_argument('-mas', '--mask', help="Brain mask for bias correction. Default: reg_inputs/autofl_50um_brain_mask.nii.gz. or pass in None", default="reg_inputs/autofl_50um_brain_mask.nii.gz", action=SM) + parser.add_argument('-ro', '--reg_outputs', help="Name of folder w/ outputs from ``reg`` (e.g., transforms). Default: reg_outputs", default="reg_outputs", action=SM) + parser.add_argument('-tp', '--tform_prefix', help='Prefix of transforms output from ants.registration. Default: ANTsPy_', default="ANTsPy_", action=SM) + parser.add_argument('-bc', '--bias_correct', help='Perform N4 bias field correction. Default: False', action='store_true', default=False) + parser.add_argument('-pad', '--pad_img', help='If True, add 15 percent padding to image. Default: False', action='store_true', default=False) + parser.add_argument('-sm', '--smooth', help='Sigma value for smoothing the fixed image. Default: 0 for no smoothing. Use 0.4 for autofl', default=0, type=float, action=SM) + parser.add_argument('-ort', '--ort_code', help='3 letter orientation code of fixed image if not set in fixed_img (e.g., RAS)', action=SM) + parser.add_argument('-ia', '--init_align', help='Name of initially aligned image (moving reg input). Default: <moving_img>__initial_alignment_to_fixed_img.nii.gz' , default=None, action=SM) + parser.add_argument('-it', '--init_time', help='Time in seconds allowed for ``reg_affine_initializer`` to run. Default: 30' , default='30', type=str, action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz (Default: /usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def bias_correction(image_path, mask_path=None, shrink_factor=2, verbose=False): + """Perform N4 bias field correction on a .nii.gz and return an ndarray + + Args: + image_path (str): Path to input image.nii.gz + mask_path (str): Path to mask image.nii.gz + shrink_factor (int): Shrink factor for bias field correction + verbose (bool): Print output + output_dir (str): Path to save corrected image""" + ants_img = ants.image_read(str(image_path)) + if mask_path: + ants_mask = ants.image_read(str(mask_path)) + ants_img_corrected = n4_bias_field_correction(image=ants_img, mask=ants_mask, shrink_factor=shrink_factor, verbose=verbose) + else: + ants_img_corrected = n4_bias_field_correction(ants_img) + ndarray = ants_img_corrected.numpy() + + return ndarray
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Directory with outputs (e.g., transforms) from registration + reg_outputs_path = resolve_path(sample_path, args.reg_outputs) + reg_outputs_path.mkdir(parents=True, exist_ok=True) + + # Define inputs and outputs for the fixed image + fixed_img_nii_path = resolve_path(sample_path, args.fixed_img) + if not fixed_img_nii_path.exists(): + print(f"\n [red]The fixed image to be padded for registration ({fixed_img_nii_path}) does not exist. Exiting.\n") + import sys ; sys.exit() + fixed_img_for_reg = str(Path(args.fixed_img).name).replace(".nii.gz", "_fixed_reg_input.nii.gz") + fixed_img_for_reg_path = str(Path(reg_outputs_path, fixed_img_for_reg)) + + # Preprocess the fixed image + if not Path(fixed_img_for_reg_path).exists(): + fixed_img_nii = nib.load(fixed_img_nii_path) + + # Optionally perform bias correction on the fixed image (e.g., when it is an autofluorescence image) + if args.bias_correct: + print(f'\n Bias correcting the registration input\n') + if args.mask != "None": + mask_path = resolve_path(sample_path, args.mask) + fixed_img = bias_correction(str(fixed_img_nii_path), mask_path=str(mask_path), shrink_factor=2, verbose=args.verbose) + elif args.mask == "None": + fixed_img = bias_correction(str(fixed_img_nii_path), mask_path=None, shrink_factor=2, verbose=args.verbose) + else: + fixed_img = fixed_img_nii.get_fdata(dtype=np.float32) + + # Optionally pad the fixed image with 15% of voxels on all sides + if args.pad_img: + print(f'\n Adding padding to the registration input\n') + fixed_img = pad(fixed_img, pad_width=0.15) + + # Optionally smooth the fixed image (e.g., when it is an autofluorescence image) + if args.smooth > 0: + print(f'\n Smoothing the registration input\n') + fixed_img = gaussian_filter(fixed_img, sigma=args.smooth) + + # Create NIfTI, set header info, and save the registration input (reference image) + print(f'\n Setting header info for the registration input\n') + fixed_img = fixed_img.astype(np.float32) # Convert the fixed image to FLOAT32 for ANTsPy + reg_inputs_fixed_img_nii = nib.Nifti1Image(fixed_img, fixed_img_nii.affine.copy(), fixed_img_nii.header) + reg_inputs_fixed_img_nii.set_data_dtype(np.float32) + + # Set the orientation of the image (use if not already set correctly in the header; check with ``io_nii``) + if args.ort_code: + reg_inputs_fixed_img_nii = reorient_nii(reg_inputs_fixed_img_nii, args.ort_code, zero_origin=True, apply=False, form_code=1) + + # Save the fixed input for registration + nib.save(reg_inputs_fixed_img_nii, fixed_img_for_reg_path) + + # Generate the initial transform matrix for aligning the moving image to the fixed image + if not Path(reg_outputs_path, f"{args.tform_prefix}init_tform.mat").exists(): + + # Check if required files exist + if not Path(fixed_img_for_reg_path).exists(): + print(f"\n [red]The fixed image for registration ({fixed_img_for_reg_path})does not exist. Exiting.\n") + import sys ; sys.exit() + if not Path(args.moving_img).exists(): + print(f"\n [red]The moving image for registration ({args.moving_img}) does not exist. Exiting.\n") + import sys ; sys.exit() + + print(f'\n\n Generating the initial transform matrix for aligning the moving image (e.g., template) to the fixed image (e.g., tissue) \n') + command = [ + 'reg_affine_initializer', + '-f', fixed_img_for_reg_path, + '-m', args.moving_img, + '-o', str(Path(reg_outputs_path, f"{args.tform_prefix}init_tform.mat")), + '-t', args.init_time # Time in seconds allowed for this step. Increase time out duration if needed. + ] + + # Redirect stderr to os.devnull + with open(os.devnull, 'w') as devnull: + subprocess.run(command, stderr=devnull) + + # Perform initial approximate alignment of the moving image to the fixed image + if args.init_align: + init_align_out = str(Path(reg_outputs_path, args.init_align)) + else: + init_align_out = str(Path(reg_outputs_path, str(Path(args.moving_img).name).replace(".nii.gz", "__initial_alignment_to_fixed_img.nii.gz"))) + if not Path(init_align_out).exists(): + print(f'\n Applying the initial transform matrix to aligning the moving image to the fixed image \n') + fixed_image = ants.image_read(fixed_img_for_reg_path) + moving_image = ants.image_read(args.moving_img) + transformed_image = ants.apply_transforms( + fixed=fixed_image, + moving=moving_image, + transformlist=[str(Path(reg_outputs_path, f"{args.tform_prefix}init_tform.mat"))] + ) + ants.image_write(transformed_image, str(Path(reg_outputs_path, init_align_out))) + + # Define final output and skip processing if it exists + output = str(Path(reg_outputs_path, str(Path(args.moving_img).name).replace(".nii.gz", "__warped_to_fixed_image.nii.gz"))) + if not Path(output).exists(): + + # Perform registration (reg is a dict with multiple outputs) + print(f'\n Running registration \n') + output_prefix = str(Path(reg_outputs_path, args.tform_prefix)) + reg = ants.registration( + fixed=fixed_image, # e.g., fixed autofluo image + moving=transformed_image, # e.g., the initially aligned moving image (e.g., template) + type_of_transform='SyN', # SyN = symmetric normalization + grad_step=0.1, # Gradient step size + syn_metric='CC', # Cross-correlation + syn_sampling=2, # Corresponds to CC radius + reg_iterations=(100, 70, 50, 20), # Convergence criteria + outprefix=output_prefix, + verbose=args.verbose + ) + + # Save the warped moving image output + ants.image_write(reg['warpedmovout'], output) + print(f"\nTransformed moving image saved to: \n{output}") + + # Save the warped fixed image output + # warpedfixout = str(Path(reg_outputs_path, str(Path(args.fixed_img).name).replace(".nii.gz", "__warped_to_moving_image.nii.gz"))) + # ants.image_write(reg['warpedfixout'], warpedfixout) + # print(f"\nTransformed fixed image saved to: \n{warpedfixout}") + + # Warp the atlas image to the tissue image for checking registration + warped_atlas = str(Path(reg_outputs_path, str(Path(args.atlas).name).replace(".nii.gz", "_in_tissue_space.nii.gz"))) + if not Path(warped_atlas).exists(): + print(f'\n Warping the atlas image to the tissue image for checking registration \n') + warp(reg_outputs_path, args.atlas, fixed_img_for_reg_path, warped_atlas, inverse=False, interpol='multiLabel') + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/register/reg_check.html b/unravel/docs/_build/html/_modules/unravel/register/reg_check.html new file mode 100644 index 00000000..71f27eaf --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/register/reg_check.html @@ -0,0 +1,496 @@ + + + + + + + + + + unravel.register.reg_check — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.register.reg_check

+#!/usr/bin/env python3
+
+"""
+Use ``reg_check`` from UNRAVEL to check registration QC, copies autofl_<asterisk>um_masked_fixed_reg_input.nii.gz and atlas_in_tissue_space.nii.gz for each sample to a target dir.
+
+Usage:
+------
+    reg_check -e <list of experiment directories> # copies to the current working directory
+    reg_check -e <list of experiment directories> -td <target_output_dir
+"""
+
+import argparse
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples, copy_files
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-td', '--target_dir', help='path/target_output_dir name for aggregating outputs from all samples (cwd if omitted).', default=None, action=SM) + parser.add_argument('-ro', '--reg_outputs', help="Name of folder w/ outputs from ``reg``. Default: reg_outputs", default="reg_outputs", action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Fixed image from registration ``reg``. Default: autofl_50um_masked_fixed_reg_input.nii.gz', default="autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-wa', '--warped_atlas', help='Warped atlas image from ``reg``. Default: gubra_ano_combined_25um_in_tissue_space.nii.gz', default="gubra_ano_combined_25um_in_tissue_space.nii.gz", action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + # Create the target directory for copying the selected slices + target_dir = Path(args.target_dir) if args.target_dir is not None else Path.cwd() + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define input paths + source_path = sample_path / args.reg_outputs + + # Copy the selected slices to the target directory + copy_files(source_path, target_dir, args.fixed_reg_in, sample_path, args.verbose) + copy_files(source_path, target_dir, args.warped_atlas, sample_path, args.verbose) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/register/reg_check_brain_mask.html b/unravel/docs/_build/html/_modules/unravel/register/reg_check_brain_mask.html new file mode 100644 index 00000000..7974046c --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/register/reg_check_brain_mask.html @@ -0,0 +1,494 @@ + + + + + + + + + + unravel.register.reg_check_brain_mask — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.register.reg_check_brain_mask

+#!/usr/bin/env python3
+
+"""
+Use ``reg_check_brain_mask`` from UNRAVEL for masking QC, copies autofluo_50um.nii.gz and autofluo_50_masked.nii.gz for each sampled to a target dir
+
+Usage:
+------
+    reg_check_brain_mask -e <list of experiment directories> # copies to the current working directory
+    reg_check_brain_mask -e <list of experiment directories> -td <target_output_dir
+"""
+
+import argparse
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples, copy_files
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-td', '--target_dir', help='path/target_output_dir name for aggregating outputs from all samples. If omitted, uses cwd', default=None, action=SM) + parser.add_argument('-i', '--input', help='Output path. Default: reg_inputs/autofl_50um.nii.gz', default="reg_inputs/autofl_50um.nii.gz", action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + # Create the target directory for copying the selected slices + target_dir = Path(args.target_dir) if args.target_dir is not None else Path.cwd() + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define input paths + source_path = sample_path / Path(args.input).parent + + # Copy the selected slices to the target directory + copy_files(source_path, target_dir, Path(args.input).name, sample_path, args.verbose) + copy_files(source_path, target_dir, str(Path(args.input).name).replace('.nii.gz', '_masked.nii.gz'), sample_path, args.verbose) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/register/reg_prep.html b/unravel/docs/_build/html/_modules/unravel/register/reg_prep.html new file mode 100644 index 00000000..d2c73d66 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/register/reg_prep.html @@ -0,0 +1,568 @@ + + + + + + + + + + unravel.register.reg_prep — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.register.reg_prep

+#!/usr/bin/env python3
+
+"""
+Use ``reg_prep`` from UNRAVEL to load a full resolution autofluo image and resamples to a lower resolution for registration.
+
+Usage:
+------
+    reg_prep -i <asterisk>.czi -td <path/brain_mask_tifs> -e <list of paths to exp dirs> -v
+
+Run command from the experiment directory w/ sample?? folder(s), a sample?? folder, or provide -e or -d arguments.
+
+Input examples (path is relative to ./sample??; 1st glob match processed): 
+    <asterisk>.czi, autofluo/<asterisk>.tif series, autofluo, <asterisk>.tif, or <asterisk>.h5 
+
+Outputs: 
+    ./sample??/reg_inputs/autofl_<asterisk>um.nii.gz
+    ./sample??/reg_inputs/autofl_<asterisk>um_tifs/<asterisk>.tif series (used for training ilastik for ``seg_brain_mask``) 
+
+Next command: 
+    ``seg_copy_tifs`` for ``seg_brain_mask`` or ``reg``
+"""
+
+import argparse
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, resolve_path, save_as_tifs, save_as_nii
+from unravel.core.img_tools import resample, reorient_for_raw_to_nii_conv
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples, print_func_name_args_times
+from unravel.segment.copy_tifs import copy_specific_slices
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='Full res image input path relative (rel_path) to ./sample??', required=True, action=SM) + parser.add_argument('-c', '--channel', help='.czi channel number. Default: 0 for autofluo', default=0, type=int, action=SM) + parser.add_argument('-o', '--output', help='Output path. Default: reg_inputs/autofl_50um.nii.gz', default="reg_inputs/autofl_50um.nii.gz", action=SM) + parser.add_argument('-x', '--xy_res', help='x/y voxel size in microns of the input image. Default: get via metadata', default=None, type=float, action=SM) + parser.add_argument('-z', '--z_res', help='z voxel size in microns of the input image. Default: get via metadata', default=None, type=float, action=SM) + parser.add_argument('-r', '--reg_res', help='Resample input to this res in um for reg. Default: 50', default=50, type=int, action=SM) + parser.add_argument('-zo', '--zoom_order', help='Order for resampling (scipy.ndimage.zoom). Default: 1', default=1, type=int, action=SM) + parser.add_argument('-td', '--target_dir', help='path/target_dir name to copy specific slices for seg_brain_mask (see usage)', default=None, action=SM) + parser.add_argument('-s', '--slices', help='List of slice numbers to copy, e.g., 0000 0400 0800', nargs='*', type=str, default=[]) + parser.add_argument('-mi', '--miracl', help="Include reorientation step to mimic MIRACL's tif to .nii.gz conversion", action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def reg_prep(ndarray, xy_res, z_res, reg_res, zoom_order, miracl): + """Prepare the autofluo image for ``reg`` or mimic preprocessing for ``vstats_prep``. + + Args: + - ndarray (np.ndarray): full res 3D autofluo image. + - xy_res (float): x/y resolution in microns of ndarray. + - z_res (float): z resolution in microns of ndarray. + - reg_res (int): Resample input to this resolution in microns for ``reg``. + - zoom_order (int): Order for resampling (scipy.ndimage.zoom). + - miracl (bool): Include reorientation step to mimic MIRACL's tif to .nii.gz conversion. + + Returns: + - img_resampled (np.ndarray): Resampled image.""" + + # Resample autofluo image (for registration) + img_resampled = resample(ndarray, xy_res, z_res, reg_res, zoom_order=zoom_order) + + # Optionally reorient autofluo image (mimics MIRACL's tif to .nii.gz conversion) + if miracl: + img_resampled = reorient_for_raw_to_nii_conv(img_resampled) + + return img_resampled
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.target_dir is not None: + # Create the target directory for copying the selected slices for ``seg_brain_mask`` + target_dir = Path(args.target_dir) + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define output + output = resolve_path(sample_path, args.output, make_parents=True) + if output.exists(): + print(f"\n\n {output.name} already exists. Skipping.\n") + continue + + # Define input image path + img_path = resolve_path(sample_path, args.input) + + # Load full res autofluo image [and xy and z voxel size in microns] + img, xy_res, z_res = load_3D_img(img_path, args.channel, "xyz", return_res=True, xy_res=args.xy_res, z_res=args.z_res) + + # Prepare the autofluo image for registration + img_resampled = reg_prep(img, xy_res, z_res, args.reg_res, args.zoom_order, args.miracl) + + # Save the prepped autofluo image as tif series (for ``seg_brain_mask``) + tif_dir = Path(str(output).replace('.nii.gz', '_tifs')) + tif_dir.mkdir(parents=True, exist_ok=True) + save_as_tifs(img_resampled, tif_dir, "xyz") + + # Save the prepped autofl image (for ``reg`` if skipping ``seg_brain_mask`` and for applying the brain mask) + save_as_nii(img_resampled, output, args.reg_res, args.reg_res, np.uint16) + + if args.target_dir is not None: + # Copy specific slices to the target directory + tif_dir = str(output).replace('.nii.gz', '_tifs') + copy_specific_slices(sample_path, tif_dir, target_dir, args.slices) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/segment/brain_mask.html b/unravel/docs/_build/html/_modules/unravel/segment/brain_mask.html new file mode 100644 index 00000000..b71ab6c4 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/segment/brain_mask.html @@ -0,0 +1,541 @@ + + + + + + + + + + unravel.segment.brain_mask — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.segment.brain_mask

+#!/usr/bin/env python3
+
+"""
+Use ``seg_brain_mask`` from UNRAVEL to run a trained ilastik project (pixel classification) to mask the brain (often better registration).
+
+Usage:
+------
+    seg_brain_mask -ilp <path/brain_mask.ilp> [-i reg_inputs/autofl_50um.nii.gz] [-r 50] [-v] 
+
+Prereqs: 
+    - Train ilastik (tissue = label 1) w/ tifs from reg_inputs/autofl_<asterisk>um_tifs/<asterisk>.tif (from ``reg_prep``)
+    - Save brain_mask.ilp in experiment directory of use -ilp
+
+Inputs: 
+    - reg_inputs/autofl_<asterisk>um.nii.gz
+    - brain_mask.ilp # in exp dir
+
+Outputs: 
+    - reg_inputs/autofl_<asterisk>um_tifs_ilastik_brain_seg/slice_<asterisk>.tif series
+    - reg_inputs/autofl_<asterisk>um_brain_mask.nii.gz (can be used for ``reg`` and ``vstats_z_score``)
+    - reg_inputs/autofl_<asterisk>um_masked.nii.gz
+
+Next command: 
+    - ``reg``
+"""
+
+import argparse
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.img_io import load_3D_img, resolve_path, save_as_nii
+from unravel.core.img_tools import pixel_classification
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='reg_inputs/autofl_50um.nii.gz (from ``reg_prep``)', default="reg_inputs/autofl_50um.nii.gz", action=SM) + parser.add_argument('-ilp', '--ilastik_prj', help='path/brain_mask.ilp. Default: brain_mask.ilp', default='brain_mask.ilp', action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of autofluo input image in microns. Default: 50', default=50, type=int, action=SM) + parser.add_argument('-l', '--ilastik_log', help='Show Ilastik log', action='store_true') + parser.add_argument('-v', '--verbose', help='Enable verbose mode', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define input and output paths + autofl_img_path = resolve_path(sample_path, path_or_pattern=args.input) + brain_mask_output = Path(str(autofl_img_path).replace('.nii.gz', '_brain_mask.nii.gz')) + autofl_img_masked_output = Path(str(autofl_img_path).replace('.nii.gz', '_masked.nii.gz')) + autofl_tif_directory = str(autofl_img_path).replace('.nii.gz', '_tifs') + seg_dir = f"{autofl_tif_directory}_ilastik_brain_seg" + + # Skip if output exists + if autofl_img_masked_output.exists(): + print(f"\n\n {autofl_img_masked_output} already exists. Skipping.\n") + continue + + # Run ilastik segmentation + if args.ilastik_prj == 'brain_mask.ilp': + ilastik_project = Path(sample_path.parent, args.ilastik_prj).resolve() + else: + ilastik_project = Path(args.ilastik_prj).resolve() + pixel_classification(autofl_tif_directory, ilastik_project, seg_dir, args.ilastik_log) + + # Load brain mask image + seg_img = load_3D_img(seg_dir, "xyz") + + # Convert anything voxels to 0 if > 1 (label 1 = tissue; other labels converted to 0) + brain_mask = np.where(seg_img > 1, 0, seg_img) + + # Save brain mask as nifti + save_as_nii(brain_mask, brain_mask_output, args.reg_res, args.reg_res, np.uint8) + + # Load autofl image + autofl_img = load_3D_img(autofl_img_path) + + # Apply brain mask to autofluo image + autofl_masked = np.where(seg_img == 1, autofl_img, 0) + + # Save masked autofl image + save_as_nii(autofl_masked, autofl_img_masked_output, args.reg_res, args.reg_res, np.uint16) + + # brain_mask(sample, args) + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/segment/copy_tifs.html b/unravel/docs/_build/html/_modules/unravel/segment/copy_tifs.html new file mode 100644 index 00000000..f8c0f2ac --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/segment/copy_tifs.html @@ -0,0 +1,523 @@ + + + + + + + + + + unravel.segment.copy_tifs — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.segment.copy_tifs

+#!/usr/bin/env python3
+
+"""
+Use ``seg_copy_tifs`` from UNRAVEL to copy a subset of .tif files to a target dir for training ilastik.
+
+Usage to prep for ``seg_brain_mask`` (if --mask_dir <path/mask_dir> and -e <exp dir paths> were not specified in ``reg_prep``):
+-------------------------------------------------------------------------------------------------------------------------------
+    seg_copy_tifs -i reg_inputs/autofl_50um_tifs -s 0010 0060 0110 -o ilastik_brain_mask
+
+Usage to prep for ``seg_ilastik`` to segment full resolution immunofluorescence images:
+---------------------------------------------------------------------------------------
+    seg_copy_tifs -i raw_tif_dir -s 0100 0500 1000 -o ilastik_segmentation
+"""
+
+import argparse
+import shutil
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.img_io import resolve_path
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='reg_inputs/autofl_50um_tifs (from ``reg_prep``) or name of directory with raw tifs', default=None, action=SM) + parser.add_argument('-o', '--output', help='path/dir to copy TIF files. (e.g., ilastik_brain_mask or ilastik_segmentation)', required=True, action=SM) + parser.add_argument('-s', '--slices', help='List of slice numbers to copy (4 digits each; space separated)', nargs='*', type=str, default=[]) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def copy_specific_slices(sample_path, source_dir, target_dir, slice_numbers, verbose=False): + """Copy the specified slices to the target directory. + + Args: + - sample_path (Path): Path to the sample directory. + - source_dir (Path): Path to the source directory containing the .tif files. + - target_dir (Path): Path to the target directory where the selected slices will be copied. + - slice_numbers (list): List of slice numbers to copy.""" + + for file_path in source_dir.glob('*.tif'): + if any(file_path.stem.endswith(f"{slice:04}") for slice in map(int, slice_numbers)): + dest_file = target_dir / f'{sample_path.name}_{file_path.name}' + shutil.copy(file_path, dest_file) + if verbose: + print(f"Copied {file_path} to {dest_file}") + else: + if verbose: + print(f"File {file_path.name} does not match specified slices and was not copied.")
+ + + +
+[docs] +def main(): + args = parse_args() + + # Create the target directory for copying the selected slices + target_dir = Path(args.output) + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define input paths + source_path = resolve_path(sample_path, args.input) + + # Copy the selected slices to the target directory + copy_specific_slices(sample_path, source_path, target_dir, args.slices, args.verbose) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/segment/ilastik_pixel_classification.html b/unravel/docs/_build/html/_modules/unravel/segment/ilastik_pixel_classification.html new file mode 100644 index 00000000..9db91612 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/segment/ilastik_pixel_classification.html @@ -0,0 +1,555 @@ + + + + + + + + + + unravel.segment.ilastik_pixel_classification — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.segment.ilastik_pixel_classification

+#!/usr/bin/env python3
+
+"""
+Use ``seg_ilastik`` from UNRAVEL to run a trained ilastik project (pixel classification) to segment features in images.
+
+Usage:
+------
+    seg_ilastik -t cfos -o cfos_seg -ilp path/ilastik_project.ilp
+    seg_ilastik -i <asterisk>.czi -o cfos_seg -ilp path/ilastik_project.ilp
+
+To train an Ilastik project, organize training slices into folder (e.g., 3 slices from 3 samples per condition; ``seg_copy_tifs`` can help).
+
+For info on training, see: https://b-heifets.github.io/UNRAVEL/guide.html#train-an-ilastik-project  
+"""
+
+import argparse
+import os
+import nibabel as nib
+import numpy as np
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_tifs
+from unravel.core.img_tools import pixel_classification
+from unravel.core.utils import get_samples, initialize_progress_bar, print_cmd_and_times, print_func_name_args_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-ilp', '--ilastik_prj', help='path/ilastik_project.ilp', required=True, action=SM) + parser.add_argument('-t', '--tifs_dir', help='path/input_dir_w_tifs', required=True, action=SM) + parser.add_argument('-i', '--input', help='If path/input_dir_w_tifs does not exist, provide a rel_path/image to make it', action=SM) + parser.add_argument('-c', '--channel', help='.czi channel number. Default: 1', default=1, type=int, metavar='') + parser.add_argument('-o', '--output', help='output dir name', default=None, action=SM) + parser.add_argument('-l', '--labels', help='List of segmetation label IDs to save as binary .nii.gz images. Default: 1', default=1, nargs='+', type=int, action=SM) + parser.add_argument('-rmi', '--rm_in_tifs', help='Delete the dir w/ the input tifs (e.g., if a *.czi was the input)', action='store_true', default=False) + parser.add_argument('-rmo', '--rm_out_tifs', help='Delete the dir w/ the output tifs', action='store_true', default=False) + parser.add_argument('-log', '--ilastik_log', help='Show Ilastik log', action='store_true') + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Consolidate pixel_segmentation() in unravel/core/img_tools.py + + +
+[docs] +def count_files(directory): + """Count the number of files in a directory, excluding subdirectories.""" + return sum(1 for entry in os.scandir(directory) if entry.is_file())
+ + +
+[docs] +@print_func_name_args_times() +def save_labels_as_masks(tif_dir, labels, segmentation_dir, output_name): + img = load_3D_img(tif_dir) + for label in labels: + print(f"\n Converting label {label} to mask and saving as .nii.gz in {segmentation_dir}\n") + label_img = (img == label).astype(np.uint8) + nifti_img = nib.Nifti1Image(label_img, np.eye(4)) + nib.save(nifti_img, segmentation_dir.joinpath(f"{output_name}_{label}.nii.gz"))
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define output and skip processing if it already exists + segmentation_dir = sample_path / args.output + output_tif_dir = segmentation_dir / args.output + if not isinstance(args.labels, list): + args.labels = [args.labels] + last_label = args.labels[-1] + final_output = segmentation_dir.joinpath(f"{args.output}_{last_label}.nii.gz") + if final_output.exists(): + print(f"\n\n {final_output.name} already exists. Skipping.\n") + continue + + # Define path to input tifs and create them if they don't exist + input_tif_dir = sample_path / args.tifs_dir + if not input_tif_dir.exists(): + img_path = next(sample_path.glob(str(args.input)), None) + img = load_3D_img(img_path, channel=args.channel) + save_as_tifs(img, input_tif_dir) + + # Perform pixel classification and output segmented tifs to output dir + output_tif_dir.mkdir(exist_ok=True, parents=True) + pixel_classification(str(input_tif_dir), str(args.ilastik_prj), str(output_tif_dir), ilastik_log=args.ilastik_log) + + # Convert each label to a binary mask and save as .nii.gz + save_labels_as_masks(output_tif_dir, args.labels, segmentation_dir, args.output) + + # Remove input tifs if requested + if args.rm_in_tifs: + Path(input_tif_dir).unlink() + + # Remove output tifs if requested + if args.rm_out_tifs: + Path(output_tif_dir).unlink() + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/unravel_commands.html b/unravel/docs/_build/html/_modules/unravel/unravel_commands.html new file mode 100644 index 00000000..dc975db3 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/unravel_commands.html @@ -0,0 +1,896 @@ + + + + + + + + + + unravel.unravel_commands — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.unravel_commands

+#!/usr/bin/env python3
+
+"""
+Use ``unravel_commands`` to print a list of commands available in the unravel package. 
+
+Usage to print common commands and descriptions:
+------------------------------------------------
+    unravel_commands -c -d
+
+Usage to print all commands and module names:
+---------------------------------------------
+    unravel_commands -m
+
+For help on a command, run: 
+    <command> -h
+
+Note: 
+    Commands are roughly organized by the order of the workflow and/or the relatedness of the commands.
+
+GitHub repo: 
+    https://github.com/b-heifets/UNRAVEL/tree/dev
+
+Documentation:
+    https://b-heifets.github.io/UNRAVEL/
+
+If you encounter a situation where a command from the UNRAVEL package has the same name as a command from another package or system command, follow these steps to diagnose and fix the issue:
+
+1. Check the conflicting command:
+    - Use the `which` command to determine the location of the executable being called: which <command>
+    - This will show the path to the executable that is being invoked when you run the command.
+
+2. Diagnose the conflict:
+    - If the path does not point to the UNRAVEL package's command, it means there is a conflict with another package or system command.
+    - For example, if you run `which reg` and it points to `/usr/bin/reg` instead of the expected path in your UNRAVEL package's environment, you have identified the conflict.
+
+3. Resolve the conflict:
+    - Rename the UNRAVEL command: One way to resolve the conflict is to rename the conflicting command in the `pyproject.toml` file of your UNRAVEL package by adding a unique prefix or suffix.
+    - For instance, rename `reg` to `unravel_reg` in the `[project.scripts]` section (i.e., reg = "unravel.register.unravel_reg:main")
+    - After making this change, reinstall the package (cd <path/to/clone/of/repo> ; pip install -e .)
+
+4. Re-run the renamed command:
+    - Use the new command name to avoid the conflict: unravel_reg -h
+
+5. Create aliases (optional):
+    - If you prefer to keep using the original command names, you can create shell aliases in your `.bashrc` or `.zshrc` file to point to the UNRAVEL commands: alias reg="path/to/unravel_env/bin/reg"
+    - Reload your shell configuration: source ~/.bashrc  # or source ~/.zshrc
+
+6. Verify the fix:
+    - Use the `which` command again to verify that the correct command is being invoked: which unravel_reg  # or which reg if using an alias
+"""
+
+import argparse
+from rich import print
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-c', '--common', help='Provide flag to only print common commands', action='store_true', default=False) + parser.add_argument('-m', '--module', help='Provide flag to print the module (script name and location in the unravel package) run by each command', action='store_true', default=False) + parser.add_argument('-d', '--description', help="Provide flag to print the description of the module's purpose", action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + commands = { + "Registration": { + "reg_prep": { + "module": "unravel.register.reg_prep", + "description": "Prepare registration (resample the autofluo image).", + "common": True + }, + "reg": { + "module": "unravel.register.reg", + "description": "Perform registration (register the autofluo image to an average template).", + "common": True + }, + "reg_check": { + "module": "unravel.register.reg_check", + "description": "Check registration (aggregate the autofluo and warped atlas images).", + "common": True + }, + "reg_check_brain_mask": { + "module": "unravel.register.reg_check_brain_mask", + "description": "Check brain mask for over/under segmentation.", + "common": False + } + }, + "Warping": { + "warp_to_atlas": { + "module": "unravel.warp.to_atlas", + "description": "Warp images to atlas space.", + "common": True + }, + "warp_to_native": { + "module": "unravel.warp.to_native", + "description": "Warp images to native space.", + "common": True + }, + "warp": { + "module": "unravel.warp.warp", + "description": "Warp between moving and fixed images.", + "common": False + } + }, + "Segmentation": { + "seg_copy_tifs": { + "module": "unravel.segment.copy_tifs", + "description": "Copy TIF images (copy select tifs to target dir for training ilastik).", + "common": True + }, + "seg_brain_mask": { + "module": "unravel.segment.brain_mask", + "description": "Create brain mask (segment resampled autofluo tifs).", + "common": True + }, + "seg_ilastik": { + "module": "unravel.segment.ilastik_pixel_classification", + "description": "Perform pixel classification w/ Ilastik to segment features of interest.", + "common": True + } + }, + "Voxel-wise stats": { + "vstats_apply_mask": { + "module": "unravel.voxel_stats.apply_mask", + "description": "Apply mask to image (e.g., nullify artifacts or isolate signals).", + "common": False + }, + "vstats_prep": { + "module": "unravel.voxel_stats.vstats_prep", + "description": "Prepare immunofluo images for voxel statistics (e.g., background subtract and warp to atlas space).", + "common": True + }, + "vstats_z_score": { + "module": "unravel.voxel_stats.z_score", + "description": "Z-score images.", + "common": True + }, + "vstats_whole_to_avg": { + "module": "unravel.voxel_stats.whole_to_LR_avg", + "description": "Average left and right hemispheres together", + "common": True + }, + "vstats_hemi_to_avg": { + "module": "unravel.voxel_stats.hemi_to_LR_avg", + "description": "If left and right hemispheres were processed separately (less common), average them together.", + "common": False + }, + "vstats": { + "module": "unravel.voxel_stats.vstats", + "description": "Compute voxel statistics.", + "common": True + }, + "vstats_mirror": { + "module": "unravel.voxel_stats.mirror", + "description": "Flip and optionally shift content of images in atlas space.", + "common": False + } + }, + "Cluster-wise stats": { + "cluster_fdr_range": { + "module": "unravel.cluster_stats.fdr_range", + "description": "Get FDR q value range yielding clusters.", + "common": True + }, + "cluster_fdr": { + "module": "unravel.cluster_stats.fdr", + "description": "FDR-correct 1-p value map --> cluster map.", + "common": True + }, + "cluster_mirror_indices": { + "module": "unravel.cluster_stats.recursively_mirror_rev_cluster_indices", + "description": "Recursively mirror cluster maps for validating clusters in left and right hemispheres.", + "common": True + }, + "cluster_validation": { + "module": "unravel.cluster_stats.cluster_validation", + "description": "Validate clusters w/ cell/label density measurements.", + "common": True + }, + "cluster_summary": { + "module": "unravel.cluster_stats.cluster_summary", + "description": "Summarize info on valid clusters (run after cluster_validation).", + "common": True + }, + "cluster_org_data": { + "module": "unravel.cluster_stats.org_data", + "description": "Organize CSVs from custer_validation.", + "common": False + }, + "cluster_group_data": { + "module": "unravel.cluster_stats.group_bilateral_data", + "description": "Group bilateral cluster data.", + "common": False + }, + "cluster_stats": { + "module": "unravel.cluster_stats.stats", + "description": "Compute cluster vallidation statistics.", + "common": False + }, + "cluster_index": { + "module": "unravel.cluster_stats.index", + "description": "Make a valid cluster map and sunburst plots.", + "common": False + }, + "cluster_brain_model": { + "module": "unravel.cluster_stats.brain_model", + "description": "Make a 3D brain model from a cluster map (for DSI studio)", + "common": False + }, + "cluster_table": { + "module": "unravel.cluster_stats.table", + "description": "Create a table of cluster validation data.", + "common": False + }, + "cluster_prism": { + "module": "unravel.cluster_stats.prism", + "description": "Generate CSVs for bar charts in Prism.", + "common": False + }, + "cluster_legend": { + "module": "unravel.cluster_stats.legend", + "description": "Make a legend of regions in cluster maps.", + "common": False + }, + "cluster_sunburst": { + "module": "unravel.cluster_stats.sunburst", + "description": "Create a sunburst plot of regional volumes.", + "common": False + }, + "cluster_find_incongruent": { + "module": "unravel.cluster_stats.find_incongruent_clusters", + "description": "Find clusters where the effect direction does not match the prediction of cluster_fdr (for validation of non-directional p value maps).", + "common": False + }, + "cluster_crop": { + "module": "unravel.cluster_stats.crop", + "description": "Crop clusters to a bounding box.", + "common": False + }, + "effect_sizes": { + "module": "unravel.cluster_stats.effect_sizes.effect_sizes", + "description": "Calculate effect sizes for clusters.", + "common": False + }, + "effect_sizes_sex_abs": { + "module": "unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute", + "description": "Calculate absolute effect sizes by sex.", + "common": False + }, + "effect_sizes_sex_rel": { + "module": "unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative", + "description": "Calculate relative effect sizes by sex.", + "common": False + } + }, + "Region-wise stats": { + "rstats": { + "module": "unravel.region_stats.rstats", + "description": "Compute regional cell counts, regional volumes, or regional cell densities.", + "common": True + }, + "rstats_summary": { + "module": "unravel.region_stats.rstats_summary", + "description": "Summarize regional cell densities.", + "common": True + }, + "rstats_mean_IF": { + "module": "unravel.region_stats.rstats_mean_IF", + "description": "Compute mean immunofluo intensities for regions.", + "common": False + }, + "rstats_mean_IF_in_seg": { + "module": "unravel.region_stats.rstats_mean_IF_in_segmented_voxels", + "description": "Compute mean immunofluo intensities in segmented voxels.", + "common": False + }, + "rstats_mean_IF_summary": { + "module": "unravel.region_stats.rstats_mean_IF_summary", + "description": "Summarize mean immunofluo intensities for regions.", + "common": False + } + }, + "Image I/O": { + "io_metadata": { + "module": "unravel.image_io.metadata", + "description": "Handle image metadata.", + "common": True + }, + "io_img": { + "module": "unravel.image_io.io_img", + "description": "Image I/O operations.", + "common": False + }, + "io_nii_info": { + "module": "unravel.image_io.nii_info", + "description": "Print info about NIfTI files.", + "common": True + }, + "io_nii_hd": { + "module": "unravel.image_io.nii_hd", + "description": "Print NIfTI headers.", + "common": False + }, + "io_nii": { + "module": "unravel.image_io.io_nii", + "description": "NIfTI I/O operations (binarize, convert data type, scale, etc).", + "common": False + }, + "io_reorient_nii": { + "module": "unravel.image_io.reorient_nii", + "description": "Reorient NIfTI files.", + "common": False + }, + "io_nii_to_tifs": { + "module": "unravel.image_io.nii_to_tifs", + "description": "Convert NIfTI files to TIFFs.", + "common": False + }, + "io_nii_to_zarr": { + "module": "unravel.image_io.nii_to_zarr", + "description": "Convert NIfTI files to Zarr.", + "common": False + }, + "io_zarr_to_nii": { + "module": "unravel.image_io.zarr_to_nii", + "description": "Convert Zarr format to NIfTI.", + "common": False + }, + "io_h5_to_tifs": { + "module": "unravel.image_io.h5_to_tifs", + "description": "Convert H5 files to TIFFs.", + "common": False + }, + "io_tif_to_tifs": { + "module": "unravel.image_io.tif_to_tifs", + "description": "Convert TIF to TIFF series.", + "common": False + }, + "io_img_to_npy": { + "module": "unravel.image_io.img_to_npy", + "description": "Convert images to Numpy arrays.", + "common": False + } + }, + "Image tools": { + "img_avg": { + "module": "unravel.image_tools.avg", + "description": "Average NIfTI images.", + "common": True + }, + "img_unique": { + "module": "unravel.image_tools.unique_intensities", + "description": "Find unique intensities in images.", + "common": True + }, + "img_max": { + "module": "unravel.image_tools.max", + "description": "Print the max intensity value in an image.", + "common": True + }, + "img_bbox": { + "module": "unravel.image_tools.bbox", + "description": "Compute bounding box of non-zero voxels in an image.", + "common": False + }, + "img_spatial_avg": { + "module": "unravel.image_tools.spatial_averaging", + "description": "Perform spatial averaging on images.", + "common": True + }, + "img_rb": { + "module": "unravel.image_tools.rb", + "description": "Apply rolling ball filter to TIF images.", + "common": True + }, + "img_DoG": { + "module": "unravel.image_tools.DoG", + "description": "Apply Difference of Gaussian filter to TIF images.", + "common": False + }, + "img_pad": { + "module": "unravel.image_tools.pad", + "description": "Pad images.", + "common": False + }, + "img_extend": { + "module": "unravel.image_tools.extend", + "description": "Extend images (add padding to one side).", + "common": False + }, + "img_transpose": { + "module": "unravel.image_tools.transpose_axes", + "description": "Transpose image axes.", + "common": False + } + }, + "Atlas tools": { + "atlas_relabel": { + "module": "unravel.image_tools.atlas.relable_nii", + "description": "Relabel atlas IDs.", + "common": False + }, + "atlas_wireframe": { + "module": "unravel.image_tools.atlas.wireframe", + "description": "Make an atlas wireframe.", + "common": False + } + }, + "Utilities": { + "utils_agg_files": { + "module": "unravel.utilities.aggregate_files_from_sample_dirs", + "description": "Aggregate files from sample directories.", + "common": True + }, + "utils_agg_files_rec": { + "module": "unravel.utilities.aggregate_files_recursively", + "description": "Recursively aggregate files.", + "common": False + }, + "utils_prepend": { + "module": "unravel.utilities.prepend_conditions", + "description": "Prepend conditions to files using sample_key.csv.", + "common": True + }, + "utils_rename": { + "module": "unravel.utilities.rename", + "description": "Rename files.", + "common": True + }, + "utils_toggle": { + "module": "unravel.utilities.toggle_samples", + "description": "Toggle sample?? folders for select batch processing.", + "common": False + }, + "utils_clean_tifs": { + "module": "unravel.utilities.clean_tif_dirs", + "description": "Clean TIF directories (no spaces, move non-tifs).", + "common": False + } + } + } + + print("\n[magenta bold]Category[/], [cyan bold]Command[/], [purple3]Module[/], [grey50]Description\n") + + for category, cmds in commands.items(): + if args.common: + cmds = {k: v for k, v in cmds.items() if v.get("common")} + if not cmds: + continue + + print(f"[magenta bold]{category}:") + for cmd, details in cmds.items(): + output = f" [cyan bold]{cmd}[/]" + if args.module: + output += f" [purple3]{details['module']}[/]" + if args.description: + output += f" [grey50]{details['description']}[/]" + print(output)
+ + +if __name__ == "__main__": + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/utilities/aggregate_files_from_sample_dirs.html b/unravel/docs/_build/html/_modules/unravel/utilities/aggregate_files_from_sample_dirs.html new file mode 100644 index 00000000..6f75d305 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/utilities/aggregate_files_from_sample_dirs.html @@ -0,0 +1,521 @@ + + + + + + + + + + unravel.utilities.aggregate_files_from_sample_dirs — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.utilities.aggregate_files_from_sample_dirs

+#!/usr/bin/env python3
+
+
+"""
+Use ``utils_agg_files`` from UNRAVEL to aggregate files from sample?? directories to a target directory.
+
+Usage for when sample?? is already in the name of files being copied:
+---------------------------------------------------------------------
+    utils_agg_files -i atlas_space/sample??_FOS_rb4_gubra_space_z_LRavg.nii.gz -e $DIRS -v
+
+Usage to prepend sample?? to the name of files being copied:
+------------------------------------------------------------
+    utils_agg_files -i atlas_space/FOS_rb4_gubra_space_z_LRavg.nii.gz -e $DIRS -v -a
+"""
+
+import argparse
+import shutil
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-td', '--target_dir', help='path/target_dir name for gathering files. Default: current working dir', default=None, action=SM) + parser.add_argument('-i', '--input', help='relative path to the source file to copy (if sample?? )', required=True, action=SM) + parser.add_argument('-a', '--add_prefix', help='Add "sample??_" prefix to the output files', action='store_true') + parser.add_argument('-v', '--verbose', help='Enable verbose mode', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def aggregate_files_from_sample_dirs(sample_path, pattern, rel_path_to_src_file, target_dir, add_prefix=False, verbose=False): + if f"{pattern}_" in rel_path_to_src_file: + src_path = sample_path / rel_path_to_src_file.replace(f"{pattern}_", f"{sample_path.name}_") + else: + src_path = sample_path / rel_path_to_src_file + + if add_prefix: + target_output = target_dir / f"{sample_path.name}_{src_path.name}" + if verbose and src_path.exists(): + print(f"Copying {src_path.name} as {target_output.name}") + else: + target_output = target_dir / src_path.name + if verbose and src_path.exists(): + print(f"Copying {src_path}") + if src_path.exists(): + shutil.copy(src_path, target_output)
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.target_dir is None: + target_dir = Path().cwd() + else: + target_dir = Path(args.target_dir) + target_dir.mkdir(exist_ok=True, parents=True) + + if args.verbose: + print(f'\nCopying files to: {target_dir}\n') + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + aggregate_files_from_sample_dirs(sample_path, args.pattern, args.input, target_dir, args.add_prefix, args.verbose) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/utilities/aggregate_files_w_recursive_search.html b/unravel/docs/_build/html/_modules/unravel/utilities/aggregate_files_w_recursive_search.html new file mode 100644 index 00000000..103d93a3 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/utilities/aggregate_files_w_recursive_search.html @@ -0,0 +1,478 @@ + + + + + + + + + + unravel.utilities.aggregate_files_w_recursive_search — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.utilities.aggregate_files_w_recursive_search

+#!/usr/bin/env python3
+
+"""
+Use ``utils_agg_files_rec`` from UNRAVEL to recusively copy files matching a glob pattern.
+
+Usage:
+------
+    utils_agg_files_rec -p '<asterisk>.txt' -s /path/to/source -d /path/to/destination
+"""
+
+import argparse
+from pathlib import Path
+import shutil
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(description='', formatter_class=SuppressMetavar) + parser.add_argument("-p", "--pattern", help="The pattern to match files, e.g., '*.txt'", required=True, action=SM) + parser.add_argument("-s", "--source", help="The source directory to search files in. Default: current working dir", default=".", action=SM) + parser.add_argument("-d", "--destination", help="The destination directory to copy files to. Default: current working dir", default=".", action=SM) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def find_and_copy_files(pattern, src_dir, dest_dir): + src_dir = Path(src_dir) + dest_dir = Path(dest_dir) + if not dest_dir.is_absolute(): + dest_dir = src_dir.joinpath(dest_dir) + dest_dir.mkdir(parents=True, exist_ok=True) + + for file_path in src_dir.rglob(pattern): # Use rglob for recursive globbing + if dest_dir not in file_path.parents: + shutil.copy(str(file_path), dest_dir)
+ + # print(f"Copied: {file_path} to {dest_dir}") + +
+[docs] +def main(): + args = parse_args() + + find_and_copy_files(args.pattern, args.source, args.destination)
+ + +if __name__ == "__main__": + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/utilities/clean_tif_dirs.html b/unravel/docs/_build/html/_modules/unravel/utilities/clean_tif_dirs.html new file mode 100644 index 00000000..28130e09 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/utilities/clean_tif_dirs.html @@ -0,0 +1,548 @@ + + + + + + + + + + unravel.utilities.clean_tif_dirs — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.utilities.clean_tif_dirs

+#!/usr/bin/env python3
+
+"""
+Use ``utils_clean_tifs`` from UNRAVEL to clean directories w/ tif series.
+
+Run command from the experiment directory w/ sample?? folder(s), a sample?? folder, or provide -e or -d arguments.
+
+Usage:
+------
+    utils_clean_tifs -t <tif_folder_name> -m -v
+
+Tif directory clean up involves:
+    - Moving subdirectories to parent dir
+    - Moving non-TIF files to parent dir
+    - Replacing spaces in TIF file names
+
+Assumes that the extension of the TIF files is .tif or .ome.tif.
+"""
+
+import argparse
+import shutil
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-t', '--tif_dirs', help='List of tif series dirs to check.', nargs='*', required=True, action=SM) + parser.add_argument('-m', '--move', help='Enable moving of subdirs and non-TIF files to parent dir.', action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def clean_tifs_dir(path_to_tif_dir, move, verbose): + """Clean up a directory containing TIF files: + - Move subdirs to parent dir + - Move non-TIF files to parent dir. + - Replace spaces in TIF file names with underscores. + + Args: + path_to_tif_dir (str): Path to the directory containing the TIF files. + move (bool): Enable moving of subdirs and non-TIF files to parent dir. + verbose (bool): Increase verbosity.""" + if verbose: + print(f"\n\nProcessing directory: {path_to_tif_dir}\n") + + # Move subdirectories to parent directory + for subdir in path_to_tif_dir.iterdir(): + if subdir.is_dir(): + if verbose: + print(f"Found subdir: {subdir}") + if move: + new_dir_name = f"{path_to_tif_dir.name}_{subdir.name}".replace(' ', '_') + target_dir = path_to_tif_dir.parent / new_dir_name + if not target_dir.exists(): + shutil.move(str(subdir), str(target_dir)) + if verbose: + print(f"Moved {subdir} to {target_dir}") + else: + print(f"Skipping {subdir} because {target_dir} already exists/") + + # Move non-TIF files and replace spaces in filenames + for file in path_to_tif_dir.iterdir(): + if file.is_file() and not file.suffix.lower() in ('.tif', '.ome.tif'): + if verbose: + print(f"Found non-TIF file: {file}") + if move: + new_location = path_to_tif_dir.parent / file.name + if not new_location.exists(): + shutil.move(str(file), str(new_location)) + print(f"Moved {file} to {new_location}") + else: + print(f"Skipping {file} because {new_location} already exists.") + elif file.suffix.lower() == '.tif': + new_file_name = file.name.replace(' ', '_') + if new_file_name != file.name: + new_file_path = file.with_name(new_file_name) + file.rename(new_file_path)
+ + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Clean TIF directories + tif_dirs = [sample_path / tif_dir for tif_dir in args.tif_dirs] + for tif_dir in tif_dirs: + clean_tifs_dir(Path(tif_dir), args.move, args.verbose) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/utilities/prepend_conditions.html b/unravel/docs/_build/html/_modules/unravel/utilities/prepend_conditions.html new file mode 100644 index 00000000..d04e0fa4 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/utilities/prepend_conditions.html @@ -0,0 +1,516 @@ + + + + + + + + + + unravel.utilities.prepend_conditions — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.utilities.prepend_conditions

+#!/usr/bin/env python3
+
+"""
+Use ``utils_prepend`` from UNRAVEL to prepend conditions to filenames based on a CSV key.
+
+Usage:
+------
+    utils_prepend -sk <path/sample_key.csv> -f -r
+
+This command renames files in the current directory based on the conditions specified in the CSV file.
+
+The sample_key.csv should have two columns: 'dir_name' and 'condition'.
+The command will prepend the 'condition' to the filenames matching the 'dir_name' prefix.
+
+For example, if the CSV contains the following rows:
+    dir_name,condition
+    sample01,control
+    sample02,treatment
+
+Files will be renamed as follows:
+    'sample01_file.csv' --> 'control_sample01_file.csv'
+    'sample02_file.csv' --> 'treatment_sample02_file.csv'.
+
+If needed, files and/or folders can be renamed with ``utils_rename``.
+""" 
+
+import argparse
+import pandas as pd
+from glob import glob
+from pathlib import Path
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-sk', '--sample_key', help='path/sample_key.csv w/ directory names and conditions', required=True, action=SM) + parser.add_argument('-f', '--file', help='Rename matching files', action='store_true', default=False) + parser.add_argument('-d', '--dirs', help='Rename matching dirs', action='store_true', default=False) + parser.add_argument('-r', '--recursive', help='Recursively rename files/dirs', action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def rename_items(base_path, dir_name, condition, rename_files, rename_dirs, recursive): + search_pattern = f'**/{dir_name}*' if recursive else f'{dir_name}*' + for item in base_path.glob(search_pattern): + if item.is_file() and rename_files: + new_name = item.parent / f'{condition}_{item.name}' + item.rename(new_name) + elif item.is_dir() and rename_dirs: + new_name = item.parent / f'{condition}_{item.name}' + item.rename(new_name)
+ + +
+[docs] +@print_func_name_args_times() +def prepend_conditions(base_path, csv_file, rename_files, rename_dirs, recursive): + mapping_df = pd.read_csv(csv_file) + + for index, row in mapping_df.iterrows(): + dir_name = row['dir_name'] + condition = row['condition'] + rename_items(base_path, dir_name, condition, rename_files, rename_dirs, recursive)
+ + + +
+[docs] +def main(): + args = parse_args() + + base_path = Path.cwd() + prepend_conditions(base_path, args.sample_key, args.file, args.dirs, args.recursive)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/utilities/rename.html b/unravel/docs/_build/html/_modules/unravel/utilities/rename.html new file mode 100644 index 00000000..9871eeb7 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/utilities/rename.html @@ -0,0 +1,506 @@ + + + + + + + + + + unravel.utilities.rename — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.utilities.rename

+#!/usr/bin/env python3
+
+"""
+Use ``utils_rename`` from UNRAVEL to recursively rename files and/or directories by replacing text in filenames.
+
+Usage for renaming files: 
+-------------------------
+    utils_rename -o old_text -n new_text -r -t files
+
+Usage for renaming directories:
+-------------------------------
+    utils_rename -o old_text -n new_text -r -t dirs
+"""
+
+import argparse
+from pathlib import Path
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-o', '--old_text', type=str, help='Text to be replaced in the filenames', action=SM) + parser.add_argument('-n', '--new_text', type=str, help='Text to replace with in the filenames', action=SM) + parser.add_argument('-t', '--type', help='Specify what to rename: "files", "dirs", or "both" (default: both)', choices=['files', 'dirs', 'both'], default='both', action=SM) + parser.add_argument('-r', '--recursive', help='Perform the renaming recursively', action='store_true', default=False) + parser.add_argument('-d', '--dry_run', help='Print old and new names without performing the renaming', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def rename_files(directory, old_text, new_text, recursive=False, rename_type='both', dry_run=False): + """ + Renames files and/or directories in the specified directory, replacing old_text + with new_text in the filenames. Can operate recursively and selectively based + on type. + + Args: + - directory (Path): the directory to search for files and/or directories + - old_text (str): the text to be replaced in the filenames + - new_text (str): the text to replace with in the filenames + - recursive (bool): whether to perform the renaming recursively + - rename_type (str): what to rename: "files", "dirs", or "both" (default: both) + - dry_run (bool): if true, print changes without making them + """ + if recursive: + pattern = '**/*' + else: + pattern = '*' + + for path in Path(directory).glob(pattern): + if (rename_type == 'both' or + (rename_type == 'files' and path.is_file()) or + (rename_type == 'dirs' and path.is_dir())): + if old_text in path.name: + new_name = path.name.replace(old_text, new_text) + new_path = path.parent / new_name + if dry_run: + print(f"Would rename '{path}' to '{new_path}'") + else: + path.rename(new_path) + print(f"Renamed '{path}' to '{new_path}'")
+ + + +
+[docs] +def main(): + args = parse_args() + + rename_files(Path().cwd(), args.old_text, args.new_text, args.recursive, args.type, args.dry_run)
+ + + +if __name__ == '__main__': + install() + main() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/utilities/toggle_samples.html b/unravel/docs/_build/html/_modules/unravel/utilities/toggle_samples.html new file mode 100644 index 00000000..f4ccf0e9 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/utilities/toggle_samples.html @@ -0,0 +1,521 @@ + + + + + + + + + + unravel.utilities.toggle_samples — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.utilities.toggle_samples

+#!/usr/bin/env python3
+
+"""
+Use ``utils_toggle`` from UNRAVEL to inactivate/activate sample?? dirs (i.e., prepend/remove "_" from dir name).
+
+Usage for toggling all sample?? dirs to active:
+-----------------------------------------------
+    utils_toggle -t -e <list_of_exp_dir_paths>
+    
+Usage for activating sample?? dirs for certain conditions:
+----------------------------------------------------------
+    utils_toggle -c <path/sample_key.csv> -a <Saline MDMA> -v -e <list_of_exp_dir_paths>
+
+For conditions in the activate list, the command will remove the "_" from the sample?? dir name.
+For conditions not in the activate list, the command will prepend "_" to the sample?? dir name.    
+
+The sample_key.csv file should have the following format:
+    dir_name,condition
+    sample01,control
+    sample02,treatment
+"""    
+
+import argparse
+import pandas as pd
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-t', '--toggle_all', help='Toggle all sample folders to active, ignoring condition checks.', action='store_true', default=False) + parser.add_argument('-sk', '--sample_key', help='path/sample_key.csv w/ directory names and conditions', default=None, action=SM) + parser.add_argument('-a', '--activate', help='Space separated list of conditions to enable processing for (must match sample_key.csv)', default=None, nargs='*', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity.', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def main(): + args = parse_args() + + active_samples = get_samples(args.dirs, args.pattern, args.exp_paths) + inactive_samples = get_samples(args.dirs, f'_{args.pattern}', args.exp_paths) + samples = active_samples + inactive_samples + + for sample in samples: + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + stripped_sample_name = sample_path.name.lstrip('_') # Strip leading underscore for accurate CSV matching + + # Get the condition for the current sample + if args.sample_key is not None: + mapping_df = pd.read_csv(args.sample_key) + condition_df = mapping_df[mapping_df['dir_name'] == stripped_sample_name]['condition'] + + if args.toggle_all: + new_name = sample_path.parent / stripped_sample_name + print(f'{new_name=}') + status = "Activated" + else: + if args.sample_key is not None: + condition = condition_df.values[0] + if condition in args.activate: + new_name = sample_path.parent / stripped_sample_name + status = "Activated" + else: + new_name = sample_path.parent / f'_{stripped_sample_name}' + status = "Inactivated" + + sample_path.rename(new_name) + + if args.verbose: + if status == "Activated": + print(f" [green]{status}[/] [default bold]{stripped_sample_name}[/] ([default bold]{condition}[/]). New path: {new_name}") + else: + print(f"[red1]{status}[/] [default bold]{stripped_sample_name}[/] ([default bold]{condition}[/]). New path: {new_name}")
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/apply_mask.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/apply_mask.html new file mode 100644 index 00000000..58e45341 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/apply_mask.html @@ -0,0 +1,641 @@ + + + + + + + + + + unravel.voxel_stats.apply_mask — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.apply_mask

+#!/usr/bin/env python3
+
+""" 
+Use ``vstats_apply_mask`` from UNRAVEL to zeros out voxels in image based on a mask and direction args.
+
+Usage to zero out voxels in image where mask > 0 (e.g., to exclude voxels representing artifacts):
+--------------------------------------------------------------------------------------------------
+    vstats_apply_mask -mas 6e10_seg_ilastik_2/sample??_6e10_seg_ilastik_2.nii.gz -i 6e10_rb20 -o 6e10_rb20_wo_artifacts -di greater -v
+
+Usage to zero out voxels in image where mask < 1 (e.g., to preserve signal from segmented microglia clusters):
+--------------------------------------------------------------------------------------------------------------
+    vstats_apply_mask -mas iba1_seg_ilastik_2/sample??_iba1_seg_ilastik_2.nii.gz -i iba1_rb20 -o iba1_rb20_clusters -v 
+
+Usage to replace voxels in image with the mean intensity in the brain where mask > 0:
+-------------------------------------------------------------------------------------
+    vstats_apply_mask -mas FOS_seg_ilastik/FOS_seg_ilastik_2.nii.gz -i FOS -o FOS_wo_halo.zarr -di greater -m -v 
+
+This version allows for dilatation of the full res seg_mask (slow, but precise)
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+from scipy.ndimage import binary_dilation, zoom
+
+from unravel.register.reg_prep import reg_prep
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration 
+from unravel.core.img_io import load_3D_img, load_image_metadata_from_txt, resolve_path, save_as_tifs, save_as_nii, save_as_zarr
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times, initialize_progress_bar, get_samples
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? folders', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern (sample??) for dirs to process. Else: use cwd', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-i', '--input', help='Image input path relative to ./ or ./sample??/', required=True, action=SM) + parser.add_argument('-mas', '--seg_mask', help='rel_path/mask_to_apply.nii.gz (in full res tissue space)', required=True, action=SM) + parser.add_argument("-dil", "--dilation", help="Number of dilation iterations to perform on seg_mask. Default: 0", default=0, type=int, action=SM) + parser.add_argument('-m', '--mean', help='If provided, conditionally replace values w/ the mean intensity in the brain', action='store_true', default=False) + parser.add_argument('-tmas', '--tissue_mask', help='For the mean itensity. rel_path/brain_mask.nii.gz. Default: reg_inputs/autofl_50um_brain_mask.nii.gz', default="reg_inputs/autofl_50um_brain_mask.nii.gz", action=SM) + parser.add_argument('-omas', '--other_mask', help='For restricting application of -mas. E.g., reg_inputs/autofl_50um_brain_mask_outline.nii.gz (from ./UNRAVEL/_other/uncommon_scripts/brain_mask_outline.py)', default=None, action=SM) + parser.add_argument('-di', '--direction', help='"greater" to zero out where mask > 0, "less" (default) to zero out where mask < 1', default='less', choices=['greater', 'less'], action=SM) + parser.add_argument('-o', '--output', help='Image output path relative to ./ or ./sample??/', action=SM) + parser.add_argument('-md', '--metadata', help='path/metadata.txt. Default: ./parameters/metadata.txt', default="./parameters/metadata.txt", action=SM) + parser.add_argument('-r', '--reg_res', help='Resample input to this res in microns for ``reg``. Default: 50', default=50, type=int, action=SM) + parser.add_argument('-mi', '--miracl', help="Include reorientation step to mimic MIRACL's tif to .nii.gz conversion", action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +@print_func_name_args_times() +def load_mask(mask_path): + """Load .nii.gz and return to an ndarray with a binary dtype""" + mask_nii = nib.load(mask_path) + return np.asanyarray(mask_nii.dataobj, dtype=np.bool_).squeeze()
+ + +
+[docs] +@print_func_name_args_times() +def mean_intensity_in_brain(img, tissue_mask): + """Z-score the image using the mask. + + Args: + - img (str): the ndarray to be z-scored. + - mask (str): the brain mask ndarray""" + + # Zero out voxels outside the mask + masked_data = img * tissue_mask + + # Calculate mean for masked data + masked_nonzero = masked_data[masked_data != 0] # Exclude zero voxels and flatten the array (1D) + mean_intensity = masked_nonzero.mean() + + return mean_intensity
+ + +
+[docs] +@print_func_name_args_times() +def dilate_mask(mask, iterations): + """Dilate the given mask (ndarray) by a specified number of iterations.""" + dilated_mask = binary_dilation(mask, iterations=iterations) + return dilated_mask
+ + +
+[docs] +@print_func_name_args_times() +def scale_bool_to_full_res(ndarray, full_res_dims): + """Scale ndarray to match x, y, z dimensions provided. Uses nearest-neighbor interpolation by default to preserve a binary data type.""" + zoom_factors = (full_res_dims[0] / ndarray.shape[0], full_res_dims[1] / ndarray.shape[1], full_res_dims[2] / ndarray.shape[2]) + return zoom(ndarray, zoom_factors, order=0).astype(np.bool_)
+ + +
+[docs] +@print_func_name_args_times() +def apply_mask_to_ndarray(ndarray, mask_ndarray, other_mask=None, mask_condition='less', new_value=0): + """Replace voxels in the ndarray with a new_value based on mask conditions. Optionally use a second mask to restrict application spatially.""" + if mask_ndarray.shape != ndarray.shape: + raise ValueError("Primary mask and input image must have the same shape") + + if other_mask is not None and other_mask.shape != ndarray.shape: + raise ValueError("Other mask and input image must have the same shape") + + # Combine masks if other_mask is provided, using logical AND (both masks need to be True) + if other_mask is not None: + mask_ndarray = np.logical_and(mask_ndarray, other_mask) # Both masks must be True to remain True + + # Apply the combined mask to the ndarray + if mask_condition == 'greater': + ndarray[mask_ndarray] = new_value # mask_ndarray already represents where mask is True + elif mask_condition == 'less': + ndarray[~mask_ndarray] = new_value # Use logical NOT to flip True/False + + return ndarray
+ + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + # Resolve path to sample folder + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + # Define output + output = resolve_path(sample_path, args.output, make_parents=True) + if output.exists(): + print(f"\n\n {output.name} already exists. Skipping.\n") + continue + + # Load image + img = load_3D_img(sample_path / args.input, return_res=False) + + # Load metadata + metadata_path = sample_path / args.metadata + xy_res, z_res, _, _, _ = load_image_metadata_from_txt(metadata_path) + if xy_res is None: + print(" [red1]./sample??/parameters/metadata.txt is missing. Generate w/ io_metadata") + import sys ; sys.exit() + + # Resample to registration resolution to get the mean intensity in the brain + img_resampled = reg_prep(img, xy_res, z_res, args.reg_res, int(1), args.miracl) + + # Load 50 um tissue mask + tissue_mask_img = load_3D_img(sample_path / args.tissue_mask) + + # Calculate mean intensity in brain + if args.mean: + mean_intensity = mean_intensity_in_brain(img_resampled, tissue_mask_img) + + # Check if "sample??_" is in the mask path and replace it with the actual sample name + if f"{args.pattern}_" in args.seg_mask: + dynamic_mask_path = args.seg_mask.replace(f"{args.pattern}_", f"{sample_path.name}_") + else: + dynamic_mask_path = args.seg_mask + + # Load full res mask with the updated or original path + mask = load_mask(sample_path / dynamic_mask_path) + + # Dilate the primary mask + if args.dilation > 0: + mask = dilate_mask(mask, args.dilation) + + # Load the other mask and scale to full resolution + if args.other_mask: + other_mask_img = load_mask(sample_path / args.other_mask) + + metadata_path = sample_path / args.metadata + xy_res, z_res, x_dim, y_dim, z_dim = load_image_metadata_from_txt(metadata_path) + original_dimensions = np.array([x_dim, y_dim, z_dim]) + other_mask_img = scale_bool_to_full_res(other_mask_img, original_dimensions).astype(np.bool_) + + # Apply mask to image + if args.mean: + masked_img = apply_mask_to_ndarray(img, mask, other_mask=other_mask_img, mask_condition=args.direction, new_value=mean_intensity) + else: + masked_img = apply_mask_to_ndarray(img, mask, other_mask=other_mask_img, mask_condition=args.direction, new_value=0) + + # Save masked image + output.parent.mkdir(parents=True, exist_ok=True) + if str(output).endswith(".zarr"): + save_as_zarr(masked_img, output) + elif str(output).endswith('.nii.gz'): + save_as_nii(masked_img, output, xy_res, z_res, img.dtype) + else: + output.mkdir(parents=True, exist_ok=True) + save_as_tifs(masked_img, output, "xyz") + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/hemi_to_LR_avg.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/hemi_to_LR_avg.html new file mode 100644 index 00000000..280a7621 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/hemi_to_LR_avg.html @@ -0,0 +1,542 @@ + + + + + + + + + + unravel.voxel_stats.hemi_to_LR_avg — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.hemi_to_LR_avg

+#!/usr/bin/env python3
+
+"""
+Use ``vstats_hemi_to_avg`` from UNRAVEL to automatically average hemisphere images with their mirrored counterparts. This can also smooth the images with a kernel and apply a mask.
+
+Usage:
+------
+    vstats_hemi_to_avg -k 0.1 -tp -v
+
+Inputs: 
+    - input_img_LH.nii.gz
+    - input_img_RH.nii.gz
+
+Outputs:
+    - input_img_LRavg.nii.gz
+    - input_img_s100_LRavg.nii.gz
+
+"""
+
+
+import argparse
+import numpy as np
+import nibabel as nib
+from glob import glob
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+from concurrent.futures import ThreadPoolExecutor
+
+from fsl.wrappers import fslmaths
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+from unravel.voxel_stats.apply_mask import load_mask
+from unravel.voxel_stats.mirror import mirror
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-k', '--kernel', help='Smoothing kernel radius in mm if > 0. Default: 0 ', default=0, type=float, action=SM) + parser.add_argument('-ax', '--axis', help='Axis to flip the image along. Default: 0', default=0, type=int, action=SM) + parser.add_argument('-s', '--shift', help='Number of voxels to shift content after flipping. Default: 2', default=2, type=int, action=SM) + parser.add_argument('-tp', '--parallel', help='Enable parallel processing with thread pools', default=False, action='store_true') + parser.add_argument('-amas', '--atlas_mask', help='path/atlas_mask.nii.gz', default=None, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def hemi_to_LR_avg(lh_file, rh_file, kernel=0, axis=0, shift=2, atlas_mask=None): + path = lh_file.parent + output_filename = rh_file.name.replace('_RH.nii.gz', f'_s{str(int(kernel * 1000))}_LRavg.nii.gz' if kernel > 0 else '_LRavg.nii.gz') + output_path = path / output_filename + + # Check if the output file already exists + if output_path.exists(): + print(f"Output {output_filename} already exists. Skipping...") + return + + # Load images + right_nii = nib.load(str(rh_file)) + left_nii = nib.load(str(lh_file)) + + # Optionally smooth images + if kernel > 0: + print(f" Smoothing images with a kernel radius of {kernel} mm") + right_nii = fslmaths(right_nii).s(kernel).run() + left_nii = fslmaths(left_nii).s(kernel).run() + + right_img = np.asanyarray(right_nii.dataobj, dtype=right_nii.header.get_data_dtype()).squeeze() + left_img = np.asanyarray(left_nii.dataobj, dtype=left_nii.header.get_data_dtype()).squeeze() + + # Mirror and average images + mirrored_left_img = mirror(left_img, axis=axis, shift=shift) + averaged_img = (right_img + mirrored_left_img) / 2 + + # Apply the mask + if atlas_mask is not None: + mask_img = load_mask(atlas_mask) + averaged_img[~mask_img] = 0 # Use logical NOT to flip True/False + + # Save the averaged image + averaged_nii = nib.Nifti1Image(averaged_img, right_nii.affine, right_nii.header) + nib.save(averaged_nii, output_path) + print(f" Saved averaged image to {output_filename}")
+ + + +
+[docs] +def main(): + args = parse_args() + + path = Path.cwd() + rh_files = list(path.glob('*_RH.nii.gz')) + + if args.parallel: + with ThreadPoolExecutor() as executor: + executor.map(lambda rh_file: hemi_to_LR_avg(path / str(rh_file).replace('_RH.nii.gz', '_LH.nii.gz'), rh_file, args.kernel, args.axis, args.shift, args.atlas_mask), rh_files) + else: + for rh_file in rh_files: + lh_file = path / str(rh_file).replace('_RH.nii.gz', '_LH.nii.gz') + hemi_to_LR_avg(lh_file, rh_file, args.kernel, args.axis, args.shift, args.atlas_mask)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/mirror.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/mirror.html new file mode 100644 index 00000000..4c9813c6 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/mirror.html @@ -0,0 +1,512 @@ + + + + + + + + + + unravel.voxel_stats.mirror — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.mirror

+#!/usr/bin/env python3
+
+
+"""
+Use ``vstats_mirror`` from UNRAVEL to load a <asterisk>.nii.gz, flip a copy [and shift content], average original and copy together, and save as .nii.gz.
+
+Usage:
+------
+    vstats_mirror -v
+
+Note: 
+    The current defaults are specific to our version of the Gubra atlas and may need to be adjusted for other atlases.
+"""
+
+import argparse
+import os
+import numpy as np
+import nibabel as nib
+from pathlib import Path
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-p', '--pattern', help='Pattern to match files. Default: *.nii.gz', default='*.nii.gz', action=SM) + parser.add_argument('-ax', '--axis', help='Axis to flip the image along. Default: 0', default=0, type=int, action=SM) + parser.add_argument('-s', '--shift', help='Number of voxels to shift content after flipping. Default: 2', default=2, type=int, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: adapt to work with CCFv3 images if needed + + +
+[docs] +@print_func_name_args_times() +def mirror(img, axis=0, shift=2): + """Mirror an image along the specified axis and shift the content by the specified number of voxels. + + Args: + img (np.ndarray): Image data to mirror + axis (int): Axis to flip the image along. Default: 0 + shift (int): Number of voxels to shift content after flipping. Default: 2 (useful when the atlas is not centered)""" + # Flip the image data along the specified axis + flipped_img = np.flip(img, axis=axis) + + # Shift the image data by padding with zeros on the left and cropping on the right + # This adds 2 voxels of zeros on the left side (beginning) and removes 2 voxels from the right side (end) + mirrored_img = np.pad(flipped_img, ((shift, 0), (0, 0), (0, 0)), mode='constant', constant_values=0)[:-shift, :, :] + + return mirrored_img
+ + + +
+[docs] +def main(): + args = parse_args() + + files = Path().cwd().glob(args.pattern) + for file in files: + + basename = Path(file).name + mirrored_filename = f"mirror_{basename}" + + if not os.path.exists(mirrored_filename): + nii = nib.load(file) + img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze() + + mirrored_img = mirror(img, axis=args.axis, shift=args.shift) + + mirrored_nii = nib.Nifti1Image(mirrored_img, nii.affine, nii.header) + nib.save(mirrored_nii, mirrored_filename)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/other/IF_outliers.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/other/IF_outliers.html new file mode 100644 index 00000000..6819cc9c --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/other/IF_outliers.html @@ -0,0 +1,526 @@ + + + + + + + + + + unravel.voxel_stats.other.IF_outliers — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.other.IF_outliers

+#!/usr/bin/env python3
+
+"""
+Loads .nii.gz images matching pattern, gets the mean intensity of voxels using the mask, checks for outliers (>3*SD +/- the mean), and plots results
+
+Usage:
+------ 
+    path/IF_outliers.py -p '<asterisk>.nii.gz' -m path/mask.nii.gz -o means_in_mask_plot.pdf -v
+"""
+
+import argparse
+import glob
+import os
+import numpy as np
+import matplotlib.pyplot as plt
+from rich import print
+from rich.live import Live
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-p', '--pattern', help='Regex pattern in quotes for matching .nii.gz images', default=None, action=SM) + parser.add_argument('-m', '--mask', help='path/mask.nii.gz', default=None, action=SM) + parser.add_argument('-o', '--output', help='path/name.[pdf/png]. Default: means_in_mask.pdf ', default='means_in_mask.pdf', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +def mean_intensity_within_mask(image, mask): + return np.mean(image[mask > 0])
+ + +
+[docs] +def detect_outliers(values): + mean_val = np.mean(values) + std_dev = np.std(values) + lower_bound = mean_val - 3 * std_dev + upper_bound = mean_val + 3 * std_dev + + outliers = [(i, v) for i, v in enumerate(values) if v < lower_bound or v > upper_bound] + return outliers
+ + + +
+[docs] +def main(): + args = parse_args() + + mask = load_3D_img(args.mask) + + # Collect .nii.gz files matching the pattern + images = [f for f in glob.glob(args.pattern) if os.path.basename(f) != args.mask] + + mean_values = [] + + # For each image, calculate the mean intensity value within the masked region. + progress = initialize_progress_bar(total_tasks=len(images)) + task_id = progress.add_task("[red]Getting means...", total=len(images)) + with Live(progress): + for idx, img in enumerate(images): + image = load_3D_img(img) + mean_intensity = mean_intensity_within_mask(image, mask) + mean_values.append(mean_intensity) + print(f"{idx} Mean in mask for {img}: {mean_intensity}") + progress.update(task_id, advance=1) + + # Plot mean values + plt.scatter(range(len(mean_values)), mean_values) + plt.xlabel('Image Index') + plt.ylabel('Mean Intensity within mask') + plt.title('Mean Intensities within mask for each image') + plt.savefig(args.output) + + # Detect outliers + outliers = detect_outliers(mean_values) + if outliers: + for idx, value in outliers: + print(f"Potential outlier: {images[idx]} with mean intensity value: {value}") + else: + print("No outliers detected!")
+ + + +if __name__ == "__main__": + from rich.traceback import install + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/other/r_to_p.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/other/r_to_p.html new file mode 100644 index 00000000..c0b348c6 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/other/r_to_p.html @@ -0,0 +1,531 @@ + + + + + + + + + + unravel.voxel_stats.other.r_to_p — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.other.r_to_p

+#!/usr/bin/env python3
+
+"""
+Converts correlation map to z-score, p value, and FDR p value maps.
+
+Usage:
+------
+    path/r_to_p.py -i sample01_cfos_correlation_map.nii.gz -x 25 -z 25 -v
+
+Outputs: 
+    - <image>_z_score_map.nii.gz
+    - <image>_p_value_map.nii.gz
+    - <image>_p_value_map_fdr_corrected.nii.gz
+"""
+
+import argparse
+from pathlib import Path
+import numpy as np
+from rich import print
+from rich.traceback import install
+from scipy.stats import norm
+from statsmodels.stats.multitest import multipletests
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img, save_as_nii
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-i', '--input', help='[path/]image.nii.gz', required=True, action=SM) + parser.add_argument('-x', '--xy_res', help='x/y voxel size in microns. Default: get via metadata', default=None, type=float, action=SM) + parser.add_argument('-z', '--z_res', help='z voxel size in microns. Default: get via metadata', default=None, type=float, action=SM) + parser.add_argument('-a', '--alpha', help='FDR alpha. Default: 0.05', default=0.05, type=float, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def r_to_z(correlation_map): + """Convert a Pearson correlation map (ndarray) to a Z-score map (ndarray) using Fisher's Z-transformation via np.arctanh""" + # Adjust values slightly (e.g., in the seed region) if they are exactly 1 or -1 to avoid divide by zero error + max_value = 1 - 1e-10 # Adjust 1e-10 to a suitable tolerance value + adjusted_correlation_map = np.clip(correlation_map, -max_value, max_value) + return np.arctanh(adjusted_correlation_map) # Fast equivalent to Fisher's Z-transformation: 0.5 * np.log((1 + adjusted_correlation_map) / (1 - adjusted_correlation_map))
+ + +
+[docs] +@print_func_name_args_times() +def z_to_p(z_map): + """Convert a Z-score map (ndarray) to a two-tailed p-value map (ndarray)""" + return norm.sf(abs(z_map)) * 2 #https://www.geeksforgeeks.org/how-to-find-a-p-value-from-a-z-score-in-python/
+ + + +
+[docs] +def main(): + args = parse_args() + + # Load Pearson correlation map + if args.xy_res is None or args.z_res is None: + img, xy_res, z_res = load_3D_img(args.input, return_res=True) + else: + img = load_3D_img(args.input) + xy_res, z_res = args.xy_res, args.z_res + + correlation_map = load_3D_img(args.input) + + # Apply Fisher Z-transformation to convert to z-score map + z_map = r_to_z(correlation_map) + + # Convert z-score map to p-value map + p_map = z_to_p(z_map) + + # Invert P value map for visualization + inv_p_map = 1 - p_map + + # Apply multiple comparisons correction (False Discovery Rate, FDR) + alpha_level = 0.05 # Set your desired alpha level + _, p_map_fdr_corrected, _, _ = multipletests(p_map.flatten(), alpha=alpha_level, method='fdr_bh') + p_map_fdr_corrected = p_map_fdr_corrected.reshape(p_map.shape) + + # Invert FDR corrected P value map for visualization + inv_p_map_fdr_corrected = 1 - p_map_fdr_corrected + + # Save the Z-score map and P-value maps + output_prefix = str(Path(args.input).resolve()).replace(".nii.gz", "") + + save_as_nii(z_map, f"{output_prefix}_z_score_map.nii.gz", xy_res, z_res, data_type='float32') + save_as_nii(inv_p_map, f"{output_prefix}_1-p_value_map.nii.gz", xy_res, z_res, data_type='float32') + save_as_nii(inv_p_map_fdr_corrected, f"{output_prefix}_1-p_value_map_fdr_corrected.nii.gz", xy_res, z_res, data_type='float32') + print("\n Z-score map, P-value map, and FDR-corrected P-value map saved.\n")
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/vstats.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/vstats.html new file mode 100644 index 00000000..e46b9ff5 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/vstats.html @@ -0,0 +1,631 @@ + + + + + + + + + + unravel.voxel_stats.vstats — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.vstats

+#!/usr/bin/env python3
+
+"""
+Use ``vstats`` from UNRAVEL to run voxel-wise stats using FSL's randomise_parallel command.
+
+Usage:
+------
+    vstats -mas mask.nii.gz -v
+
+Prereqs: 
+    - Input images from ``vstats_prep``, ``vstats_z_score``, or ``vstats_whole_to_avg``.
+
+Next steps:
+    - Run ``cluster_fdr_range`` and ``cluster_fdr`` to correct for multiple comparisons.
+
+For info on how to set up and run voxel-wise analyses, see: https://b-heifets.github.io/UNRAVEL/guide.html#voxel-wise-stats
+"""
+
+import argparse
+import shutil
+import subprocess
+from glob import glob
+import sys
+from fsl.wrappers import fslmaths, avwutils
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-mas', '--mask', help='path/mask.nii.gz', required=True, action=SM) + parser.add_argument('-p', '--permutations', help='Number of permutations (divisible by 300). Default: 18000', type=int, default=18000, action=SM) + parser.add_argument('-k', '--kernel', help='Smoothing kernel radius in mm if > 0. Default: 0 ', default=0, type=float, action=SM) + parser.add_argument('-opt', '--options', help='Additional options for randomise, specified like "--seed=1 -T"', nargs='*', default=[]) + parser.add_argument('-on', '--output_prefix', help='Prefix of output files. Default: current working dir name.', action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz (Default: /usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Add an email option to send a message when the processing is complete. Add progress bar. See if fragments can be generated in parallel. Could make avg and avg diff maps in this script (e.g., before merge since this is fast) + +
+[docs] +def check_fdr_command(): + """Check if the 'fdr' command is available in the system's path.""" + if shutil.which('fdr') is None: + print("Error: The 'fdr' command is not available. Please ensure that it is installed and in your PATH.") + sys.exit(1)
+ + +
+[docs] +def create_design_ttest2(mat_file, group1_size, group2_size): + """Create design matrix for a two-group unpaired t-test.""" + cmd = ["design_ttest2", str(mat_file), str(group1_size), str(group2_size)] + subprocess.run(cmd, check=True, stderr=subprocess.STDOUT)
+ + +
+[docs] +def get_groups_info(): + files = sorted(Path('.').glob('*.nii.gz')) + groups = {} + + for file in files: + prefix = file.stem.split('_')[0] + if prefix in groups: + groups[prefix] += 1 + else: + groups[prefix] = 1 + + for group, count in groups.items(): + print(f" Group {group} has {count} members") + + return groups
+ + +
+[docs] +def calculate_fragments(num_contrasts, total_permutations_per_contrast=18000, permutations_per_fragment=300): + """Calculate the total number of fragments based on the number of contrasts.""" + if num_contrasts is None: + return "Number of contrasts not determined." + total_fragments = (total_permutations_per_contrast * num_contrasts) // permutations_per_fragment + return total_fragments
+ + +
+[docs] +@print_func_name_args_times() +def run_randomise_parallel(input_image_path, mask_path, permutations, output_name, design_fts_path, options, verbose): + + # Construct the command + command = [ + "randomise_parallel", + "-i", str(input_image_path), + "-m", str(mask_path), + "-n", str(permutations), + "-o", str(output_name), + "-d", "stats/design.mat", + "-t", "stats/design.con", + "--uncorrp", + "-x" + ] + options # Make sure options is a list of strings + + design_fts_path = str(design_fts_path) if Path(design_fts_path).exists() else None + if design_fts_path: + command += ["-f", design_fts_path] + + command_line = " ".join(command) + print(f"\n[bold]{command_line}\n") + + if verbose: + # Execute the command and stream output + try: + with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) as proc: + for line in proc.stdout: + print(line, end='') # Print each line as it comes + if proc.returncode != 0: + print("Error executing command.") + except Exception as e: + print(f"Error during command execution: {str(e)}") + else: + # Execute the command silently and capture output + try: + process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + if process.returncode == 0: + print("randomise_parallel executed successfully\n") + else: + print("Error executing randomise_parallel:\n" + process.stderr) + except subprocess.CalledProcessError as e: + print("Error during command execution:\n" + str(e))
+ + + +
+[docs] +def main(): + args = parse_args() + + cwd = Path.cwd() + stats_dir = cwd / 'stats' + stats_dir.mkdir(exist_ok=True) + + # Copy the mask and the atlas to the stats directory using shutil + shutil.copy(args.mask, stats_dir) + shutil.copy(args.atlas, stats_dir) + + # Merge and smooth the input images + images = glob('*.nii.gz') + merged_file = stats_dir / 'all.nii.gz' + if not merged_file.exists(): + print('\n Merging *.nii.gz into ./stats/all.nii.gz') + avwutils.fslmerge('t', str(merged_file), *images) + else: + print('\n ./stats/all.nii.gz exists. Skipping...\n') + + # Smooth the image with a kernel + if args.kernel > 0: + kernel_in_um = int(args.kernel * 1000) + smoothed_file = merged_file.with_name(f'all_s{kernel_in_um}.nii.gz') + print(f'\n Smoothing all.nii.gz w/ fslmaths stats/all -s {args.kernel} {smoothed_file}') + fslmaths(merged_file).s(args.kernel).run(output=smoothed_file) + glm_input_file = smoothed_file + else: + glm_input_file = merged_file + + # Set up required design files or check that they exist + groups_info = get_groups_info() + group_keys = list(groups_info.keys()) + design_fts_path = stats_dir / 'design.fts' + if len(group_keys) == 2: + design_path_and_prefix = stats_dir / 'design' + create_design_ttest2(design_path_and_prefix, groups_info[group_keys[0]], groups_info[group_keys[1]]) + print(f"\n Running t-test for groups {group_keys[0]} and {group_keys[1]}\n") + elif len(group_keys) > 2: + print("\n Running ANOVA\n") + if not design_fts_path.exists(): + print(f'\n [red1]{design_fts_path} does not exist. See extended help for setting up files for the ANOVA\n') + import sys ; sys.exit() + else: + print("\n [red1]There should be at least two groups with different prefixes in the input .nii.gz files.\n") + + if args.output_prefix: + output_prefix = stats_dir / args.output_prefix + else: + output_prefix = stats_dir / cwd.name + + # Run the randomise_parallel command + run_randomise_parallel(glm_input_file, args.mask, args.permutations, output_prefix, design_fts_path, args.options, args.verbose)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/vstats_prep.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/vstats_prep.html new file mode 100644 index 00000000..7d2fb4ec --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/vstats_prep.html @@ -0,0 +1,565 @@ + + + + + + + + + + unravel.voxel_stats.vstats_prep — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.vstats_prep

+#!/usr/bin/env python3
+
+"""
+Use ``vstats_prep`` from UNRAVEL to load an immunofluo image, subtract its background, and warp it to atlas space.
+
+Usage:
+    vstats_prep -i ochann -rb 4 -x 3.5232 -z 6 -o ochann_rb4_gubra_space.nii.gz -e <list of paths to experiment directories> -v
+
+Prereqs: 
+    ``reg``
+
+Input examples (path is relative to ./sample??; 1st glob match processed): 
+    <asterisk>.czi, ochann/<asterisk>.tif, ochann, <asterisk>.tif, <asterisk>.h5, or <asterisk>.zarr
+
+Example output:
+    ./sample??/atlas_space/sample??_ochann_rb4_gubra_space.nii.gz
+
+Next steps: 
+    Aggregate outputs with ``utils_agg_files`` and run ``vstats``.
+"""
+
+import argparse
+import shutil
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.image_tools.spatial_averaging import apply_2D_mean_filter, spatial_average_2D, spatial_average_3D
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.img_tools import rolling_ball_subtraction_opencv_parallel
+from unravel.core.utils import print_cmd_and_times, initialize_progress_bar, get_samples
+from unravel.register.reg_prep import reg_prep
+from unravel.warp.to_atlas import to_atlas
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-td', '--target_dir', help='path/target_dir name for gathering outputs from all samples (use -e w/ all paths)', default=None, action=SM) + + # Required arguments: + parser.add_argument('-i', '--input', help='path to full res image', required=True, action=SM) + parser.add_argument('-o', '--output', help='Output file name w/o "sample??_" (added automatically). E.g., ochann_rb4_gubra_space.nii.gz', required=True, action=SM) + + # Optional arguments: + parser.add_argument('-sa', '--spatial_avg', help='Spatial averaging in 2D or 3D (2 or 3). Default: None', default=None, type=int, action=SM) + parser.add_argument('-rb', '--rb_radius', help='Radius of rolling ball in pixels (Default: 4)', default=4, type=int, action=SM) + parser.add_argument('-x', '--xy_res', help='Native x/y voxel size in microns (Default: get via metadata)', default=None, type=float, action=SM) + parser.add_argument('-z', '--z_res', help='Native z voxel size in microns (Default: get via metadata)', default=None, type=float, action=SM) + parser.add_argument('-c', '--chann_idx', help='.czi channel index. Default: 1', default=1, type=int, action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of registration inputs in microns. Default: 50', default='50',type=int, action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Reference nii header from ``reg``. Default: reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz', default="reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz (Default: /usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-dt', '--dtype', help='Desired dtype for output (e.g., uint8, uint16). Default: uint16', default="uint16", action=SM) + parser.add_argument('-zo', '--zoom_order', help='SciPy zoom order for resampling the raw image. Default: 1', default=1, type=int, action=SM) + parser.add_argument('-inp', '--interpol', help='Type of interpolation (linear, bSpline [default]).', default='bSpline', action=SM) + parser.add_argument('-mi', '--miracl', help='Mode for compatibility (accounts for tif to nii reorienting)', action='store_true', default=False) + parser.add_argument('-th', '--threads', help='Number of threads for rolling ball subtraction. Default: 8', default=8, type=int, action=SM) + parser.add_argument('-v', '--verbose', help='Enable verbose mode', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Fix this so -x and -z don't have to be provided if io_metadata has been run + +
+[docs] +def main(): + args = parse_args() + + if args.target_dir is not None: + # Create the target directory for copying outputs for ``vstats`` + target_dir = Path(args.target_dir) + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + output_name = f"{sample_path.name}_{Path(args.output).name}" + output = sample_path / "atlas_space" / output_name + output.parent.mkdir(exist_ok=True, parents=True) + if output.exists(): + print(f"\n {output} already exists. Skipping.") + continue + + # Load full res image [and xy and z voxel size in microns], to be resampled [and reoriented], padded, and warped + img_path = next(sample_path.glob(str(args.input)), None) + if img_path is None: + print(f"\n [red1]No files match the pattern {args.input} in {sample_path}\n") + continue + img = load_3D_img(img_path, args.chann_idx, "xyz") + + # Apply spatial averaging + if args.spatial_avg == 3: + img = spatial_average_3D(img, kernel_size=3) + elif args.spatial_avg == 2: + img = spatial_average_2D(img, apply_2D_mean_filter, kernel_size=(3, 3)) + + # Rolling ball background subtraction + rb_img = rolling_ball_subtraction_opencv_parallel(img, radius=args.rb_radius, threads=args.threads) + + # Resample the rb_img to the resolution of registration (and optionally reorient for compatibility with MIRACL) + rb_img = reg_prep(rb_img, args.xy_res, args.z_res, args.reg_res, args.zoom_order, args.miracl) + + # Warp the image to atlas space + fixed_reg_input = Path(sample_path, args.fixed_reg_in) + if not fixed_reg_input.exists(): + fixed_reg_input = sample_path / "reg_outputs" / "autofl_50um_fixed_reg_input.nii.gz" + + to_atlas(sample_path, rb_img, fixed_reg_input, args.atlas, output, args.interpol, dtype='uint16') + + # Copy the atlas to atlas_space + atlas_space = sample_path / "atlas_space" + shutil.copy(args.atlas, atlas_space) + + if args.target_dir is not None: + # Copy output to the target directory + target_output = target_dir / output.name + shutil.copy(output, target_output) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/whole_to_LR_avg.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/whole_to_LR_avg.html new file mode 100644 index 00000000..282dd8fc --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/whole_to_LR_avg.html @@ -0,0 +1,546 @@ + + + + + + + + + + unravel.voxel_stats.whole_to_LR_avg — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.whole_to_LR_avg

+#!/usr/bin/env python3
+
+"""
+Use ``vstats_whole_to_avg`` from UNRAVEL to average an image with its mirrored version for voxel-wise stats. This can also smooth the image with a kernel and apply a mask.
+
+Usage:
+------
+    vstats_whole_to_avg -k 0.1 -v -tp
+    
+Output:
+    input_img_LRavg.nii.gz
+
+Prereqs:
+    - Input images from ``vstats_prep``.
+        - These may be z-scored with ``vstats_z_score``.
+
+Next steps:
+    - Run ``vstats`` to perform voxel-wise stats.
+"""
+
+import argparse
+import numpy as np
+import nibabel as nib
+from glob import glob
+from fsl.wrappers import fslmaths
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+from concurrent.futures import ThreadPoolExecutor
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_cmd_and_times, print_func_name_args_times
+from unravel.voxel_stats.apply_mask import load_mask
+from unravel.voxel_stats.mirror import mirror
+        
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-p', '--pattern', help='Pattern to match atlas space input images in the working dir. Default: *.nii.gz', default='*.nii.gz', action=SM) + parser.add_argument('-k', '--kernel', help='Smoothing kernel radius in mm if > 0. Default: 0 ', default=0, type=float, action=SM) + parser.add_argument('-ax', '--axis', help='Axis to flip the image along. Default: 0', default=0, type=int, action=SM) + parser.add_argument('-s', '--shift', help='Number of voxels to shift content after flipping. Default: 2', default=2, type=int, action=SM) + parser.add_argument('-tp', '--parallel', help='Enable parallel processing with thread pools', default=False, action='store_true') + parser.add_argument('-amas', '--atlas_mask', help='path/atlas_mask.nii.gz', default=None, action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def whole_to_LR_avg(file, kernel=0, axis=0, shift=2, atlas_mask=None): + + if kernel > 0: + kernel_in_um = str(int(kernel * 1000)) + averaged_filename = f"{Path(file).name}".replace('.nii.gz', f'_s{kernel_in_um}_LRavg.nii.gz') + else: + averaged_filename = f"{Path(file).name}".replace('.nii.gz', '_LRavg.nii.gz') + + # Check if the output file already exists + if Path(averaged_filename).exists(): + print(f"Output {averaged_filename} already exists. Skipping...") + return + + print(f" Processing {file}\n") + nii = nib.load(file) + + # Smooth the image with a kernel + if kernel > 0: + print(f" Smoothing image with a kernel radius of {kernel} mm") + nii_smoothed = fslmaths(nii).s(kernel).run() + img = np.asanyarray(nii_smoothed.dataobj, dtype=np.float32).squeeze() + else: + img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze() + + # Mirror the image along the specified axis and shift the content by the specified number of voxels + mirrored_img = mirror(img, axis=axis, shift=shift) + + # Average the original and mirrored images + averaged_img = (img + mirrored_img) / 2 + + # Apply the mask + if atlas_mask is not None: + mask_img = load_mask(atlas_mask) + averaged_img[~mask_img] = 0 # Use logical NOT to flip True/False + + # Save the averaged image + averaged_nii = nib.Nifti1Image(averaged_img, nii.affine, nii.header) + + nib.save(averaged_nii, averaged_filename)
+ + + +
+[docs] +def main(): + args = parse_args() + + files = Path().cwd().glob(args.pattern) + print(f'\nImages to process: {list(files)}\n') + + files = Path().cwd().glob(args.pattern) + if args.parallel: + with ThreadPoolExecutor() as executor: + executor.map(lambda file: whole_to_LR_avg(file, args.kernel, args.axis, args.shift, args.atlas_mask), files) + else: + for file in files: + whole_to_LR_avg(file, args) + whole_to_LR_avg(file, args.kernel, args.axis, args.shift, args.atlas_mask)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/voxel_stats/z_score.html b/unravel/docs/_build/html/_modules/unravel/voxel_stats/z_score.html new file mode 100644 index 00000000..b0a5738b --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/voxel_stats/z_score.html @@ -0,0 +1,603 @@ + + + + + + + + + + unravel.voxel_stats.z_score — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.voxel_stats.z_score

+#!/usr/bin/env python3
+
+"""
+Use ``vstats_z_score`` from UNRAVEL to z-score an atlas space image using a tissue mask and/or an atlas mask.
+
+Usage w/ a tissue mask (warped to atlas space):
+-----------------------------------------------
+    vstats_z_score -i atlas_space/sample??_cfos_rb4_atlas_space.nii.gz -v
+
+Usage w/ an atlas mask (warped to atlas space):
+-----------------------------------------------
+    vstats_z_score -i path/img.nii.gz -n -amas path/atlas_mask.nii.gz -v
+
+Usage w/ both masks for side-specific z-scoring:
+------------------------------------------------
+    vstats_z_score -i atlas_space/sample??_cfos_rb4_atlas_space.nii.gz -amas path/RH_mask.nii.gz -s RHz -v
+
+Next steps: 
+    - If analyzing whole brains, consider using ``vstats_whole_to_avg`` to average hemispheres together.
+    - If using side-specific z-scoring, next use ``vstats_hemi_to_avg`` to average the images.
+    - Run ``vstats`` to perform voxel-wise stats.
+
+Outputs:
+    - <path/input_img>_z.nii.gz (float32)
+    - [sample??/atlas_space/autofl_50um_brain_mask.nii.gz]
+
+z-score = (img.nii.gz - mean pixel intensity in brain)/standard deviation of intensity in brain
+
+Prereqs:
+    ``vstats_prep`` for inputs [& ``seg_brain_mask`` for tissue masks]
+"""
+
+import argparse
+import shutil
+import nibabel as nib
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.utils import get_samples, initialize_progress_bar, print_func_name_args_times, print_cmd_and_times
+from unravel.warp.to_atlas import to_atlas
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-td', '--target_dir', help='path/target_dir name for gathering outputs from all samples (use -e w/ all paths)', default=None, action=SM) + parser.add_argument('-i', '--input', help='full_path/img.nii.gz or rel_path/img.nii.gz ("sample??" works for batch processing)', required=True, action=SM) + parser.add_argument('-s', '--suffix', help='Output suffix. Default: z (.nii.gz replaced w/ _z.nii.gz)', default='z', action=SM) + parser.add_argument('-tmas', '--tissue_mask', help='rel_path/brain_mask.nii.gz. Default: reg_inputs/autofl_50um_brain_mask.nii.gz', default="reg_inputs/autofl_50um_brain_mask.nii.gz", action=SM) + parser.add_argument('-amas', '--atlas_mask', help='path/atlas_mask.nii.gz (can use tmas and/or amas)', default=None, action=SM) + parser.add_argument('-n', '--no_tmask', help='Provide flag to avoid use of tmas', action='store_true') + parser.add_argument('-fri', '--fixed_reg_in', help='Reference nii header from ``reg``. Default: reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz', default="reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz for warping mask to atlas space (Default: path/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-inp', '--interpol', help='Type of interpolation (nearestNeighbor, multiLabel [default]).', default='multiLabel', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + +# TODO: Set voxels outside the mask(s) to zero + + +
+[docs] +@print_func_name_args_times() +def z_score(img, mask): + """Z-score the image using the mask. + + Args: + - img (str): the ndarray to be z-scored. + - mask (str): the brain mask ndarray""" + + # Zero out voxels outside the mask + masked_data = img * mask + + # Calculate mean and standard deviation for masked data + masked_nonzero = masked_data[masked_data != 0] # Exclude zero voxels + + mean_intensity = masked_nonzero.mean() + std_dev = masked_nonzero.std() + + # Z-score calculation + z_scored_img = (masked_data - mean_intensity) / std_dev + + return z_scored_img
+ + + +
+[docs] +def main(): + args = parse_args() + + if args.no_tmask and args.atlas_mask is None: + print("\n [red]Please provide a path for --atlas_mask if --tissue_mask is not used\n") + + if args.target_dir is not None: + # Create the target directory for copying outputs for ``vstats`` + target_dir = Path(args.target_dir) + target_dir.mkdir(exist_ok=True, parents=True) + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + if Path(args.input).is_absolute(): + input_path = Path(args.input) + if not input_path.exists(): + print(f"\n [red]The specified input file {input_path} does not exist.") + import sys ; sys.exit() + else: + # Handle relative path or pattern replacement + if f"{args.pattern}" in args.input: + input_path = Path(sample_path / args.input.replace(f"{args.pattern}", f"{sample_path.name}")) + else: + input_path = Path(sample_path / args.input) + + output = Path(str(input_path).replace('.nii.gz', f'_{args.suffix}.nii.gz')) + if output.exists(): + print(f"\n\n {output} already exists. Skipping.\n") + continue + + nii = nib.load(input_path) + img = np.asanyarray(nii.dataobj, dtype=np.float32).squeeze() + + if not args.no_tmask: + # Warp tissue mask to atlas space + brain_mask_in_tissue_space = load_3D_img(Path(sample_path, args.tissue_mask)) + mask_output = input_path.parent / Path(args.tissue_mask).name + + fixed_reg_input = Path(sample_path, args.fixed_reg_in) + if not fixed_reg_input.exists(): + fixed_reg_input = sample_path / "reg_outputs" / "autofl_50um_fixed_reg_input.nii.gz" + + to_atlas(sample_path, brain_mask_in_tissue_space, fixed_reg_input, args.atlas, mask_output, args.interpol, dtype='float32') + mask = load_3D_img(mask_output) + mask = np.where(mask > 0, 1, 0).astype(np.uint8) + + if args.atlas_mask: + atlas_mask_img = load_3D_img(args.atlas_mask) + atlas_mask_img = np.where(atlas_mask_img > 0, 1, 0).astype(np.uint8) + + if args.no_tmask: + mask = atlas_mask_img + elif args.atlas_mask: + mask *= atlas_mask_img + + z_scored_img = z_score(img, mask) + nii.header.set_data_dtype(np.float32) + z_scored_nii = nib.Nifti1Image(z_scored_img, nii.affine, nii.header) + nib.save(z_scored_nii, output) + + if args.target_dir is not None: + target_output = target_dir / output.name + shutil.copy(output, target_output) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/warp/to_atlas.html b/unravel/docs/_build/html/_modules/unravel/warp/to_atlas.html new file mode 100644 index 00000000..11cfaa7d --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/warp/to_atlas.html @@ -0,0 +1,585 @@ + + + + + + + + + + unravel.warp.to_atlas — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.warp.to_atlas

+#!/usr/bin/env python3
+
+"""
+Use ``warp_to_atlas`` from UNRAVEL to warp a native image to atlas space.
+
+Usage:
+------
+    warp_to_atlas -i ochann -o img_in_atlas_space.nii.gz -x 3.5232 -z 6 [-mi -v] 
+
+Prereqs: 
+    ``reg``
+
+Input examples (path is relative to ./sample??; 1st glob match processed): 
+    <asterisk>.czi, ochann/<asterisk>.tif, ochann, <asterisk>.tif, <asterisk>.h5, or <asterisk>.zarr 
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+
+from unravel.image_io.io_nii import convert_dtype
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_3D_img
+from unravel.core.img_tools import pad
+from unravel.core.utils import print_func_name_args_times, print_cmd_and_times, initialize_progress_bar, get_samples
+from unravel.register.reg_prep import reg_prep
+from unravel.warp.warp import warp
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + + # Required arguments: + parser.add_argument('-i', '--input', help='INPUT: Path of native image relative to ./sample??', required=True, action=SM) + parser.add_argument('-o', '--output', help='Output img.nii.gz (saved as ./sample??/atlas_space/img.nii.gz', required=True, action=SM) + + # Optional arguments: + parser.add_argument('-c', '--chann_idx', help='.czi channel index. Default: 1', default=1, type=int, action=SM) + parser.add_argument('-x', '--xy_res', help='Native x/y voxel size in microns (Default: get via metadata)', default=None, type=float, action=SM) + parser.add_argument('-z', '--z_res', help='Native z voxel size in microns (Default: get via metadata)', default=None, type=float, action=SM) + parser.add_argument('-a', '--atlas', help='path/atlas.nii.gz (Default: /usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz)', default='/usr/local/unravel/atlases/gubra/gubra_ano_combined_25um.nii.gz', action=SM) + parser.add_argument('-dt', '--dtype', help='Desired dtype for output (e.g., uint8, uint16). Default: uint16', default="uint16", action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Reference nii header from ``reg``. Default: reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz', default="reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of registration inputs in microns. Default: 50', default='50',type=int, action=SM) + parser.add_argument('-inp', '--interpol', help='Type of interpolation (linear, bSpline [default]).', default='bSpline', action=SM) + parser.add_argument('-zo', '--zoom_order', help='SciPy zoom order for resampling the raw image. Default: 1', default=1, type=int, action=SM) + parser.add_argument('-mi', '--miracl', help='Mode for compatibility (accounts for tif to nii reorienting)', action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Enable verbose mode', action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def copy_nii_header(source_img, new_img): + """Copy header info from nii_img to new_img""" + fields_to_copy = [ + 'xyzt_units', 'descrip', 'qform_code', 'sform_code', + 'qoffset_x', 'qoffset_y', 'qoffset_z', 'pixdim', + ] + for field in fields_to_copy: + new_img.header[field] = source_img.header[field] + + return new_img
+ + +
+[docs] +@print_func_name_args_times() +def to_atlas(sample_path, img, fixed_reg_in, atlas, output, interpol, dtype='uint16'): + """Warp the image to atlas space using ANTsPy. + + Args: + - sample_path (Path): Path to the sample directory. + - img (np.ndarray): 3D image. + - fixed_reg_in (str): Name of the fixed image for registration. + - atlas (str): Path to the atlas. + - output (str): Path to the output. + - interpol (str): Type of interpolation (linear, bSpline, nearestNeighbor, multiLabel). + - dtype (str): Desired dtype for output (e.g., uint8, uint16). Default: uint16""" + # Pad the image + img = pad(img, pad_width=0.15) + + # Create NIfTI, set header info, and save the input for warp() + fixed_reg_input = sample_path / fixed_reg_in + reg_outputs_path = fixed_reg_input.parent + warp_inputs_dir = reg_outputs_path / "warp_inputs" + warp_inputs_dir.mkdir(exist_ok=True, parents=True) + warp_input_path = str(warp_inputs_dir / output.name) + print(f'\n Setting header info and saving the input for warp() here: {warp_input_path}\n') + img = img.astype(np.float32) # Convert the fixed image to FLOAT32 for ANTsPy + fixed_reg_input_nii = nib.load(fixed_reg_input) + img_nii = nib.Nifti1Image(img, fixed_reg_input_nii.affine.copy(), fixed_reg_input_nii.header) + img_nii.set_data_dtype(np.float32) + nib.save(img_nii, warp_input_path) + + # Warp the image to atlas space + print(f'\n Warping image to atlas space\n') + warp(reg_outputs_path, warp_input_path, atlas, output, inverse=True, interpol=interpol) + + # Optionally lower the dtype of the output if the desired dtype is not float32 + if dtype.lower() != 'float32': + output_nii = nib.load(output) + output_img = output_nii.get_fdata(dtype=np.float32) + output_img = convert_dtype(output_img, dtype, scale_mode='none') + output_nii = nib.Nifti1Image(output_img, output_nii.affine.copy(), output_nii.header) + output_nii.header.set_data_dtype(dtype) + nib.save(output_nii, output)
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + output = sample_path / "atlas_space" / args.output + output.mkdir(exist_ok=True, parents=True) + if output.exists(): + print(f"\n\n {output} already exists. Skipping.\n") + continue + + # Load full res image [and xy and z voxel size in microns], to be resampled [and reoriented], padded, and warped + img_path = sample_path / args.input + img, xy_res, z_res = load_3D_img(img_path, args.chann_idx, "xyz", return_res=True, xy_res=args.xy_res, z_res=args.z_res) + + # Resample the rb_img to the resolution of registration (and optionally reorient for compatibility with MIRACL) + img = reg_prep(img, xy_res, z_res, args.reg_res, args.zoom_order, args.miracl) + + # Warp native image to atlas space + to_atlas(sample_path, img, args.fixed_reg_in, args.atlas, output, args.interpol, dtype='uint16') + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/warp/to_native.html b/unravel/docs/_build/html/_modules/unravel/warp/to_native.html new file mode 100644 index 00000000..3c74fcc1 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/warp/to_native.html @@ -0,0 +1,615 @@ + + + + + + + + + + unravel.warp.to_native — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.warp.to_native

+#!/usr/bin/env python3
+
+"""
+Use ``warp_to_native`` from UNRAVEL to warp an atlas space image to tissue space and scale to full resolution.
+
+CLI usage:
+----------
+    warp_to_native -m <path/image_to_warp_from_atlas_space.nii.gz> -o <native>/native_<img>.zarr
+
+Python usage:
+-------------
+    >>> import unravel.warp.to_native as to_native
+    >>> native_img = to_native(sample_path, reg_outputs, fixed_reg_in, moving_img_path, metadata_rel_path, reg_res, miracl, zoom_order, interpol, output=None)
+    >>> # native_img is an np.ndarray
+
+Prereq:
+    ./parameters/metadata.txt (from io_metadata)
+"""
+
+import argparse
+import nibabel as nib
+import numpy as np
+from pathlib import Path
+from rich import print
+from rich.live import Live
+from rich.traceback import install
+from scipy.ndimage import zoom
+
+from unravel.core.argparse_utils import SuppressMetavar, SM
+from unravel.core.config import Configuration
+from unravel.core.img_io import load_image_metadata_from_txt, save_as_zarr, save_as_nii
+from unravel.core.img_tools import reverse_reorient_for_raw_to_nii_conv
+from unravel.core.utils import get_samples, initialize_progress_bar, print_cmd_and_times, print_func_name_args_times
+from unravel.warp.warp import warp
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-e', '--exp_paths', help='List of experiment dir paths w/ sample?? dirs to process.', nargs='*', default=None, action=SM) + parser.add_argument('-p', '--pattern', help='Pattern for sample?? dirs. Use cwd if no matches.', default='sample??', action=SM) + parser.add_argument('-d', '--dirs', help='List of sample?? dir names or paths to dirs to process', nargs='*', default=None, action=SM) + parser.add_argument('-m', '--moving_img', help='path/image.nii.gz to warp from atlas space', required=True, action=SM) + parser.add_argument('-fri', '--fixed_reg_in', help='Fixed input for registration (``reg``). Default: autofl_50um_masked_fixed_reg_input.nii.gz', default="autofl_50um_masked_fixed_reg_input.nii.gz", action=SM) + parser.add_argument('-i', '--interpol', help='Interpolator for ants.apply_transforms (nearestNeighbor, multiLabel [default], linear, bSpline)', default="multiLabel", action=SM) + parser.add_argument('-o', '--output', help='Save as rel_path/native_image.zarr (fast) or rel_path/native_image.nii.gz if provided', default=None, action=SM) + parser.add_argument('-md', '--metadata', help='path/metadata.txt. Default: ./parameters/metadata.txt', default="./parameters/metadata.txt", action=SM) + parser.add_argument('-ro', '--reg_outputs', help="Name of folder w/ outputs from registration. Default: reg_outputs", default="reg_outputs", action=SM) + parser.add_argument('-r', '--reg_res', help='Resolution of registration inputs in microns. Default: 50', default='50',type=int, action=SM) + parser.add_argument('-zo', '--zoom_order', help='SciPy zoom order for scaling to full res. Default: 0 (nearest-neighbor)', default='0',type=int, action=SM) + parser.add_argument('-mi', '--miracl', help='Mode for compatibility (accounts for tif to nii reorienting)', action='store_true', default=False) + parser.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) + parser.epilog = __doc__ + return parser.parse_args()
+ + +
+[docs] +@print_func_name_args_times() +def calculate_resampled_padded_dimensions(original_dimensions, xy_res, z_res, target_res=50, pad_fraction=0.15, miracl=False): + # Calculate zoom factors for xy and z dimensions + zf_xy = xy_res / target_res + zf_z = z_res / target_res + + # Calculate expected dimensions of the resampled image (reg input is typically 50um) + resampled_dimensions = [ + round(dim * zf) for dim, zf in zip(original_dimensions, (zf_xy, zf_xy, zf_z)) + ] + + # Calculate padding for the resampled image (15% of the resampled dimensions) + padded_dimensions = [] + for dim in resampled_dimensions: + # Calculate pad width for one side, then round to the nearest integer + pad_width_one_side = np.round(pad_fraction * dim) + # Calculate total padding for the dimension (both sides) + total_pad = 2 * pad_width_one_side + # Calculate new dimension after padding + new_dim = dim + total_pad + padded_dimensions.append(int(new_dim)) + + # Swap axes if miracl compatibility mode is True + if miracl: + resampled_dimensions[0], resampled_dimensions[1] = resampled_dimensions[1], resampled_dimensions[0] + padded_dimensions[0], padded_dimensions[1] = padded_dimensions[1], padded_dimensions[0] + + return np.array(resampled_dimensions), np.array(padded_dimensions)
+ + +
+[docs] +@print_func_name_args_times() +def scale_to_full_res(ndarray, full_res_dims, zoom_order=0): + """Scale ndarray to match x, y, z dimensions provided as ndarray (order=0 is nearest-neighbor). Returns scaled ndarray.""" + zoom_factors = (full_res_dims[0] / ndarray.shape[0], full_res_dims[1] / ndarray.shape[1], full_res_dims[2] / ndarray.shape[2]) + scaled_img = zoom(ndarray, zoom_factors, order=zoom_order) + return scaled_img
+ + +
+[docs] +@print_func_name_args_times() +def to_native(sample_path, reg_outputs, fixed_reg_in, moving_img_path, metadata_rel_path, reg_res, miracl, zoom_order, interpol, output=None): + """Warp image from atlas space to tissue space and scale to full resolution""" + + # Warp the moving image to tissue space + reg_outputs_path = sample_path / reg_outputs + warp_outputs_dir = reg_outputs_path / "warp_outputs" + warp_outputs_dir.mkdir(exist_ok=True, parents=True) + warped_nii_path = str(warp_outputs_dir / str(Path(moving_img_path).name).replace(".nii.gz", "_in_tissue_space.nii.gz")) + if not Path(warped_nii_path).exists(): + print(f'\n Warping the moving image to tissue space\n') + fixed_img_for_reg_path = str(reg_outputs_path / fixed_reg_in) + warp(reg_outputs_path, moving_img_path, fixed_img_for_reg_path, warped_nii_path, inverse=False, interpol=interpol) + + # Lower bit depth to match atlas space image + warped_nii = nib.load(warped_nii_path) + moving_nii = nib.load(moving_img_path) + warped_img = np.asanyarray(warped_nii.dataobj, dtype=moving_nii.header.get_data_dtype()).squeeze() + + # Load resolutions and dimensions of full res image for scaling + metadata_path = sample_path / metadata_rel_path + xy_res, z_res, x_dim, y_dim, z_dim = load_image_metadata_from_txt(metadata_path) + if xy_res is None: + print(" [red1]./sample??/parameters/metadata.txt is missing. Generate w/ ``io_metadata``") + import sys ; sys.exit() + original_dimensions = np.array([x_dim, y_dim, z_dim]) + + # Calculate resampled and padded dimensions + resampled_dims, padded_dims = calculate_resampled_padded_dimensions(original_dimensions, xy_res, z_res, reg_res, pad_fraction=0.15, miracl=miracl) + + # Determine where to start cropping (combined padding size) // 2 for padding on one side + crop_mins = (padded_dims - resampled_dims) // 2 + + # Find img dims of warped image lacking padding + crop_sizes = resampled_dims + + # Perform cropping to remove padding + warped_img = warped_img[ + crop_mins[0]:crop_mins[0] + crop_sizes[0], + crop_mins[1]:crop_mins[1] + crop_sizes[1], + crop_mins[2]:crop_mins[2] + crop_sizes[2] + ] + + # Reorient if needed + if miracl: + warped_img = reverse_reorient_for_raw_to_nii_conv(warped_img) + + # Scale to full resolution + native_img = scale_to_full_res(warped_img, original_dimensions, zoom_order=zoom_order) + + # Save as .nii.gz or .zarr + if output is not None: + if str(output).endswith(".zarr"): + save_as_zarr(native_img, output) + elif str(output).endswith(".nii.gz"): + save_as_nii(native_img, output, xy_res, z_res, native_img.dtype) + + return native_img
+ + + +
+[docs] +def main(): + args = parse_args() + + samples = get_samples(args.dirs, args.pattern, args.exp_paths) + + progress, task_id = initialize_progress_bar(len(samples), "[red]Processing samples...") + with Live(progress): + for sample in samples: + + sample_path = Path(sample).resolve() if sample != Path.cwd().name else Path.cwd() + + if args.output is not None: + output = sample_path / args.output + else: + output = None + + to_native(sample_path, args.reg_outputs, args.fixed_reg_in, args.moving_img, args.metadata, args.reg_res, args.miracl, args.zoom_order, args.interpol, output=output) + + progress.update(task_id, advance=1)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_modules/unravel/warp/warp.html b/unravel/docs/_build/html/_modules/unravel/warp/warp.html new file mode 100644 index 00000000..753845a3 --- /dev/null +++ b/unravel/docs/_build/html/_modules/unravel/warp/warp.html @@ -0,0 +1,560 @@ + + + + + + + + + + unravel.warp.warp — UNRAVEL docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for unravel.warp.warp

+#!/usr/bin/env python3
+
+"""
+Use ``warp`` from UNRAVEL to warp to/from atlas space and registration input space
+
+Usage for forward warping atlas to tissue space:
+------------------------------------------------
+    warp -m atlas_img.nii.gz -f reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz -ro reg_outputs -o warp/atlas_in_tissue_space.nii.gz -inp multiLabel -v
+
+Usage for inverse warping tissue to atlas space:
+------------------------------------------------
+    warp -m reg_outputs/autofl_50um_masked_fixed_reg_input.nii.gz -f atlas_img.nii.gz -ro reg_outputs -o warp/tissue_in_atlas_space.nii.gz -inv -v
+
+Prereq: 
+    ``reg``
+"""
+
+import ants
+import argparse
+import numpy as np
+import nibabel as nib
+from pathlib import Path
+from rich import print
+from rich.traceback import install
+
+from unravel.core.argparse_utils import SM, SuppressMetavar
+from unravel.core.config import Configuration
+from unravel.core.utils import print_func_name_args_times, print_cmd_and_times
+
+
+
+[docs] +def parse_args(): + parser = argparse.ArgumentParser(formatter_class=SuppressMetavar) + parser.add_argument('-ro', '--reg_outputs', help='path/reg_outputs', required=True, action=SM) + parser.add_argument('-f', '--fixed_img', help='path/fixed_image.nii.gz', required=True, action=SM) + parser.add_argument('-m', '--moving_img', help='path/moving_image.nii.gz', required=True, action=SM) + parser.add_argument('-o', '--output', help='path/output.nii.gz', required=True, action=SM) + parser.add_argument('-inv', '--inverse', help='Perform inverse warping (use flag if -f & -m are opposite from ``reg``)', default=False, action='store_true') + parser.add_argument('-inp', '--interpol', help='Type of interpolation (linear, bSpline [default], nearestNeighbor, multiLabel).', default='bSpline', action=SM) + parser.add_argument('-v', '--verbose', help='Increase verbosity if flag provided', default=False, action='store_true') + parser.epilog = __doc__ + return parser.parse_args()
+ + + +
+[docs] +@print_func_name_args_times() +def warp(reg_outputs_path, moving_img_path, fixed_img_path, output_path, inverse, interpol): + """ + Applies the transformations to an image using ANTsPy. + + Parameters: + reg_outputs_path (str): Path to the reg_outputs folder (contains transformation files) + moving_img_path (str): Path to the image to be transformed. + fixed_img_path (str): Path to the reference image for applying the transform. + output_path (str): Path where the transformed image will be saved. + inverse (bool): If True, apply the inverse transformation. Defaults to False. + interpol (str): Type of interpolation (e.g., 'Linear', 'NearestNeighbor', etc.) + """ + + # Get the transforms prefix + transforms_prefix_file = next(reg_outputs_path.glob(f"*1Warp.nii.gz"), None) + if transforms_prefix_file is None: + raise FileNotFoundError(f"No '1Warp.nii.gz' file found in {reg_outputs_path}") + transforms_prefix = str(transforms_prefix_file.name).replace("1Warp.nii.gz", "") + + # Load images + fixed_img_ants = ants.image_read(fixed_img_path) + moving_img_ants = ants.image_read(moving_img_path) + + # Paths to the transformation files + generic_affine_matrix = str(reg_outputs_path / f'{transforms_prefix}0GenericAffine.mat') + initial_transform_matrix = str(reg_outputs_path / f'{transforms_prefix}init_tform.mat') + if not Path(reg_outputs_path / f'{transforms_prefix}init_tform.mat').exists(): + initial_transform_matrix = str(reg_outputs_path / 'init_tform.mat') # For backward compatibility + + + # Apply the transformations + if inverse: + deformation_field_inverse = str(reg_outputs_path / f'{transforms_prefix}1InverseWarp.nii.gz') + transformlist = [initial_transform_matrix, generic_affine_matrix, deformation_field_inverse] + whichtoinvert = [True, True, False] + else: + deformation_field = str(reg_outputs_path / f'{transforms_prefix}1Warp.nii.gz') + transformlist = [deformation_field, generic_affine_matrix, initial_transform_matrix] + whichtoinvert = [False, False, False] + + warped_img_ants = ants.apply_transforms( + fixed=fixed_img_ants, + moving=moving_img_ants, + transformlist=transformlist, + whichtoinvert=whichtoinvert, + interpolator=interpol + ) + + # Convert the ANTsImage to a numpy array + warped_img = warped_img_ants.numpy() + + # Round the floating-point label values to the nearest integer + warped_img = np.round(warped_img) + + # Convert dtype of warped image to match the moving image + moving_img_nii = nib.load(moving_img_path) + data_type = moving_img_nii.header.get_data_dtype() + warped_img[warped_img < 0] = 0 # Removes negative values from bSpline interpolation + warped_img = warped_img.astype(data_type) + + # Save the transformed image with appropriate header and affine information + fixed_img_nii = nib.load(fixed_img_path) + warped_img_nii = nib.Nifti1Image(warped_img, fixed_img_nii.affine.copy(), fixed_img_nii.header.copy()) + warped_img_nii.set_data_dtype(data_type) + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + nib.save(warped_img_nii, output_path)
+ + + +
+[docs] +def main(): + args = parse_args() + + reg_outputs_path = Path(args.reg_outputs).resolve() + moving_img_path = str(Path(args.moving_img).resolve()) + fixed_img_path = str(Path(args.fixed_img).resolve()) + + warp(reg_outputs_path, moving_img_path, fixed_img_path, args.output, args.inverse, args.interpol)
+ + + +if __name__ == '__main__': + install() + args = parse_args() + Configuration.verbose = args.verbose + print_cmd_and_times(main)() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/guide.md.txt b/unravel/docs/_build/html/_sources/guide.md.txt new file mode 100644 index 00000000..abea71e9 --- /dev/null +++ b/unravel/docs/_build/html/_sources/guide.md.txt @@ -0,0 +1,915 @@ +# Guide + +* If you are unfamiliar with the terminal, please review these [command line tutorials](https://andysbrainbook.readthedocs.io/en/latest/index.html). + +--- + +## Typical workflow + +* Each nodes shows a description of the step and related commands. + +:::{mermaid} +flowchart TD + A(((LSFM))) --> B[3D autofluo image] + B --> C(Registration: reg_prep, seg_copy_tifs, seg_brain_mask, reg, reg_check) + A --> D[3D immunofluo image] + D --> F(Remove autofluo + warp IF image to atlas space: vstats_prep) + C --> F + F --> G(Z-score: vstats_z_score) + G --> N(Aggregate images: utils_agg_files) + N --> H(Smoothing + average left and right sides: vstats_whole_to_avg) + H --> J(Voxel-wise stats: vstats) + J --> K(FDR correction of p value maps: cluster_fdr_range, cluster_fdr, cluster_mirror_indices) + K --> L(Warp clusters of significant voxels to tissue space + validate clusters with cell/label density measurements: cluster_validation, cluster_summary) + C --> L + D --> M(Segment features of interest: seg_copy_tifs, seg_ilastik) + M --> L +::: + +--- + +## Help on commands + +For help/info on a command, run this in the terminal: +```bash + -h +``` + +:::{admonition} Syntax +:class: hint dropdown +* The symbols < and > indicate placeholders. Replace \ with the actual command name you want to learn more about. +* Square brackets [ ] in command syntax signify optional elements. +* Double backticks are used in help guides to indicate a command. (e.g., \`\`command\`\`) +::: + +To view help on arguments for each script (a.k.a. module) in the online documentation, go to the page for that module, scroll to the parse_args() function, and click the link for viewing the source code. + +--- + +## Listing commands +```bash +# List common commands and their descriptions +unravel_commands -c -d + +# List all commands and their descriptions +unravel_commands -d + +# List the modules run by each command +unravel_commands -m +``` + +:::{hint} +* **Prefixes** group together related commands. Use **tab completion** in the terminal to quickly view and access sets of commands within each group. +::: + +--- + +## Common commands + +::::{tab-set} + +::: {tab-item} Registration +- [**reg_prep**](unravel.register.reg_prep): Prepare registration (resample the autofluo image). +- [**reg**](unravel.register.reg): Perform registration (register the autofluo image to an average template). +- [**reg_check**](unravel.register.reg_check): Check registration (aggregate the autofluo and warped atlas images). +::: + +::: {tab-item} Warping +- [**warp_to_atlas**](unravel.warp.to_atlas): Warp images to atlas space. +- [**warp_to_native**](unravel.warp.to_native): Warp images to native space. +::: + +::: {tab-item} Segmentation +- [**seg_copy_tifs**](unravel.segment.copy_tifs): Copy TIF images (copy select tifs to target dir for training ilastik). +- [**seg_brain_mask**](unravel.segment.brain_mask): Create brain mask (segment resampled autofluo tifs). +- [**seg_ilastik**](unravel.segment.ilastik_pixel_classification): Perform pixel classification w/ Ilastik to segment features of interest. +::: + +::: {tab-item} Voxel-wise stats +- [**vstats_prep**](unravel.voxel_stats.vstats_prep): Prepare immunofluo images for voxel statistics (e.g., background subtract and warp to atlas space). +- [**vstats_z_score**](unravel.voxel_stats.z_score): Z-score images. +- [**vstats_whole_to_avg**](unravel.voxel_stats.whole_to_LR_avg): Average left and right hemispheres together. +- [**vstats**](unravel.voxel_stats.vstats): Compute voxel statistics. +::: + +::: {tab-item} Cluster-wise stats +- [**cluster_fdr_range**](unravel.cluster_stats.fdr_range): Get FDR q value range yielding clusters. +- [**cluster_fdr**](unravel.cluster_stats.fdr): FDR-correct 1-p value map → cluster map. +- [**cluster_mirror_indices**](unravel.cluster_stats.recursively_mirror_rev_cluster_indices): Recursively mirror cluster maps for validating clusters in left and right hemispheres. +- [**cluster_validation**](unravel.cluster_stats.cluster_validation): Validate clusters w/ cell/label density measurements. +- [**cluster_summary**](unravel.cluster_stats.cluster_summary): Summarize info on valid clusters (run after cluster_validation). +::: + +::: {tab-item} Region-wise stats +- [**rstats**](unravel.region_stats.rstats): Compute regional cell counts, regional volumes, or regional cell densities. +- [**rstats_summary**](unravel.region_stats.rstats_summary): Summarize regional cell densities. +::: + +::: {tab-item} Image I/O +- [**io_metadata**](unravel.image_io.metadata): Handle image metadata. +- [**io_nii_info**](unravel.image_io.nii_info): Print info about NIfTI files. +::: + +::: {tab-item} Image tools +- [**img_avg**](unravel.image_tools.avg): Average NIfTI images. +- [**img_unique**](unravel.image_tools.unique_intensities): Find unique intensities in images. +- [**img_max**](unravel.image_tools.max): Print the max intensity value in an image. +- [**img_spatial_avg**](unravel.image_tools.spatial_averaging): Perform spatial averaging on images. +- [**img_rb**](unravel.image_tools.rb): Apply rolling ball filter to TIF images. +::: + +::: {tab-item} Utilities +- [**utils_agg_files**](unravel.utilities.aggregate_files_from_sample_dirs): Aggregate files from sample directories. +- [**utils_prepend**](unravel.utilities.prepend_conditions): Prepend conditions to files using sample_key.csv. +- [**utils_rename**](unravel.utilities.rename): Rename files. +::: + +:::: + +:::::{admonition} All commands +:class: note dropdown + +::::{tab-set} + +:::{tab-item} Registration +- [**reg_prep**](unravel.register.reg_prep): Prepare registration (resample the autofluo image). +- [**reg**](unravel.register.reg): Perform registration (register the autofluo image to an average template). +- [**reg_affine_initializer**](unravel.register.affine_initializer): Part of reg. Roughly aligns the template to the autofl image. +- [**reg_check**](unravel.register.reg_check): Check registration (aggregate the autofluo and warped atlas images). +- [**reg_check_brain_mask**](unravel.register.reg_check_brain_mask): Check brain mask for over/under segmentation. +::: + +:::{tab-item} Warping +- [**warp_to_atlas**](unravel.warp.to_atlas): Warp images to atlas space. +- [**warp_to_native**](unravel.warp.to_native): Warp images to native space. +- [**warp**](unravel.warp.warp): Warp between moving and fixed images. +::: + +:::{tab-item} Segmentation +- [**seg_copy_tifs**](unravel.segment.copy_tifs): Copy TIF images (copy select tifs to target dir for training ilastik). +- [**seg_brain_mask**](unravel.segment.brain_mask): Create brain mask (segment resampled autofluo tifs). +- [**seg_ilastik**](unravel.segment.ilastik_pixel_classification): Perform pixel classification w/ Ilastik to segment features of interest. +::: + +:::{tab-item} Voxel-wise stats +- [**vstats_apply_mask**](unravel.voxel_stats.apply_mask): Apply mask to image (e.g., nullify artifacts or isolate signals). +- [**vstats_prep**](unravel.voxel_stats.vstats_prep): Prepare immunofluo images for voxel statistics (e.g., background subtract and warp to atlas space). +- [**vstats_z_score**](unravel.voxel_stats.z_score): Z-score images. +- [**vstats_whole_to_avg**](unravel.voxel_stats.whole_to_LR_avg): Average left and right hemispheres together. +- [**vstats_hemi_to_avg**](unravel.voxel_stats.hemi_to_LR_avg): If left and right hemispheres were processed separately (less common), average them together. +- [**vstats**](unravel.voxel_stats.vstats): Compute voxel statistics. +- [**vstats_mirror**](unravel.voxel_stats.mirror): Flip and optionally shift content of images in atlas space. +::: + +:::{tab-item} Cluster-wise stats +- [**cluster_fdr_range**](unravel.cluster_stats.fdr_range): Get FDR q value range yielding clusters. +- [**cluster_fdr**](unravel.cluster_stats.fdr): FDR-correct 1-p value map → cluster map. +- [**cluster_mirror_indices**](unravel.cluster_stats.recursively_mirror_rev_cluster_indices): Recursively mirror cluster maps for validating clusters in left and right hemispheres. +- [**cluster_validation**](unravel.cluster_stats.cluster_validation): Validate clusters w/ cell/label density measurements. +- [**cluster_summary**](unravel.cluster_stats.cluster_summary): Summarize info on valid clusters (run after cluster_validation). +- [**cluster_org_data**](unravel.cluster_stats.org_data): Organize CSVs from cluster_validation. +- [**cluster_group_data**](unravel.cluster_stats.group_bilateral_data): Group bilateral cluster data. +- [**cluster_stats**](unravel.cluster_stats.stats): Compute cluster validation statistics. +- [**cluster_index**](unravel.cluster_stats.index): Make a valid cluster map and sunburst plots. +- [**cluster_brain_model**](unravel.cluster_stats.brain_model): Make a 3D brain model from a cluster map (for DSI studio). +- [**cluster_table**](unravel.cluster_stats.table): Create a table of cluster validation data. +- [**cluster_prism**](unravel.cluster_stats.prism): Generate CSVs for bar charts in Prism. +- [**cluster_legend**](unravel.cluster_stats.legend): Make a legend of regions in cluster maps. +- [**cluster_sunburst**](unravel.cluster_stats.sunburst): Create a sunburst plot of regional volumes. +- [**cluster_find_incongruent_clusters**](unravel.cluster_stats.find_incongruent_clusters): Find clusters where the effect direction does not match the prediction of cluster_fdr (for validation of non-directional p value maps). +- [**cluster_crop**](unravel.cluster_stats.crop): Crop clusters to a bounding box. +- [**effect_sizes**](unravel.cluster_stats.effect_sizes.effect_sizes): Calculate effect sizes for clusters. +- [**effect_sizes_sex_abs**](unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute): Calculate absolute effect sizes by sex. +- [**effect_sizes_sex_rel**](unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative): Calculate relative effect sizes by sex. +::: + +:::{tab-item} Region-wise stats +- [**rstats**](unravel.region_stats.rstats): Compute regional cell counts, regional volumes, or regional cell densities. +- [**rstats_summary**](unravel.region_stats.rstats_summary): Summarize regional cell densities. +- [**rstats_mean_IF**](unravel.region_stats.rstats_mean_IF): Compute mean immunofluo intensities for regions. +- [**rstats_mean_IF_in_seg**](unravel.region_stats.rstats_mean_IF_in_segmented_voxels): Compute mean immunofluo intensities in segmented voxels. +- [**rstats_mean_IF_summary**](unravel.region_stats.rstats_mean_IF_summary): Summarize mean immunofluo intensities for regions. +::: + +:::{tab-item} Image I/O +- [**io_metadata**](unravel.image_io.metadata): Handle image metadata. +- [**io_img**](unravel.image_io.io_img): Image I/O operations. +- [**io_nii_info**](unravel.image_io.nii_info): Print info about NIfTI files. +- [**io_nii_hd**](unravel.image_io.nii_hd): Print NIfTI headers. +- [**io_nii**](unravel.image_io.io_nii): NIfTI I/O operations (binarize, convert data type, scale, etc). +- [**io_reorient_nii**](unravel.image_io.reorient_nii): Reorient NIfTI files. +- [**io_nii_to_tifs**](unravel.image_io.nii_to_tifs): Convert NIfTI files to TIFFs. +- [**io_nii_to_zarr**](unravel.image_io.nii_to_zarr): Convert NIfTI files to Zarr. +- [**io_zarr_to_nii**](unravel.image_io.zarr_to_nii): Convert Zarr format to NIfTI. +- [**io_h5_to_tifs**](unravel.image_io.h5_to_tifs): Convert H5 files to TIFFs. +- [**io_tif_to_tifs**](unravel.image_io.tif_to_tifs): Convert TIF to TIFF series. +- [**io_img_to_npy**](unravel.image_io.img_to_npy): Convert images to Numpy arrays. +::: + +:::{tab-item} Image tools +- [**img_avg**](unravel.image_tools.avg): Average NIfTI images. +- [**img_unique**](unravel.image_tools.unique_intensities): Find unique intensities in images. +- [**img_max**](unravel.image_tools.max): Print the max intensity value in an image. +- [**img_bbox**](unravel.image_tools.bbox): Compute bounding box of non-zero voxels in an image. +- [**img_spatial_avg**](unravel.image_tools.spatial_averaging): Perform spatial averaging on images. +- [**img_rb**](unravel.image_tools.rb): Apply rolling ball filter to TIF images. +- [**img_DoG**](unravel.image_tools.DoG): Apply Difference of Gaussian filter to TIF images. +- [**img_pad**](unravel.image_tools.pad): Pad images. +- [**img_extend**](unravel.image_tools.extend): Extend images (add padding to one side). +- [**img_transpose**](unravel.image_tools.transpose_axes): Transpose image axes. +::: + +:::{tab-item} Atlas tools +- [**atlas_relabel**](unravel.image_tools.atlas.relabel_nii): Relabel atlas IDs. +- [**atlas_wireframe**](unravel.image_tools.atlas.wireframe): Make an atlas wireframe. +::: + +:::{tab-item} Utilities +- [**utils_agg_files**](unravel.utilities.aggregate_files_from_sample_dirs): Aggregate files from sample directories. +- [**utils_agg_files_rec**](unravel.utilities.aggregate_files_w_recursive_search): Recursively aggregate files. +- [**utils_prepend**](unravel.utilities.prepend_conditions): Prepend conditions to files using sample_key.csv. +- [**utils_rename**](unravel.utilities.rename): Rename files. +- [**utils_toggle**](unravel.utilities.toggle_samples): Toggle sample?? folders for select batch processing. +- [**utils_clean_tifs**](unravel.utilities.clean_tif_dirs): Clean TIF directories (no spaces, move non-tifs). +::: + +:::: + +::::: + + +:::{admonition} More info on commands +:class: note dropdown +unravel_commands runs ./\/unravel/unravel_commands.py + +Its help guide is here: {py:mod}`unravel.unravel_commands` + +Commands are defined in the `[project.scripts]` section of the [pyproject.toml](https://github.com/b-heifets/UNRAVEL/blob/main/pyproject.toml) in the root directory of the UNRAVEL repository (repo). + +If new commands are added to run new scripts (a.k.a. modules), reinstall the unravel package with pip. + +```bash +cd +pip install -e . +``` +::: + +--- + +## Set up + +Recommended steps to set up for analysis: + + +### Back up raw data + * For Heifets lab members, we keep one copy of raw data on an external drive and another on a remote server (Dan and Austen have access) + +### Stitch z-stacks + * We use [ZEN (blue edition)](https://www.micro-shop.zeiss.com/en/us/softwarefinder/software-categories/zen-blue/) since we have a [Zeiss Lightsheet 7](https://www.zeiss.com/microscopy/en/products/light-microscopes/light-sheet-microscopes/lightsheet-7.html) + + +:::{admonition} Batch stitching settings +:class: note dropdown +```{figure} _static/batch_stitching_1.JPG +:alt: Batch stitching settings +:height: 500px +:align: center +Select a mid-stack reference slice. Ideally, tissue will be present in each tile of the reference slice. +``` +::: + +:::{admonition} Running batch stitching +:class: note dropdown +```{figure} _static/batch_stitching_2.JPG +:alt: Batch stitching settings +:align: center +``` +* Drag and drop images to be stitched into this section. +* For each image in the list, apply the stitching settings one by one (do not go back to a prior image, as settings will no longer stick). +* Select all images (control + A or shift and click) +* Click "Check All" +* Click "Run Selected" +::: + +```{admonition} Open source options for stitching +:class: tip dropdown +* [TeraStitcher](https://abria.github.io/TeraStitcher/) +* [ZetaStitched](https://github.com/lens-biophotonics/ZetaStitcher) +``` + + +### Make sample folders + * Make a folder named after each condition in the experiment folder(s) +``` +. # This root level folder is referred to as the experiment directory (exp dir) +├── Control +└── Treatment +``` + * Make sample folders in the directories named after each condition + +```{admonition} Name sample folders like sample01, sample02, ... +:class: tip dropdown + +This makes batch processing easy. + +Use a csv, Google sheet, or whatever else for linking sample IDs to IDs w/ this convention. + +Other patterns (e.g., sample???) may be used (commands have a -p option for that). +``` + +``` +. +├── Control +│   ├── sample01 +│   └── sample02 +└── Treatment +    ├── sample03 +    └── sample04 +``` + +### Add images to sample?? dirs + + * For example, image.czi, image.h5, or folder(s) with tif series + +``` +. +├── Control +│   ├── sample01 +│ │ └── +│   └── sample02 +│ └── +└── Treatment +    ├── sample03 + │ └── +    └── sample04 + └── +``` + +```{admonition} Data can be distributed across multiple drives +:class: tip dropdown + +Paths to each experiment directory may be passed into scripts using the -e flag for batch processing + +This is useful if there is not enough storage on a single drive. + +Also, spreading data across ~2-4 external drives allows for faster parallel processing (minimizes i/o botlenecks) + +If SSDs are used, distrubuting data may not speed up processing as much. +``` + +### Log exp paths, commands, etc. +:::{admonition} Make an exp_notes.txt +:class: tip dropdown +This helps with keeping track of paths, commands, etc.. +```bash +cd # Change the current working directory to an exp dir +touch exp_notes.txt # Make the .txt file +``` +::: + +:::{admonition} Automatic logging of scripts +:class: tip dropdown +Most scripts log the command used to run them, appending to ./.command_log.txt. + +This is a hidden file. Use control+h to view it on Linux or command+shift+. to view it on MacOS. +```bash +# View command history for the current working directory +cat .command_log.txt + +# View last 10 lines +cat .command_log.txt | tail -10 +``` +::: + +```{todo} +Log commands instead of scripts +``` + + +### Make a sample_key.csv: + +It should have these columns: + * dir_name,condition + * sample01,control + * sample02,treatment + * ... + +### Define common variables in a shell script +:::{admonition} env_var.sh +:class: note dropdown +* To make commands easier to run, define common variables in a shell script (e.g., env_var.sh) +* Source the script to load variables in each terminal session +* Copy /UNRAVEL/unravel/env_var.sh to an exp dir and update each variable + +Add this line to your .bashrc or .zshrc terminal config file: +```bash +alias exp=". /path/env_var.sh" # Update the /path/name.sh +``` + +```bash +# Reopen the terminal or source the updated config file to apply changes +. ~/.zshrc + +# Run the alias to source the variables before using them to run commands/scripts +exp +``` +::: + + +### Optional: clean tifs + * If raw data is in the form of a tif series, consider running: +```bash +utils_clean_tifs -t -v -m -e $DIRS +``` +```{admonition} utils_clean_tifs +:class: tip dropdown +This will remove spaces from files names and move files other than *.tif to the parent directory +``` + +### Note x/y and z voxel sizes +Extract or specify metadata (outputs to ./sample??/parameters/metadata.txt). Add the resolutions to env_var.sh. + +{py:mod}`unravel.image_io.metadata` + +```bash +io_metadata -i # Glob patterns work for -i +io_metadata -i -x $XY -z $Z # Specifying x and z voxel sizes in microns +``` +
+ +--- + + +## Analysis steps + +This section provides an overview of common commands available in UNRAVEL, ~organized by their respective steps. + +### Registration +#### `reg_prep` +{py:mod}`unravel.register.reg_prep` +* Prepare autofluo images for registration (resample to a lower resolution) +```bash +reg_prep -i *.czi -x $XY -z $Z -v # -i options: tif_dir, .h5, .zarr, .tif +``` + +#### `seg_copy_tifs` +{py:mod}`unravel.segment.copy_tifs` +* Copy resampled autofluo .tif files for making a brain mask with ilastik +```bash +seg_copy_tifs -i reg_inputs/autofl_??um_tifs -s 0000 0005 0050 -o $(dirname $BRAIN_MASK_ILP) -e $DIRS +``` + +#### Train an Ilastik project +:::{admonition} Train an Ilastik project +:class: note dropdown +Launch ilastik (e.g., by running: `ilastik` if an alias was added to the shell profile) and follow these steps: + +1. **Input Data** + Drag training slices into the ilastik GUI + `ctrl+A` -> right-click -> Edit shared properties -> Storage: Copy into project file -> Ok + +2. **Feature Selection** + Select Features... -> select all features (`control+a`) or an optimized subset (faster but less accurate) + (To choose a subset of features, initially select all (`control+a`), train, turn off Live Updates, click Suggest Features, select a subset, and train again) + +3. **Training** + - Double click yellow square -> click yellow rectangle (Color for drawing) -> click in triangle and drag to the right to change color to red -> ok + - Adjust brightness and contrast as needed (select gradient button and click and drag slowly in the image as needed; faster if zoomed in) + - Use `control` + mouse wheel scroll to zoom, press mouse wheel and drag image to pan + - With label 1 selected, paint on cells + - With label 2 selected, paint on the background + - Turn on Live Update to preview pixel classification (faster if zoomed in) and refine training. + - If label 1 fuses neighboring cells, draw a thin line in between them with label 2. + - Toggle eyes to show/hide layers and/or adjust transparency of layers. + - `s` will toggle segmentation on and off. + - `p` will toggle prediction on and off. + - If you accidentally press `a` and add an extra label, turn off Live Updates and press X next to the extra label to delete it. + - If you want to go back to steps 1 & 2, turn off Live Updates off + - Change Current view to see other training slices. Check segmentation for these and refine as needed. + - Save the project in the experiment summary folder and close if using this script to run ilastik in headless mode for segmenting all images. + +[Pixel Classification Video](https://www.ilastik.org/documentation/pixelclassification/pixelclassification) + +::: + +#### `seg_brain_mask` +{py:mod}`unravel.segment.brain_mask` +* Makes reg_inputs/autofl_??um_brain_mask.nii.gz and reg_inputs/autofl_??um_masked.nii.gz for reg +```bash +seg_brain_mask -ilp $BRAIN_MASK_ILP -v -e $DIRS +``` + +:::{hint} +seg_brain_mask zeros out voxels outside of the brain. This prevents the average template (moving image) from being pulled outward during registration (reg). + +If non-zero voxles outside the brain remain and are affecting reg quality, use 3D slicer to zero them out by painting in 3D (segmentation module). + +If there is missing tissue, use 3D slicer to fill in gaps. +::: + +:::{todo} +Add tutorial for 3D slicer +::: + +#### `reg` +{py:mod}`unravel.register.reg` +* Register an average template brain/atlas to a resampled autofluo brain. + +```{admonition} 3 letter orientation code +:class: note dropdown +- Letter options: + - A/P=Anterior/Posterior + - L/R=Left/Right + - S/I=Superior/Interior +- Letter order: + - The side of the brain at the positive direction of the x, y, and z axes determines the 3 letters + - Letter 1: Side of the brain right of z-stack + - Letter 2: Side of the brain facing bottom of z-stack + - Letter 3: Side of the brain facing back of z-stack +``` + +```bash +reg -m $TEMPLATE -bc -pad -sm 0.4 -ort RPS -a $ATLAS -v -e $DIRS +``` + +:::{admonition} If sample orientations vary +:class: tip dropdown +Make a ./sample??/parameters/ort.txt with the 3 letter orientation for each sample and run: +```bash +for d in $DIRS ; do cd $d ; for s in sample?? ; do reg -m $TEMPLATE -bc -pad -sm 0.4 -ort $(cat $s/parameters/ort.txt) -a $ATLAS -v -d $PWD/$s ; done ; done +``` +::: + +:::{note} +* We use an adapted version of a `iDISCO/LSFM-specific atlas `_ +::: + + +#### `reg_check` +{py:mod}`unravel.register.reg_check` +* Check registration by copying these images to a target directory: + * sample??/reg_outputs/autofl_??um_masked_fixed_reg_input.nii.gz + * sample??/reg_outputs/atlas_in_tissue_space.nii.gz +```bash +reg_check -e $DIRS -td $BASE/reg_results +``` +* View these images with [FSLeyes](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FSLeyes) [docs](https://open.win.ox.ac.uk/pages/fsl/fsleyes/fsleyes/userdoc/index.html) + +:::{admonition} Allen brain atlas coloring +:class: note dropdown +* Replace /home/user/.config/fsleyes/luts/random.lut (location on Linux) with UNRAVEL/_other/fsleyes_luts/random.lut +* Select the atlas and change "3D/4D volume" to "Label image" +::: + + + +### Segmentation + +For detailed instructions on training Ilastik, see **Train an Ilastik project** in the [**Registration**](#registration) section. + +#### `seg_copy_tifs` +{py:mod}`unravel.segment.copy_tifs` +* Copy full res tif files to a target dir for training Ilastik to segment labels of interest +:::{tip} +Copy 3 tifs from each sample or 3 tifs from 3 samples / condition +::: +```bash +seg_copy_tifs -i -s 0100 0500 1000 -o ilastik_segmentation -e $DIRS -v +``` + +#### `seg_ilastik` +{py:mod}`unravel.segment.ilastik_pixel_classification` +* Perform pixel classification using a trained Ilastik project +```bash +seg_ilastik -i <*.czi, *.h5, or dir w/ tifs> -o seg_dir -ilp $BASE/ilastik_segmentation/trained_ilastik_project.ilp -l 1 -v -e $DIRS +``` + + +### Voxel-wise stats +:::{admonition} Overview and steps for voxel-wise stats +:class: note dropdown + +1. **Create a vstats folder and subfolders for each analysis**: + - Name subfolders succinctly (this name is added to other folder and file names). + +2. **Generate and add .nii.gz files to vstats subfolders**: + - Input images are from ``vstats_prep`` and may have been z-scored with ``vstats_z_score`` (we z-score c-Fos labeling as intensities are not extreme) + - Alternatively, ``warp_to_atlas`` may be used is preprocessing is not desired. + - For bilateral data, left and right sides can be averaged with ``vstats_whole_to_avg`` (then use a unilateral hemisphere mask for ``vstats`` and ``cluster_fdr``). + - We smooth data (e.g., with a 100 µm kernel) to account for innacuracies in registration + - This can be performed with ``vstats_whole_to_avg`` or ``vstats`` + - Prepend filenames with a one word condition (e.g., `drug_sample01_atlas_space_z.nii.gz`). + - Camel case is ok for the condition. + - ``utils_prepend`` can add conditions to filenames. + - Group order is alphabetical (e.g., drug is group 1 and saline is group 2). + - View the images in [FSLeyes](https://open.win.ox.ac.uk/pages/fsl/fsleyes/fsleyes/userdoc/index.html) to ensure they are aligned and the sides are correct. + +3. **Determine Analysis Type**: + - If there are 2 groups, ``vstats`` may be used after pre-processing. + - If there are more than 2 groups, prepare for an ANOVA as described below + +#### Vstats outputs +- **T-test outputs**: + - `vox_p_tstat1.nii.gz`: Uncorrected p-values for tstat1 (group 1 > group 2). + - `vox_p_tstat2.nii.gz`: Uncorrected p-values for tstat2 (group 1 < group 2). + +- **ANOVA outputs**: + - `vox_p_fstat1.nii.gz`: Uncorrected p-values for fstat1 (1st contrast, e.g., drug vs. saline). + - `vox_p_fstat2.nii.gz`: Uncorrected p-values for fstat2 (2nd contrast, e.g., context1 vs. context2). + - `vox_p_fstat3.nii.gz`: Uncorrected p-values for fstat3 (3rd contrast, e.g., interaction). + +#### Example: Preparing for an ANOVA +1. **Setup Design Matrix**: + - For an ANOVA, create `./vstats/vstats_dir/stats/design/`. + - Open terminal from `./stats` and run: `fsl`. + - Navigate to `Misc -> GLM Setup`. + +2. **GLM Setup Window**: + - Select `Higher-level / non-timeseries design`. + - Set `# inputs` to the total number of samples. + +3. **EVs Tab in GLM Window**: + - Set `# of main EVs` to 4. + - Name EVs (e.g., `EV1 = group 1`). + - Set Group to 1 for all. + +4. **Design Matrix**: + - Under `EV1`, enter 1 for each subject in group 1 (1 row/subject). EV2-4 are 0 for these rows. + - Under `EV2`, enter 1 for each subject in group 2, starting with the row after the last row for group 1. + - Follow this pattern for EV3 and EV4. + +5. **Contrasts & F-tests Tab in GLM Window**: + - Set `# of Contrasts` to 3 for a 2x2 ANOVA: + - `C1`: `Main_effect_` 1 1 -1 -1 (e.g., EV1/2 are drug groups and EV3/4 are saline groups). + - `C2`: `Main_effect_` 1 -1 1 -1 (e.g., EV1/3 were in context1 and EV2/4 were in context2). + - `C3`: `Interaction` 1 -1 -1 1. + - Set `# of F-tests` to 3: + - `F1`: Click upper left box. + - `F2`: Click middle box. + - `F3`: Click lower right box. + +6. **Finalize GLM Setup**: + - In the GLM Setup window, click `Save`, then click `design`, and click `OK`. + +7. **Run Voxel-wise Stats**: + - From the vstats_dir, run: ``vstats``. + +#### Background +- [FSL GLM Guide](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/GLM) +- [FSL Randomise User Guide](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Randomise/UserGuide) + +::: + +#### `vstats_prep` +{py:mod}`unravel.voxel_stats.vstats_prep` +* Preprocess immunofluo images and warp them to atlas space for voxel-wise statistics. +```bash +vstats_prep -i cFos -rb 4 -x $XY -z $Z -o cFos_rb4_atlas_space.nii.gz -v -e $DIRS +``` +:::{admonition} Background subtraction +:class: tip dropdown +Removing autofluorescence from immunolabeling improves the sensitivity of voxel-wise comparisons. + +Use -s 3 for 3x3x3 spatial averaging if there is notable noise from voxel to voxel. + +Use a smaller rolling ball radius if you want to preserve punctate signal like c-Fos+ nuclei (e.g., 4) + +Use a larger rolling ball radius if you want to preserve more diffuse signal (e.g., 20). + +The radius should be similar to the largest feature that you want to preserve. + +You can test parameters for background subtraction with: +* {py:mod}`unravel.image_tools.spatial_averaging` +* {py:mod}`unravel.image_tools.rb` + * Copy a tif to a test dir for this. + * Use {py:mod}`unravel.image_io.io_img` to create a tif series +::: + +#### `vstats_z_score` +{py:mod}`unravel.voxel_stats.z_score` +* Z-score atlas space images using tissue masks (from brain_mask) and/or an atlas mask. + +```bash +vstats_z_score -i atlas_space/sample??_cFos_rb4_atlas_space.nii.gz -v -e $DIRS +``` +:::{hint} +* atlas_space is a folder in ./sample??/ with outputs from vstats_prep +::: + +#### `utils_agg_files` +{py:mod}`unravel.utilities.aggregate_files_from_sample_dirs` +* Aggregate pre-processed immunofluorescence (IF) images for voxel-wise stats +```bash +utils_agg_files -i atlas_space/sample??_cFos_rb4_atlas_space_z.nii.gz -e $DIRS -v +``` + +#### `vstats_whole_to_avg` +{py:mod}`unravel.voxel_stats.whole_to_LR_avg` +* Smooth and average left and right hemispheres together +```bash +# Run this in the folder with the .nii.gz images to process +vstats_whole_to_avg -k 0.1 -tp -v # A 0.05 mm - 0.1 mm kernel radius is recommended for smoothing +``` + +:::{seealso} +{py:mod}`unravel.voxel_stats.hemi_to_LR_avg` +::: + +#### `utils_prepend` +{py:mod}`unravel.utilities.prepend_conditions` +* Prepend conditions to filenames based on a CSV w/ this organization + * dir_name,condition + * sample01,control + * sample02,treatment +```bash +utils_prepend -sk $SAMPLE_KEY -f +``` + +#### `vstats` +{py:mod}`unravel.voxel_stats.vstats` +* Run voxel-wise stats using FSL's randomise_parallel command. + +```bash +vstats -mas mask.nii.gz -v +``` + +:::{note} +* Outputs in ./stats/ + * vox_p maps are uncorrected 1 - p value maps + * tstat1: group1 > group2 + * tstat2: group2 > group1 + * fstat*: f contrast w/ ANOVA design (non-directional p value maps) +::: + + + +### Cluster correction +These commands are useful for multiple comparison correction of 1 - p value maps to define clusters of significant voxels. + +#### `img_avg` +{py:mod}`unravel.image_tools.avg` + +* Average *.nii.gz images +* Visualize absolute and relative differences in intensity +* Use averages from each group to convert non-directioanl 1 - p value maps into directional cluster indices with cluster_fdr +```bash +img_avg -i Control_*.nii.gz -o Control_avg.nii.gz +``` + +#### `cluster_fdr_range` +{py:mod}`unravel.cluster_stats.fdr_range` +* Outputs a list of FDR q values that yeild clusters. +```bash +# Basic usage +cluster_fdr_range -i vox_p_tstat1.nii.gz -mas mask.nii.gz + +# Perform FDR correction on multiple directional 1 - p value maps +for j in *_vox_p_*.nii.gz ; do q_values=$(cluster_fdr_range -mas $MASK -i $j) ; cluster_fdr -mas $MASK -i $j -q $q_values ; done + +# Convert a non-directioanl 1 - p value map into a directional cluster index +q_values=$(cluster_fdr_range -i vox_p_fstat1.nii.gz -mas $MASK) ; cluster_fdr -i vox_p_fstat1.nii.gz -mas $MASK -o fstat1 -v -a1 Control_avg.nii.gz -a2 Deep_avg.nii.gz -q $q_values +``` + +#### `cluster_fdr` +{py:mod}`unravel.cluster_stats.fdr` +* Perform FDR correction on a 1 - p value map to define clusters +```bash +cluster_fdr -i vox_p_tstat1.nii.gz -mas mask.nii.gz -q 0.05 +``` + +#### `cluster_mirror_indices` +{py:mod}`unravel.cluster_stats.recursively_mirror_rev_cluster_indices` +* Recursively flip the content of rev_cluster_index.nii.gz images +* Run this in the ./stats/ folder to process all subdirs with reverse cluster maps (cluster IDs go from large to small) +```bash +# Use -m RH if a right hemisphere mask was used (otherwise use -m LH) +cluster_mirror_indices -m RH -v +``` + + +### Cluster validation + +#### `cluster_validation` +{py:mod}`unravel.cluster_stats.cluster_validation` +* Warps cluster index from atlas space to tissue space, crops clusters, applies segmentation mask, and quantifies cell/object or label densities +```bash +# Basic usage: +cluster_validation -e -m -s seg_dir -v + +# Processing multiple FDR q value thresholds and both hemispheres: +for q in 0.005 0.01 0.05 0.1 ; do for side in LH RH ; do cluster_validation -e $DIRS -m path/vstats/contrast/stats/contrast_vox_p_tstat1_q${q}/contrast_vox_p_tstat1_q${q}_rev_cluster_index_${side}.nii.gz -s seg_dir/sample??_seg_dir_1.nii.gz -v ; done ; done +``` + +#### `cluster_summary` +{py:mod}`unravel.cluster_stats.cluster_summary` +* Aggregates and analyzes cluster validation data from `cluster_validation` +* Update parameters in /UNRAVEL/unravel/cluster_stats/cluster_summary.ini and save it with the experiment +```bash +cluster_summary -c path/cluster_summary.ini -e $DIRS -cvd '*' -vd path/vstats_dir -sk $SAMPLE_KEY --groups group1 group2 -v +``` +group1 and group2 must match conditions in the sample_key.csv + + + +### Region-wise stats + +#### `rstats` +{py:mod}`unravel.region_stats.regional_cell_densities` +* Perform regional cell counting (label density measurements needs to be added) +```bash +# Use if atlas is already in native space from warp_to_native +rstats -s rel_path/segmentation_image.nii.gz -a rel_path/native_atlas_split.nii.gz -c Saline --dirs sample14 sample36 + +# Use if native atlas is not available; it is not saved (faster) +rstats -s rel_path/segmentation_image.nii.gz -m path/atlas_split.nii.gz -c Saline --dirs sample14 sample36 +``` + +#### `rstats_summary` +{py:mod}`unravel.region_stats.regional_cell_densities_summary` +* Plot cell densities for each region and summarize results. +* CSV columns: + * Region_ID,Side,Name,Abbr,Saline_sample06,Saline_sample07,...,MDMA_sample01,...,Meth_sample23,... +```bash +rstats_summary --groups Saline MDMA Meth -d 10000 -hemi r +``` + + +### Example sample?? folder structure after analysis +```bash +. +├── atlas_space # Dir with images warped to atlas space +├── cfos_seg_ilastik_1 # Example dir with segmentations from ilastik +├── clusters # Dir with cell/label density CSVs from cluster_validation +│   ├── Control_v_Treatment_vox_p_tstat1_q0.005 +│ │ └── cell_density_data.csv +│   └── Control_v_Treatment_vox_p_tstat2_q0.05 +│ └── cell_density_data.csv +├── parameters # Optional dir for things like metadata.txt +├── reg_inputs # From reg_prep (autofl image resampled for reg) and seg_brain_mask (mask, masked autofl) +├── regional_cell_densities # CSVs with regional cell densities data +├── reg_outputs # Outputs from reg. These images are typically padded w/ empty voxels. +└── image.czi # Or other raw/stitched image type +``` + +### Example experiment folder structure after analysis +```bash +. +├── exp_notes.txt +├── env_var.sh +├── sample_key.csv +├── Control +│   ├── sample01 +│   └── sample02 +├── Treatment +│   ├── sample03 +│   └── sample04 +├── atlas +│   ├── gubra_ano_25um_bin.nii.gz # bin indicates that this has been binarized (background = 0; foreground = 1) +│   ├── gubra_ano_combined_25um.nii.gz # Each atlas region has a unique intensity/ID +│   ├── gubra_ano_split_25um.nii.gz # Intensities in the left hemisphere are increased by 20,000 +│   ├── gubra_mask_25um_wo_ventricles_root_fibers_LH.nii.gz # Left hemisphere mask that excludes ventricles, undefined regions (root), and fiber tracts +│   ├── gubra_mask_25um_wo_ventricles_root_fibers_RH.nii.gz +│   └── gubra_template_25um.nii.gz # Average template brain that is aligned with the atlas +├── reg_results +├── ilastik_brain_mask +│   ├── brain_mask.ilp # Ilastik project trained with the pixel classification workflow to segment the brain in resampled autofluo images +│   ├── sample01_slice_0000.tif +│   ├── sample01_slice_0005.tif +│   ├── sample01_slice_0050.tif +│ ├── ... +│   └── sample04_slice_0050.tif +├── vstats +│   └── Control_v_Treatment +│   ├── Control_sample01_rb4_atlas_space_z.tif +│   ├── Control_sample02_rb4_atlas_space_z.tif +│   ├── Treatment_sample03_rb4_atlas_space_z.tif +│   ├── Treatment_sample04_rb4_atlas_space_z.tif +│ └── stats +│   ├── Control_v_Treatment_vox_p_tstat1.nii.gz # 1 minus p value map showing where Control (group 1) > Treatment (group2) +│   ├── Control_v_Treatment_vox_p_tstat2.nii.gz # 1 - p value map showing where Treatment (group 2) > Control (group 1) +│   ├── Control_v_Treatment_vox_p_tstat1_q0.005 # cluster correction folder +│   │ ├── 1-p_value_threshold.txt # FDR adjusted 1 - p value threshold for the uncorrected 1 - p value map +│   │ ├── p_value_threshold.txt # FDR adjusted p value threshold +│   │ ├── min_cluster_size_in_voxels.txt # Often 100 voxels for c-Fos. For sparser signals like amyloid beta plaques, consider 400 or more +│   │ └── ..._rev_cluster_index.nii.gz # cluster map (index) with cluster IDs going from large to small (input for cluster_validation) +│ ├── ... +│   └── Control_v_Treatment_vox_p_tstat2_q0.05 +├── cluster_validation +│   ├── Control_v_Treatment_vox_p_tstat1_q0.005 +│ │ ├── Control_sample01_cell_density_data.csv +│ │ ├── Control_sample02_cell_density_data.csv +│ │ ├── Treatment_sample03_cell_density_data.csv +│ │ └── Treatment_sample04_cell_density_data.csv +│ ├── ... +│   └── Control_v_Treatment_vox_p_tstat2_q0.05 +└── regional_cell_densities +    ├── Control_sample01_regional_cell_densities.csv + ├── ... +    └── Treatment_sample04_regional_cell_densities.csv +``` + +```{todo} +Also show the structure of data from cluster_validation and rstats_summary + +Add support for CCFv3 2020 +``` \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/index.rst.txt b/unravel/docs/_build/html/_sources/index.rst.txt new file mode 100644 index 00000000..c8deabd4 --- /dev/null +++ b/unravel/docs/_build/html/_sources/index.rst.txt @@ -0,0 +1,172 @@ +.. UNRAVEL documentation master file, created by + sphinx-quickstart on Tue Jun 4 17:52:09 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. raw:: html + + + + + +.. raw:: html + + + + + +.. raw:: html + +
+ +UN-biased high-Resolution Analysis and Validation of Ensembles using Light sheet images +======================================================================================= +* UNRAVEL is a `Python `_ package & command line tool for the analysis of brain-wide imaging data, automating: + * Registration of brain-wide images to a common atlas space + * Quantification of cell/label densities across the brain + * Voxel-wise analysis of fluorescent signals and cluster correction + * Validation of hot/cold spots via cell/label density quantification at cellular resolution +* `UNRAVEL GitHub repository `_ +* `Initial UNRAVEL publication `_ +* UNRAVEL was developed by `the Heifets lab `_ and `TensorAnalytics `_ +* Additional support/guidance was provided by: + * `The Shamloo lab `_ + * `The Malenka lab `_ + * `The Stanford-based P50 center funded by NIDA `_ + +.. raw:: html + +
+ +.. raw:: html + +
+ + + +
+ +Getting started +--------------- +* `Guide on immunofluorescence staining, iDISCO+, & lightsheet fluorescence microscopy `_ +* :doc:`installation` +* :doc:`guide` +* :doc:`unravel/toc` + + +UNRAVEL visualizer +------------------- +* `UNRAVEL visualizer `_ is a web-based tool for visualizing and exploring 3D maps output from UNRAVEL +* `UNRAVEL visualizer GitHub repo `_ +* Developed by `MetaCell `_ with support from the `Heifets lab `_ + +.. raw:: html + +
+ + UNRAVEL visualizer + +
+ + +Contact us +---------- +If you have any questions, suggestions, or are interested in collaborations and contributions, please reach out to us. + + +Developers +---------- +* **Daniel Ryskamp Rijsketic** (lead developer and maintainer) - `danrijs@stanford.edu `_ +* **Austen Casey** (developer) - `abcasey@stanford.edu `_ +* **MetaCell** (UNRAVEL visualizer developers) - `info@metacell.us `_ +* **Boris Heifets** (PI) - `bheifets@stanford.edu `_ + + +Additional contributions from +----------------------------- +* **Mehrdad Shamloo** (PI) - `shamloo@stanford.edu `_ +* **Daniel Barbosa** (early contributer and guidance) - `Dbarbosa@pennmedicine.upenn.edu `_ +* **Wesley Zhao** (guidance) - `weszhao@stanford.edu `_ +* **Nick Gregory** (guidance) - `ngregory@stanford.edu `_ + + +Main dependencies +----------------- +* `Allen Institute for Brain Science `_ +* `FSL `_ +* `fslpy `_ +* `ANTsPy `_ +* `Ilastik `_ +* `nibabel `_ +* `numpy `_ +* `scipy `_ +* `pandas `_ +* `cc3d `_ +* Registration and warping workflows were inspired by `MIRACL `_ +* We adapted `LSFM/iDISCO+ atlases `_ from `Gubra `_ + +Support is welcome for +---------------------- +* Analysis of new datasets +* Development of new features +* Maintenance of the codebase +* Guidance of new users + +.. raw:: html + +
+ +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + installation + guide + unravel/toc + +.. raw:: html + +
+ + +Indices +======= + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + + +.. raw:: html + + \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/installation.md.txt b/unravel/docs/_build/html/_sources/installation.md.txt new file mode 100644 index 00000000..278ca4bd --- /dev/null +++ b/unravel/docs/_build/html/_sources/installation.md.txt @@ -0,0 +1,154 @@ +# Installation + +* Please send questions/issues to [danrijs@stanford.edu](mailto:danrijs@stanford.edu), so we can improve this guide for future users. + +* If you are unfamiliar with the terminal, please review these [command line tutorials](https://andysbrainbook.readthedocs.io/en/latest/index.html) + +## Setting Up Windows Subsystem for Linux (WSL) + +1. **Install WSL:** + + - Open PowerShell as Administrator and run: + ```powershell + wsl --install + ``` + + - Restart your computer if prompted. + +2. **Install a Linux distribution:** + + - After the restart, open the Microsoft Store and install your preferred Linux distribution (e.g., Ubuntu). + +3. **Initialize your Linux distribution:** + + - Open your installed Linux distribution from the Start menu. + - Follow the prompts to set up your username and password. + +For detailed instructions, visit the [WSL Installation Guide](https://docs.microsoft.com/en-us/windows/wsl/install). + +## Installing UNRAVEL on Linux or WSL + +1. **Open a terminal and navigate to the directory where you want to clone the UNRAVEL GitHub repository:** + +2. **Clone the repository:** + ```bash + git clone https://github.com/b-heifets/UNRAVEL.git + git checkout dev + ``` + [GitHub Cloning Documentation](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) + +3. **Install pyenv to manage Python versions and create a virtual environment:** + + **a. Install dependencies:** + ```bash + sudo apt-get update + sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \ + libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \ + libncurses5-dev libncursesw5-dev xz-utils tk-dev \ + libffi-dev liblzma-dev python-openssl git + ``` + + **b. Install pyenv:** + ```bash + curl https://pyenv.run | bash + ``` + + **c. Add pyenv to your shell startup file (.bashrc or .zshrc):** + ```bash + echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc + echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc + echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init --path)"\nfi' >> ~/.bashrc + echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bashrc + exec "$SHELL" + ``` + + [pyenv Installation Guide](https://github.com/pyenv/pyenv#installation) + +4. **Install Python 3.11:** + ```bash + pyenv install 3.11.3 + ``` + +5. **Create and activate a virtual environment:** + ```bash + pyenv virtualenv 3.11.3 unravel + pyenv activate unravel + ``` + +6. **Install pip if needed:** + [Pip Installation Guide](https://pip.pypa.io/en/stable/installation/) + +7. **Install UNRAVEL locally:** + ```bash + pip install -e . + ``` + +:::{todo} +Add unravel to PyPI so that users can install it by running something like: + +```bash +pip install unravel +``` +::: + + +8. **Download atlas/template files and locate them in `./atlas/`:** + [Google Drive Link](https://drive.google.com/drive/folders/1iZjQlPc2kPagnVsjWEFFObLlkSc2yRf9?usp=sharing) + +9. **Install Ilastik:** + - Download the Ilastik installer from the [Ilastik website](https://www.ilastik.org/download.html). + - Follow the installation instructions specific to your operating system. + - Example for Linux: + ```bash + wget https://files.ilastik.org/ilastik-1.3.3post3-Linux.tar.bz2 + tar -xjf ilastik-1.3.3post3-Linux.tar.bz2 + sudo mv ilastik-1.3.3post3-Linux /usr/local/ + ``` + +10. **Install FSL:** + - Follow the installation instructions from the [FSL website](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FslInstallation). + - Example for Ubuntu: + ```bash + sudo apt-get update + sudo apt-get install -y fsl + ``` + +11. **Confirm the installation and get started by viewing the help guide in the ``unravel.image_io.metadata`` module (``io_metadata`` commad) :** + ```bash + io_metadata -h + ``` + +12. **Update scripts periodically:** + :::{hint} + * Make a backup of the code that you used for analysis before updating + ::: + + ```bash + cd + + git pull # This will update the local repo + + pip install -e . # This will update commands and install new dependencies + ``` + +## Editing .bashrc or .zshrc + +Add the following to your `.bashrc` or `.zshrc` file, and change `/usr/local/` to the path where FSL is installed: + +```bash +export PATH=$PATH:/usr/local/fsl/bin +export FSLDIR=/usr/local/fsl +PATH=${FSLDIR}/bin:${PATH} +. ${FSLDIR}/etc/fslconf/fsl.sh +export FSLDIR PATH +export PATH=/usr/local/ilastik-1.3.3post3-Linux:$PATH + +# Add these to open ilastik via the terminal by running: ilastik +export PATH=/usr/local/ilastik-1.3.3post3-Linux:$PATH +alias ilastik=run_ilastik.sh +``` + +Apply the changes by restarting the terminal or source your shell configuration file: +```bash +. ~/.bashrc +``` diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/brain_model.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/brain_model.rst.txt new file mode 100644 index 00000000..15efa700 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/brain_model.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.brain_model: + +unravel.cluster_stats.brain_model module +======================================== + +.. automodule:: unravel.cluster_stats.brain_model + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/cluster_summary.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/cluster_summary.rst.txt new file mode 100644 index 00000000..85d6c584 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/cluster_summary.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.cluster_summary: + +unravel.cluster_stats.cluster_summary module +=================================================== + +.. automodule:: unravel.cluster_stats.cluster_summary + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/cluster_validation.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/cluster_validation.rst.txt new file mode 100644 index 00000000..a77040db --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/cluster_validation.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.cluster_validation: + +unravel.cluster_stats.cluster_validation module +=============================================== + +.. automodule:: unravel.cluster_stats.cluster_validation + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/crop.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/crop.rst.txt new file mode 100644 index 00000000..072cd008 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/crop.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.crop: + +unravel.cluster_stats.crop module +========================================== + +.. automodule:: unravel.cluster_stats.crop + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes.rst.txt new file mode 100644 index 00000000..451aa1fe --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.effect_sizes.effect_sizes: + +unravel.cluster_stats.effect_sizes.effect_sizes module +====================================================== + +.. automodule:: unravel.cluster_stats.effect_sizes.effect_sizes + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.rst.txt new file mode 100644 index 00000000..e94bbf50 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__absolute.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute: + +unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute module +======================================================================= + +.. automodule:: unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__absolute + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.rst.txt new file mode 100644 index 00000000..58779f4b --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/effect_sizes_by_sex__relative.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative: + +unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative module +======================================================================= + +.. automodule:: unravel.cluster_stats.effect_sizes.effect_sizes_by_sex__relative + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/toc.rst.txt new file mode 100644 index 00000000..75ee4265 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/effect_sizes/toc.rst.txt @@ -0,0 +1,15 @@ +unravel.cluster_stats.effect_sizes package +========================================== + +.. toctree:: + :maxdepth: 2 + + effect_sizes + effect_sizes_by_sex__absolute + effect_sizes_by_sex__relative + +.. automodule:: unravel.cluster_stats.effect_sizes + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/fdr.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/fdr.rst.txt new file mode 100644 index 00000000..596165bb --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/fdr.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.fdr: + +unravel.cluster_stats.fdr module +================================ + +.. automodule:: unravel.cluster_stats.fdr + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/fdr_range.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/fdr_range.rst.txt new file mode 100644 index 00000000..84f07583 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/fdr_range.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.fdr_range: + +unravel.cluster_stats.fdr_range module +====================================== + +.. automodule:: unravel.cluster_stats.fdr_range + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/find_incongruent_clusters.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/find_incongruent_clusters.rst.txt new file mode 100644 index 00000000..a0778e2f --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/find_incongruent_clusters.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.find_incongruent_clusters: + +unravel.cluster_stats.find_incongruent_clusters module +====================================================== + +.. automodule:: unravel.cluster_stats.find_incongruent_clusters + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/group_bilateral_data.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/group_bilateral_data.rst.txt new file mode 100644 index 00000000..4b8b3be0 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/group_bilateral_data.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.group_bilateral_data: + +unravel.cluster_stats.group_bilateral_data module +================================================= + +.. automodule:: unravel.cluster_stats.group_bilateral_data + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/index.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/index.rst.txt new file mode 100644 index 00000000..b16c6033 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/index.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.index: + +unravel.cluster_stats.index module +================================== + +.. automodule:: unravel.cluster_stats.index + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/legend.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/legend.rst.txt new file mode 100644 index 00000000..b2627df6 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/legend.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.legend: + +unravel.cluster_stats.legend module +=================================== + +.. automodule:: unravel.cluster_stats.legend + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/org_data.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/org_data.rst.txt new file mode 100644 index 00000000..d05e92c1 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/org_data.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.org_data: + +unravel.cluster_stats.org_data module +===================================== + +.. automodule:: unravel.cluster_stats.org_data + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/prism.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/prism.rst.txt new file mode 100644 index 00000000..2ba9fe75 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/prism.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.prism: + +unravel.cluster_stats.prism module +================================== + +.. automodule:: unravel.cluster_stats.prism + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.rst.txt new file mode 100644 index 00000000..67c7e7eb --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/recursively_mirror_rev_cluster_indices.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.recursively_mirror_rev_cluster_indices: + +unravel.cluster_stats.recursively_mirror_rev_cluster_indices module +=================================================================== + +.. automodule:: unravel.cluster_stats.recursively_mirror_rev_cluster_indices + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/stats.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/stats.rst.txt new file mode 100644 index 00000000..48bcd2f2 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/stats.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.stats: + +unravel.cluster_stats.stats module +================================== + +.. automodule:: unravel.cluster_stats.stats + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/stats_table.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/stats_table.rst.txt new file mode 100644 index 00000000..7c97acd5 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/stats_table.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.stats_table: + +unravel.cluster_stats.stats_table module +======================================== + +.. automodule:: unravel.cluster_stats.stats_table + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/sunburst.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/sunburst.rst.txt new file mode 100644 index 00000000..62492f0c --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/sunburst.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.sunburst: + +unravel.cluster_stats.sunburst module +===================================== + +.. automodule:: unravel.cluster_stats.sunburst + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/table.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/table.rst.txt new file mode 100644 index 00000000..cad2103c --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/table.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.cluster_stats.table: + +unravel.cluster_stats.table module +================================== + +.. automodule:: unravel.cluster_stats.table + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/cluster_stats/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/cluster_stats/toc.rst.txt new file mode 100644 index 00000000..eef2d48d --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/cluster_stats/toc.rst.txt @@ -0,0 +1,30 @@ +unravel.cluster_stats package +============================= + +.. toctree:: + :maxdepth: 3 + + fdr_range + fdr + recursively_mirror_rev_cluster_indices + cluster_validation + cluster_summary + brain_model + find_incongruent_clusters + group_bilateral_data + index + legend + org_data + prism + stats + stats_table + sunburst + table + crop + effect_sizes/toc + +.. automodule:: unravel.cluster_stats + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/core/argparse_utils.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/argparse_utils.rst.txt new file mode 100644 index 00000000..18b16a27 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/argparse_utils.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.core.argparse_utils: + +unravel.core.argparse_utils module +================================== + +.. automodule:: unravel.core.argparse_utils + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/core/argparse_utils_rich.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/argparse_utils_rich.rst.txt new file mode 100644 index 00000000..2616cbd3 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/argparse_utils_rich.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.core.argparse_utils_rich: + +unravel.core.argparse_utils_rich module +======================================= + +.. automodule:: unravel.core.argparse_utils_rich + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/core/config.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/config.rst.txt new file mode 100644 index 00000000..dff4869e --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/config.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.core.config: + +unravel.core.config module +========================== + +.. automodule:: unravel.core.config + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/core/img_io.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/img_io.rst.txt new file mode 100644 index 00000000..e47a2039 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/img_io.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.core.img_io: + +unravel.core.img_io module +========================== + +.. automodule:: unravel.core.img_io + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/core/img_tools.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/img_tools.rst.txt new file mode 100644 index 00000000..62479a19 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/img_tools.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.core.img_tools: + +unravel.core.img_tools module +============================= + +.. automodule:: unravel.core.img_tools + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/core/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/toc.rst.txt new file mode 100644 index 00000000..2b22a0be --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/toc.rst.txt @@ -0,0 +1,18 @@ +unravel.core package +==================== + +.. toctree:: + :maxdepth: 2 + + argparse_utils + argparse_utils_rich + config + img_io + img_tools + utils + +.. automodule:: unravel.core + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/core/utils.rst.txt b/unravel/docs/_build/html/_sources/unravel/core/utils.rst.txt new file mode 100644 index 00000000..2b47d119 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/core/utils.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.core.utils: + +unravel.core.utils module +========================= + +.. automodule:: unravel.core.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/h5_to_tifs.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/h5_to_tifs.rst.txt new file mode 100644 index 00000000..94656b83 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/h5_to_tifs.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.h5_to_tifs: + +unravel.image_io.h5_to_tifs module +================================== + +.. automodule:: unravel.image_io.h5_to_tifs + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/img_to_npy.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/img_to_npy.rst.txt new file mode 100644 index 00000000..6270f010 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/img_to_npy.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.img_to_npy: + +unravel.image_io.img_to_npy module +================================== + +.. automodule:: unravel.image_io.img_to_npy + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/io_img.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/io_img.rst.txt new file mode 100644 index 00000000..37b8e5df --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/io_img.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.io_img: + +unravel.image_io.io_img module +============================== + +.. automodule:: unravel.image_io.io_img + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/io_nii.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/io_nii.rst.txt new file mode 100644 index 00000000..618da7f3 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/io_nii.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.io_nii: + +unravel.image_io.io_nii module +============================== + +.. automodule:: unravel.image_io.io_nii + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/metadata.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/metadata.rst.txt new file mode 100644 index 00000000..14608523 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/metadata.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.metadata: + +unravel.image_io.metadata module +================================ + +.. automodule:: unravel.image_io.metadata + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/nii_hd.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/nii_hd.rst.txt new file mode 100644 index 00000000..27f6f8f4 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/nii_hd.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.nii_hd: + +unravel.image_io.nii_hd module +============================== + +.. automodule:: unravel.image_io.nii_hd + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/nii_info.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/nii_info.rst.txt new file mode 100644 index 00000000..e99d3f5d --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/nii_info.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.nii_info: + +unravel.image_io.nii_info module +================================ + +.. automodule:: unravel.image_io.nii_info + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/nii_to_tifs.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/nii_to_tifs.rst.txt new file mode 100644 index 00000000..7d7e6442 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/nii_to_tifs.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.nii_to_tifs: + +unravel.image_io.nii_to_tifs module +=================================== + +.. automodule:: unravel.image_io.nii_to_tifs + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/nii_to_zarr.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/nii_to_zarr.rst.txt new file mode 100644 index 00000000..318558ac --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/nii_to_zarr.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.nii_to_zarr: + +unravel.image_io.nii_to_zarr module +=================================== + +.. automodule:: unravel.image_io.nii_to_zarr + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/reorient_nii.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/reorient_nii.rst.txt new file mode 100644 index 00000000..7a1c9802 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/reorient_nii.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.reorient_nii: + +unravel.image_io.reorient_nii module +==================================== + +.. automodule:: unravel.image_io.reorient_nii + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/tif_to_tifs.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/tif_to_tifs.rst.txt new file mode 100644 index 00000000..1169bcb7 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/tif_to_tifs.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.tif_to_tifs: + +unravel.image_io.tif_to_tifs module +=================================== + +.. automodule:: unravel.image_io.tif_to_tifs + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/toc.rst.txt new file mode 100644 index 00000000..8a947571 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/toc.rst.txt @@ -0,0 +1,24 @@ +unravel.image_io package +======================== + +.. toctree:: + :maxdepth: 2 + + h5_to_tifs + img_to_npy + io_img + metadata + nii_hd + nii_info + io_nii + nii_to_tifs + nii_to_zarr + reorient_nii + tif_to_tifs + zarr_to_nii + +.. automodule:: unravel.image_io + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/image_io/zarr_to_nii.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_io/zarr_to_nii.rst.txt new file mode 100644 index 00000000..f6da3689 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_io/zarr_to_nii.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_io.zarr_to_nii: + +unravel.image_io.zarr_to_nii module +=================================== + +.. automodule:: unravel.image_io.zarr_to_nii + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/DoG.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/DoG.rst.txt new file mode 100644 index 00000000..c6b5c0c4 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/DoG.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.DoG: + +unravel.image_tools.DoG module +================================== + +.. automodule:: unravel.image_tools.DoG + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/relabel_nii.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/relabel_nii.rst.txt new file mode 100644 index 00000000..0c3ede74 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/relabel_nii.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.atlas.relabel_nii: + +unravel.image_tools.atlas.relabel_nii module +============================================ + +.. automodule:: unravel.image_tools.atlas.relabel_nii + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/toc.rst.txt new file mode 100644 index 00000000..a439bd99 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/toc.rst.txt @@ -0,0 +1,14 @@ +unravel.image_tools.atlas package +================================= + +.. toctree:: + :maxdepth: 2 + + relabel_nii + wireframe + +.. automodule:: unravel.image_tools.atlas + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/wireframe.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/wireframe.rst.txt new file mode 100644 index 00000000..5a1ad21c --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/atlas/wireframe.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.atlas.wireframe: + +unravel.image_tools.atlas.wireframe module +========================================== + +.. automodule:: unravel.image_tools.atlas.wireframe + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/avg.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/avg.rst.txt new file mode 100644 index 00000000..99a67366 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/avg.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.avg: + +unravel.image_tools.avg module +============================== + +.. automodule:: unravel.image_tools.avg + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/bbox.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/bbox.rst.txt new file mode 100644 index 00000000..8f10170d --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/bbox.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.bbox: + +unravel.image_tools.bbox module +=============================== + +.. automodule:: unravel.image_tools.bbox + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/extend.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/extend.rst.txt new file mode 100644 index 00000000..2e11f228 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/extend.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.extend: + +unravel.image_tools.extend module +======================================= + +.. automodule:: unravel.image_tools.extend + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/max.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/max.rst.txt new file mode 100644 index 00000000..3733f58d --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/max.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.max: + +unravel.image_tools.max module +============================== + +.. automodule:: unravel.image_tools.max + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/pad.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/pad.rst.txt new file mode 100644 index 00000000..35375bbf --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/pad.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.pad: + +unravel.image_tools.pad module +================================== + +.. automodule:: unravel.image_tools.pad + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/rb.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/rb.rst.txt new file mode 100644 index 00000000..505a0f4f --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/rb.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.rb: + +unravel.image_tools.rb module +================================= + +.. automodule:: unravel.image_tools.rb + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/spatial_averaging.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/spatial_averaging.rst.txt new file mode 100644 index 00000000..4c9d2440 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/spatial_averaging.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.spatial_averaging: + +unravel.image_tools.spatial_averaging module +============================================ + +.. automodule:: unravel.image_tools.spatial_averaging + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/toc.rst.txt new file mode 100644 index 00000000..9078206b --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/toc.rst.txt @@ -0,0 +1,23 @@ +unravel.image_tools package +=========================== + +.. toctree:: + :maxdepth: 3 + + avg + bbox + extend + max + pad + spatial_averaging + DoG + rb + transpose_axes + unique_intensities + atlas/toc + +.. automodule:: unravel.image_tools + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/transpose_axes.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/transpose_axes.rst.txt new file mode 100644 index 00000000..93381c2f --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/transpose_axes.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.transpose_axes: + +unravel.image_tools.transpose_axes module +========================================= + +.. automodule:: unravel.image_tools.transpose_axes + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/image_tools/unique_intensities.rst.txt b/unravel/docs/_build/html/_sources/unravel/image_tools/unique_intensities.rst.txt new file mode 100644 index 00000000..bc81ccb4 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/image_tools/unique_intensities.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.image_tools.unique_intensities: + +unravel.image_tools.unique_intensities module +============================================= + +.. automodule:: unravel.image_tools.unique_intensities + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/region_stats/rstats.rst.txt b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats.rst.txt new file mode 100644 index 00000000..4f466072 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.region_stats.rstats: + +unravel.region_stats.rstats module +=================================================== + +.. automodule:: unravel.region_stats.rstats + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF.rst.txt b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF.rst.txt new file mode 100644 index 00000000..427ba9a8 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.region_stats.rstats_mean_IF: + +unravel.region_stats.rstats_mean_IF module +======================================================== + +.. automodule:: unravel.region_stats.rstats_mean_IF + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.rst.txt b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.rst.txt new file mode 100644 index 00000000..ae16581c --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF_in_segmented_voxels.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.region_stats.rstats_mean_IF_in_segmented_voxels: + +unravel.region_stats.rstats_mean_IF_in_segmented_voxels module +============================================================================ + +.. automodule:: unravel.region_stats.rstats_mean_IF_in_segmented_voxels + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF_summary.rst.txt b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF_summary.rst.txt new file mode 100644 index 00000000..4cf0127b --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_mean_IF_summary.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.region_stats.rstats_mean_IF_summary: + +unravel.region_stats.rstats_mean_IF_summary module +================================================================ + +.. automodule:: unravel.region_stats.rstats_mean_IF_summary + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_summary.rst.txt b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_summary.rst.txt new file mode 100644 index 00000000..77795218 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/region_stats/rstats_summary.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.region_stats.rstats_summary: + +unravel.region_stats.rstats_summary module +=========================================================== + +.. automodule:: unravel.region_stats.rstats_summary + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/region_stats/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/region_stats/toc.rst.txt new file mode 100644 index 00000000..9a862ccc --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/region_stats/toc.rst.txt @@ -0,0 +1,17 @@ +unravel.region_stats package +============================ + +.. toctree:: + :maxdepth: 2 + + rstats + rstats_summary + rstats_mean_IF + rstats_mean_IF_in_segmented_voxels + rstats_mean_IF_summary + +.. automodule:: unravel.region_stats + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/register/affine_initializer.rst.txt b/unravel/docs/_build/html/_sources/unravel/register/affine_initializer.rst.txt new file mode 100644 index 00000000..81437e4e --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/register/affine_initializer.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.register.affine_initializer: + +unravel.register.affine_initializer module +================================================= + +.. automodule:: unravel.register.affine_initializer + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/register/reg.rst.txt b/unravel/docs/_build/html/_sources/unravel/register/reg.rst.txt new file mode 100644 index 00000000..9044af97 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/register/reg.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.register.reg: + +unravel.register.reg module +=========================== + +.. automodule:: unravel.register.reg + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/register/reg_check.rst.txt b/unravel/docs/_build/html/_sources/unravel/register/reg_check.rst.txt new file mode 100644 index 00000000..21000898 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/register/reg_check.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.register.reg_check: + +unravel.register.reg_check module +================================= + +.. automodule:: unravel.register.reg_check + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/register/reg_check_brain_mask.rst.txt b/unravel/docs/_build/html/_sources/unravel/register/reg_check_brain_mask.rst.txt new file mode 100644 index 00000000..aa48f66c --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/register/reg_check_brain_mask.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.register.reg_check_brain_mask: + +unravel.register.reg_check_brain_mask module +============================================ + +.. automodule:: unravel.register.reg_check_brain_mask + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/register/reg_prep.rst.txt b/unravel/docs/_build/html/_sources/unravel/register/reg_prep.rst.txt new file mode 100644 index 00000000..8ca983ef --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/register/reg_prep.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.register.reg_prep: + +unravel.register.reg_prep module +================================ + +.. automodule:: unravel.register.reg_prep + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/register/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/register/toc.rst.txt new file mode 100644 index 00000000..339402d8 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/register/toc.rst.txt @@ -0,0 +1,17 @@ +unravel.register package +======================== + +.. toctree:: + :maxdepth: 2 + + reg_prep + reg + affine_initializer + reg_check + reg_check_brain_mask + +.. automodule:: unravel.register + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/segment/brain_mask.rst.txt b/unravel/docs/_build/html/_sources/unravel/segment/brain_mask.rst.txt new file mode 100644 index 00000000..56e49617 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/segment/brain_mask.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.segment.brain_mask: + +unravel.segment.brain_mask module +================================= + +.. automodule:: unravel.segment.brain_mask + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/segment/copy_tifs.rst.txt b/unravel/docs/_build/html/_sources/unravel/segment/copy_tifs.rst.txt new file mode 100644 index 00000000..eddeba02 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/segment/copy_tifs.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.segment.copy_tifs: + +unravel.segment.copy_tifs module +================================ + +.. automodule:: unravel.segment.copy_tifs + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/segment/ilastik_pixel_classification.rst.txt b/unravel/docs/_build/html/_sources/unravel/segment/ilastik_pixel_classification.rst.txt new file mode 100644 index 00000000..5439fd29 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/segment/ilastik_pixel_classification.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.segment.ilastik_pixel_classification: + +unravel.segment.ilastik_pixel_classification module +=================================================== + +.. automodule:: unravel.segment.ilastik_pixel_classification + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/segment/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/segment/toc.rst.txt new file mode 100644 index 00000000..6f5e95ec --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/segment/toc.rst.txt @@ -0,0 +1,15 @@ +unravel.segment package +======================= + +.. toctree:: + :maxdepth: 2 + + brain_mask + copy_tifs + ilastik_pixel_classification + +.. automodule:: unravel.segment + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/toc.rst.txt new file mode 100644 index 00000000..8cf22779 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/toc.rst.txt @@ -0,0 +1,30 @@ + +unravel package +=============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 3 + + unravel_commands + register/toc + warp/toc + segment/toc + voxel_stats/toc + cluster_stats/toc + region_stats/toc + core/toc + image_io/toc + image_tools/toc + utilities/toc + +Module contents +--------------- + +.. automodule:: unravel + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/unravel_commands.rst.txt b/unravel/docs/_build/html/_sources/unravel/unravel_commands.rst.txt new file mode 100644 index 00000000..c6f40300 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/unravel_commands.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.unravel_commands: + +unravel.unravel_commands module +================================= + +.. automodule:: unravel.unravel_commands + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/aggregate_files_from_sample_dirs.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/aggregate_files_from_sample_dirs.rst.txt new file mode 100644 index 00000000..76baf9b1 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/aggregate_files_from_sample_dirs.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.utilities.aggregate_files_from_sample_dirs: + +unravel.utilities.aggregate_files_from_sample_dirs module +========================================================= + +.. automodule:: unravel.utilities.aggregate_files_from_sample_dirs + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/aggregate_files_w_recursive_search.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/aggregate_files_w_recursive_search.rst.txt new file mode 100644 index 00000000..c58e88c5 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/aggregate_files_w_recursive_search.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.utilities.aggregate_files_w_recursive_search: + +unravel.utilities.aggregate_files_w_recursive_search module +=========================================================== + +.. automodule:: unravel.utilities.aggregate_files_w_recursive_search + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/clean_tif_dirs.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/clean_tif_dirs.rst.txt new file mode 100644 index 00000000..4bfc35ce --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/clean_tif_dirs.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.utilities.clean_tif_dirs: + +unravel.utilities.clean_tif_dirs module +======================================= + +.. automodule:: unravel.utilities.clean_tif_dirs + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/prepend_conditions.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/prepend_conditions.rst.txt new file mode 100644 index 00000000..9f01c7c1 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/prepend_conditions.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.utilities.prepend_conditions: + +unravel.utilities.prepend_conditions module +=========================================== + +.. automodule:: unravel.utilities.prepend_conditions + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/rename.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/rename.rst.txt new file mode 100644 index 00000000..15c281c2 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/rename.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.utilities.rename: + +unravel.utilities.rename module +=============================== + +.. automodule:: unravel.utilities.rename + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/toc.rst.txt new file mode 100644 index 00000000..e4f1a6ad --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/toc.rst.txt @@ -0,0 +1,18 @@ +unravel.utilities package +========================= + +.. toctree:: + :maxdepth: 2 + + aggregate_files_from_sample_dirs + aggregate_files_w_recursive_search + clean_tif_dirs + prepend_conditions + rename + toggle_samples + +.. automodule:: unravel.utilities + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/utilities/toggle_samples.rst.txt b/unravel/docs/_build/html/_sources/unravel/utilities/toggle_samples.rst.txt new file mode 100644 index 00000000..3c24eac9 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/utilities/toggle_samples.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.utilities.toggle_samples: + +unravel.utilities.toggle_samples module +======================================= + +.. automodule:: unravel.utilities.toggle_samples + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/apply_mask.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/apply_mask.rst.txt new file mode 100644 index 00000000..996be1f2 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/apply_mask.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.apply_mask: + +unravel.voxel_stats.apply_mask module +===================================== + +.. automodule:: unravel.voxel_stats.apply_mask + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/hemi_to_LR_avg.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/hemi_to_LR_avg.rst.txt new file mode 100644 index 00000000..c63c93c9 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/hemi_to_LR_avg.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.hemi_to_LR_avg: + +unravel.voxel_stats.hemi_to_LR_avg module +========================================= + +.. automodule:: unravel.voxel_stats.hemi_to_LR_avg + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/mirror.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/mirror.rst.txt new file mode 100644 index 00000000..eda2f9f4 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/mirror.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.mirror: + +unravel.voxel_stats.mirror module +===================================== + +.. automodule:: unravel.voxel_stats.mirror + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/IF_outliers.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/IF_outliers.rst.txt new file mode 100644 index 00000000..f0ac22d1 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/IF_outliers.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.other.IF_outliers: + +unravel.voxel_stats.other.IF_outliers module +================================================ + +.. automodule:: unravel.voxel_stats.other.IF_outliers + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/r_to_p.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/r_to_p.rst.txt new file mode 100644 index 00000000..8ca33592 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/r_to_p.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.other.r_to_p: + +unravel.voxel_stats.other.r_to_p module +=========================================== + +.. automodule:: unravel.voxel_stats.other.r_to_p + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/toc.rst.txt new file mode 100644 index 00000000..6a735b1a --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/other/toc.rst.txt @@ -0,0 +1,14 @@ +unravel.voxel_stats.other package +================================= + +.. toctree:: + :maxdepth: 2 + + IF_outliers + r_to_p + +.. automodule:: unravel.voxel_stats.other + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/toc.rst.txt new file mode 100644 index 00000000..19d6a7b5 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/toc.rst.txt @@ -0,0 +1,20 @@ +unravel.voxel_stats package +=========================== + +.. toctree:: + :maxdepth: 3 + + apply_mask + vstats_prep + z_score + whole_to_LR_avg + hemi_to_LR_avg + vstats + mirror + other/toc + +.. automodule:: unravel.voxel_stats + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/vstats.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/vstats.rst.txt new file mode 100644 index 00000000..2655e705 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/vstats.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.vstats: + +unravel.voxel_stats.vstats module +================================= + +.. automodule:: unravel.voxel_stats.vstats + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/vstats_prep.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/vstats_prep.rst.txt new file mode 100644 index 00000000..59906201 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/vstats_prep.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.vstats_prep: + +unravel.voxel_stats.vstats_prep module +========================================== + +.. automodule:: unravel.voxel_stats.vstats_prep + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/whole_to_LR_avg.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/whole_to_LR_avg.rst.txt new file mode 100644 index 00000000..0228c525 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/whole_to_LR_avg.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.whole_to_LR_avg: + +unravel.voxel_stats.whole_to_LR_avg module +========================================== + +.. automodule:: unravel.voxel_stats.whole_to_LR_avg + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/voxel_stats/z_score.rst.txt b/unravel/docs/_build/html/_sources/unravel/voxel_stats/z_score.rst.txt new file mode 100644 index 00000000..c185ae06 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/voxel_stats/z_score.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.voxel_stats.z_score: + +unravel.voxel_stats.z_score module +================================== + +.. automodule:: unravel.voxel_stats.z_score + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/warp/to_atlas.rst.txt b/unravel/docs/_build/html/_sources/unravel/warp/to_atlas.rst.txt new file mode 100644 index 00000000..f1ab1807 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/warp/to_atlas.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.warp.to_atlas: + +unravel.warp.to_atlas module +============================ + +.. automodule:: unravel.warp.to_atlas + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/warp/to_native.rst.txt b/unravel/docs/_build/html/_sources/unravel/warp/to_native.rst.txt new file mode 100644 index 00000000..ef9f87e9 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/warp/to_native.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.warp.to_native: + +unravel.warp.to_native module +============================= + +.. automodule:: unravel.warp.to_native + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sources/unravel/warp/toc.rst.txt b/unravel/docs/_build/html/_sources/unravel/warp/toc.rst.txt new file mode 100644 index 00000000..4fdafd46 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/warp/toc.rst.txt @@ -0,0 +1,15 @@ +unravel.warp package +==================== + +.. toctree:: + :maxdepth: 2 + + to_atlas + to_native + warp + +.. automodule:: unravel.warp + :members: + :undoc-members: + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/unravel/docs/_build/html/_sources/unravel/warp/warp.rst.txt b/unravel/docs/_build/html/_sources/unravel/warp/warp.rst.txt new file mode 100644 index 00000000..c9e7d690 --- /dev/null +++ b/unravel/docs/_build/html/_sources/unravel/warp/warp.rst.txt @@ -0,0 +1,9 @@ +.. _unravel.warp.warp: + +unravel.warp.warp module +======================== + +.. automodule:: unravel.warp.warp + :members: + :undoc-members: + :show-inheritance: diff --git a/unravel/docs/_build/html/_sphinx_design_static/design-tabs.js b/unravel/docs/_build/html/_sphinx_design_static/design-tabs.js new file mode 100644 index 00000000..b25bd6a4 --- /dev/null +++ b/unravel/docs/_build/html/_sphinx_design_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/unravel/docs/_build/html/_sphinx_design_static/sphinx-design.min.css b/unravel/docs/_build/html/_sphinx_design_static/sphinx-design.min.css new file mode 100644 index 00000000..a325746f --- /dev/null +++ b/unravel/docs/_build/html/_sphinx_design_static/sphinx-design.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative;font-size:var(--sd-fontsize-dropdown)}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary.sd-summary-title{padding:.5em 1em;font-size:var(--sd-fontsize-dropdown-title);font-weight:var(--sd-fontweight-dropdown-title);user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;list-style:none;display:inline-flex;justify-content:space-between}details.sd-dropdown summary.sd-summary-title::-webkit-details-marker{display:none}details.sd-dropdown summary.sd-summary-title:focus{outline:none}details.sd-dropdown summary.sd-summary-title .sd-summary-icon{margin-right:.6em;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary.sd-summary-title .sd-summary-text{flex-grow:1;line-height:1.5;padding-right:.5rem}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker{pointer-events:none;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker svg{opacity:.6}details.sd-dropdown summary.sd-summary-title:hover .sd-summary-state-marker svg{opacity:1;transform:scale(1.1)}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown .sd-summary-chevron-right{transition:.25s}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-right{transform:rotate(90deg)}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-down{transform:rotate(180deg)}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-bg: rgba(0, 113, 188, 0.2);--sd-color-secondary-bg: rgba(108, 117, 125, 0.2);--sd-color-success-bg: rgba(40, 167, 69, 0.2);--sd-color-info-bg: rgba(23, 162, 184, 0.2);--sd-color-warning-bg: rgba(240, 179, 126, 0.2);--sd-color-danger-bg: rgba(220, 53, 69, 0.2);--sd-color-light-bg: rgba(248, 249, 250, 0.2);--sd-color-muted-bg: rgba(108, 117, 125, 0.2);--sd-color-dark-bg: rgba(33, 37, 41, 0.2);--sd-color-black-bg: rgba(0, 0, 0, 0.2);--sd-color-white-bg: rgba(255, 255, 255, 0.2);--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem;--sd-fontsize-dropdown: inherit;--sd-fontsize-dropdown-title: 1rem;--sd-fontweight-dropdown-title: 700} diff --git a/unravel/docs/_build/html/_static/Heifets_lab_logo.png b/unravel/docs/_build/html/_static/Heifets_lab_logo.png new file mode 100644 index 00000000..e4b6fb29 Binary files /dev/null and b/unravel/docs/_build/html/_static/Heifets_lab_logo.png differ diff --git a/unravel/docs/_build/html/_static/UNRAVEL_logo.png b/unravel/docs/_build/html/_static/UNRAVEL_logo.png new file mode 100644 index 00000000..5488bb8e Binary files /dev/null and b/unravel/docs/_build/html/_static/UNRAVEL_logo.png differ diff --git a/unravel/docs/_build/html/_static/UNRAVEL_visualizer.png b/unravel/docs/_build/html/_static/UNRAVEL_visualizer.png new file mode 100644 index 00000000..6fb1150f Binary files /dev/null and b/unravel/docs/_build/html/_static/UNRAVEL_visualizer.png differ diff --git a/unravel/docs/_build/html/_static/basic.css b/unravel/docs/_build/html/_static/basic.css new file mode 100644 index 00000000..2af6139e --- /dev/null +++ b/unravel/docs/_build/html/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/unravel/docs/_build/html/_static/batch_stitching_1.JPG b/unravel/docs/_build/html/_static/batch_stitching_1.JPG new file mode 100644 index 00000000..bcfb50bf Binary files /dev/null and b/unravel/docs/_build/html/_static/batch_stitching_1.JPG differ diff --git a/unravel/docs/_build/html/_static/batch_stitching_2.JPG b/unravel/docs/_build/html/_static/batch_stitching_2.JPG new file mode 100644 index 00000000..66cdf0d3 Binary files /dev/null and b/unravel/docs/_build/html/_static/batch_stitching_2.JPG differ diff --git a/unravel/docs/_build/html/_static/custom.css b/unravel/docs/_build/html/_static/custom.css new file mode 100644 index 00000000..5d741f12 --- /dev/null +++ b/unravel/docs/_build/html/_static/custom.css @@ -0,0 +1,21 @@ +img { + background-color: transparent; +} + +#unravel-visualizer { + background-color: transparent !important; + filter: none !important; + opacity: 1 !important; +} + +#unravel-logo { + margin-bottom: 30px; /* Adjust the value to create the desired spacing */ + background-color: transparent !important; +} + +#heifets-logo { + background-color: transparent !important; + width: 500px; + display: block; + margin: 0 auto; /* Center the image */ +} \ No newline at end of file diff --git a/unravel/docs/_build/html/_static/design-tabs.js b/unravel/docs/_build/html/_static/design-tabs.js new file mode 100644 index 00000000..b25bd6a4 --- /dev/null +++ b/unravel/docs/_build/html/_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/unravel/docs/_build/html/_static/doctools.js b/unravel/docs/_build/html/_static/doctools.js new file mode 100644 index 00000000..4d67807d --- /dev/null +++ b/unravel/docs/_build/html/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/unravel/docs/_build/html/_static/documentation_options.js b/unravel/docs/_build/html/_static/documentation_options.js new file mode 100644 index 00000000..89435bb4 --- /dev/null +++ b/unravel/docs/_build/html/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '1.0.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/unravel/docs/_build/html/_static/favicon.png b/unravel/docs/_build/html/_static/favicon.png new file mode 100644 index 00000000..0ef6390b Binary files /dev/null and b/unravel/docs/_build/html/_static/favicon.png differ diff --git a/unravel/docs/_build/html/_static/favicon_.png b/unravel/docs/_build/html/_static/favicon_.png new file mode 100644 index 00000000..5663803e Binary files /dev/null and b/unravel/docs/_build/html/_static/favicon_.png differ diff --git a/unravel/docs/_build/html/_static/file.png b/unravel/docs/_build/html/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/unravel/docs/_build/html/_static/file.png differ diff --git a/unravel/docs/_build/html/_static/language_data.js b/unravel/docs/_build/html/_static/language_data.js new file mode 100644 index 00000000..367b8ed8 --- /dev/null +++ b/unravel/docs/_build/html/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/unravel/docs/_build/html/_static/minus.png b/unravel/docs/_build/html/_static/minus.png new file mode 100644 index 00000000..d96755fd Binary files /dev/null and b/unravel/docs/_build/html/_static/minus.png differ diff --git a/unravel/docs/_build/html/_static/plus.png b/unravel/docs/_build/html/_static/plus.png new file mode 100644 index 00000000..7107cec9 Binary files /dev/null and b/unravel/docs/_build/html/_static/plus.png differ diff --git a/unravel/docs/_build/html/_static/psilocybin_up_sunburst.mp4 b/unravel/docs/_build/html/_static/psilocybin_up_sunburst.mp4 new file mode 100644 index 00000000..eadf50e4 Binary files /dev/null and b/unravel/docs/_build/html/_static/psilocybin_up_sunburst.mp4 differ diff --git a/unravel/docs/_build/html/_static/psilocybin_up_valid_clusters.mp4 b/unravel/docs/_build/html/_static/psilocybin_up_valid_clusters.mp4 new file mode 100644 index 00000000..9d56239d Binary files /dev/null and b/unravel/docs/_build/html/_static/psilocybin_up_valid_clusters.mp4 differ diff --git a/unravel/docs/_build/html/_static/pygments.css b/unravel/docs/_build/html/_static/pygments.css new file mode 100644 index 00000000..012e6a00 --- /dev/null +++ b/unravel/docs/_build/html/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #fae4c2 } +html[data-theme="light"] .highlight { background: #fefefe; color: #080808 } +html[data-theme="light"] .highlight .c { color: #515151 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #a12236 } /* Error */ +html[data-theme="light"] .highlight .k { color: #6730c5 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #7f4707 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #080808 } /* Name */ +html[data-theme="light"] .highlight .o { color: #00622f } /* Operator */ +html[data-theme="light"] .highlight .p { color: #080808 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #515151 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #515151 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #515151 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #515151 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #515151 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #005b82 } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #005b82 } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #005b82 } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #6730c5 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #6730c5 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #6730c5 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #6730c5 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #6730c5 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #7f4707 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #7f4707 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #7f4707 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #00622f } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #912583 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #7f4707 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #005b82 } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #005b82 } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #7f4707 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #00622f } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #6730c5 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #005b82 } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #7f4707 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #080808 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #080808 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #005b82 } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #005b82 } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #a12236 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #6730c5 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #080808 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #080808 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #7f4707 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #7f4707 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #7f4707 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #7f4707 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #7f4707 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #00622f } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #00622f } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #00622f } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #00622f } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #00622f } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #00622f } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #00622f } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #00622f } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #00622f } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #00622f } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #a12236 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #00622f } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #005b82 } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #7f4707 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #005b82 } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #a12236 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #a12236 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #a12236 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #7f4707 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #7f4707 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/unravel/docs/_build/html/_static/scripts/bootstrap.js b/unravel/docs/_build/html/_static/scripts/bootstrap.js new file mode 100644 index 00000000..c8178deb --- /dev/null +++ b/unravel/docs/_build/html/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>li,Collapse:()=>Ei,Dropdown:()=>Ki,Modal:()=>Ln,Offcanvas:()=>Kn,Popover:()=>bs,ScrollSpy:()=>Ls,Tab:()=>Js,Toast:()=>po,Tooltip:()=>fs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every((function(t){return t}))){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find((function(e){var i=D.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some((function(e){return t[e]>=0}))}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Tt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),T=!A,C=Q(E),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,W="y"===C?"height":"width",z=x[C],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=T?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=T?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[C])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[C]=ht,M[C]=ht-z}if(u){var dt,ut="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),jt=t=>{t.dispatchEvent(new Event(Pt))},Ft=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Ft(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Bt=t=>{if(!Ft(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),zt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?zt(t.parentNode):null},Rt=()=>{},qt=t=>{t.offsetHeight},Vt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Yt=[],Kt=()=>"rtl"===document.documentElement.dir,Qt=t=>{var e;e=()=>{const e=Vt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Yt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Yt)t()})),Yt.push(e)):e()},Xt=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Ut=(t,e,i=!0)=>{if(!i)return void Xt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Xt(t))};e.addEventListener(Pt,o),setTimeout((()=>{s||jt(e)}),n)},Gt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Jt=/[^.]*(?=\..*)\.|.*/,Zt=/\..*/,te=/::\d+$/,ee={};let ie=1;const ne={mouseenter:"mouseover",mouseleave:"mouseout"},se=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function oe(t,e){return e&&`${e}::${ie++}`||t.uidEvent||ie++}function re(t){const e=oe(t);return t.uidEvent=e,ee[e]=ee[e]||{},ee[e]}function ae(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function le(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=ue(t);return se.has(o)||(o=t),[n,s,o]}function ce(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=le(e,i,n);if(e in ne){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=re(t),c=l[a]||(l[a]={}),h=ae(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=oe(r,e.replace(Jt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return pe(s,{delegateTarget:r}),n.oneOff&&fe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return pe(n,{delegateTarget:t}),i.oneOff&&fe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function he(t,e,i,n,s){const o=ae(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function de(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&he(t,e,i,r.callable,r.delegationSelector)}function ue(t){return t=t.replace(Zt,""),ne[t]||t}const fe={on(t,e,i,n){ce(t,e,i,n,!1)},one(t,e,i,n){ce(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=le(e,i,n),a=r!==e,l=re(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))de(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(te,"");a&&!e.includes(s)||he(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;he(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Vt();let s=null,o=!0,r=!0,a=!1;e!==ue(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=pe(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function pe(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ge(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const _e={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ge(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ge(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${ge(e)}`))};class be{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ft(e)?_e.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ft(e)?_e.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],o=Ft(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${o}" but expected type "${s}".`)}var i}}class ve extends be{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),fe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Ut(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const ye=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>Mt(t))).join(","):null},we={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))},getSelectorFromElement(t){const e=ye(t);return e&&we.findOne(e)?e:null},getElementFromSelector(t){const e=ye(t);return e?we.findOne(e):null},getMultipleElementsFromSelector(t){const e=ye(t);return e?we.find(e):[]}},Ee=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;fe.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=we.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Te=`close${Ae}`,Ce=`closed${Ae}`;class Oe extends ve{static get NAME(){return"alert"}close(){if(fe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),fe.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Oe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}Ee(Oe,"close"),Qt(Oe);const xe='[data-bs-toggle="button"]';class ke extends ve{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=ke.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}fe.on(document,"click.bs.button.data-api",xe,(t=>{t.preventDefault();const e=t.target.closest(xe);ke.getOrCreateInstance(e).toggle()})),Qt(ke);const Le=".bs.swipe",Se=`touchstart${Le}`,De=`touchmove${Le}`,$e=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},Me={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class je extends be{constructor(t,e){super(),this._element=t,t&&je.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return Me}static get NAME(){return"swipe"}dispose(){fe.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Xt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Xt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(fe.on(this._element,Ie,(t=>this._start(t))),fe.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(fe.on(this._element,Se,(t=>this._start(t))),fe.on(this._element,De,(t=>this._move(t))),fe.on(this._element,$e,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Fe=".bs.carousel",He=".data-api",Be="ArrowLeft",We="ArrowRight",ze="next",Re="prev",qe="left",Ve="right",Ye=`slide${Fe}`,Ke=`slid${Fe}`,Qe=`keydown${Fe}`,Xe=`mouseenter${Fe}`,Ue=`mouseleave${Fe}`,Ge=`dragstart${Fe}`,Je=`load${Fe}${He}`,Ze=`click${Fe}${He}`,ti="carousel",ei="active",ii=".active",ni=".carousel-item",si=ii+ni,oi={[Be]:Ve,[We]:qe},ri={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ai={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class li extends ve{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=we.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ti&&this.cycle()}static get Default(){return ri}static get DefaultType(){return ai}static get NAME(){return"carousel"}next(){this._slide(ze)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(Re)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?fe.one(this._element,Ke,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void fe.one(this._element,Ke,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ze:Re;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&fe.on(this._element,Qe,(t=>this._keydown(t))),"hover"===this._config.pause&&(fe.on(this._element,Xe,(()=>this.pause())),fe.on(this._element,Ue,(()=>this._maybeEnableCycle()))),this._config.touch&&je.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of we.find(".carousel-item img",this._element))fe.on(t,Ge,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(qe)),rightCallback:()=>this._slide(this._directionToOrder(Ve)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new je(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=oi[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=we.findOne(ii,this._indicatorsElement);e.classList.remove(ei),e.removeAttribute("aria-current");const i=we.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(ei),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ze,s=e||Gt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>fe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Ye).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(ei),i.classList.remove(ei,c,l),this._isSliding=!1,r(Ke)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return we.findOne(si,this._element)}_getItems(){return we.find(ni,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Kt()?t===qe?Re:ze:t===qe?ze:Re}_orderToDirection(t){return Kt()?t===Re?qe:Ve:t===Re?Ve:qe}static jQueryInterface(t){return this.each((function(){const e=li.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}fe.on(document,Ze,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=we.getElementFromSelector(this);if(!e||!e.classList.contains(ti))return;t.preventDefault();const i=li.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===_e.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),fe.on(window,Je,(()=>{const t=we.find('[data-bs-ride="carousel"]');for(const e of t)li.getOrCreateInstance(e)})),Qt(li);const ci=".bs.collapse",hi=`show${ci}`,di=`shown${ci}`,ui=`hide${ci}`,fi=`hidden${ci}`,pi=`click${ci}.data-api`,mi="show",gi="collapse",_i="collapsing",bi=`:scope .${gi} .${gi}`,vi='[data-bs-toggle="collapse"]',yi={parent:null,toggle:!0},wi={parent:"(null|element)",toggle:"boolean"};class Ei extends ve{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=we.find(vi);for(const t of i){const e=we.getSelectorFromElement(t),i=we.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return yi}static get DefaultType(){return wi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Ei.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(fe.trigger(this._element,hi).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(gi),this._element.classList.add(_i),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi,mi),this._element.style[e]="",fe.trigger(this._element,di)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(fe.trigger(this._element,ui).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(_i),this._element.classList.remove(gi,mi);for(const t of this._triggerArray){const e=we.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi),fe.trigger(this._element,fi)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(mi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(vi);for(const e of t){const t=we.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=we.find(bi,this._config.parent);return we.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Ei.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}fe.on(document,pi,vi,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of we.getMultipleElementsFromSelector(this))Ei.getOrCreateInstance(t,{toggle:!1}).toggle()})),Qt(Ei);const Ai="dropdown",Ti=".bs.dropdown",Ci=".data-api",Oi="ArrowUp",xi="ArrowDown",ki=`hide${Ti}`,Li=`hidden${Ti}`,Si=`show${Ti}`,Di=`shown${Ti}`,$i=`click${Ti}${Ci}`,Ii=`keydown${Ti}${Ci}`,Ni=`keyup${Ti}${Ci}`,Pi="show",Mi='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ji=`${Mi}.${Pi}`,Fi=".dropdown-menu",Hi=Kt()?"top-end":"top-start",Bi=Kt()?"top-start":"top-end",Wi=Kt()?"bottom-end":"bottom-start",zi=Kt()?"bottom-start":"bottom-end",Ri=Kt()?"left-start":"right-start",qi=Kt()?"right-start":"left-start",Vi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Yi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Ki extends ve{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=we.next(this._element,Fi)[0]||we.prev(this._element,Fi)[0]||we.findOne(Fi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Vi}static get DefaultType(){return Yi}static get NAME(){return Ai}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!fe.trigger(this._element,Si,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Pi),this._element.classList.add(Pi),fe.trigger(this._element,Di,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!fe.trigger(this._element,ki,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Pi),this._element.classList.remove(Pi),this._element.setAttribute("aria-expanded","false"),_e.removeDataAttribute(this._menu,"popper"),fe.trigger(this._element,Li,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ft(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ai.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Ft(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Pi)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Ri;if(t.classList.contains("dropstart"))return qi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Bi:Hi:e?zi:Wi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(_e.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Xt(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=we.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Gt(i,e,t===xi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ki.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=we.find(ji);for(const i of e){const e=Ki.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Oi,xi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Mi)?this:we.prev(this,Mi)[0]||we.next(this,Mi)[0]||we.findOne(Mi,t.delegateTarget.parentNode),o=Ki.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}fe.on(document,Ii,Mi,Ki.dataApiKeydownHandler),fe.on(document,Ii,Fi,Ki.dataApiKeydownHandler),fe.on(document,$i,Ki.clearMenus),fe.on(document,Ni,Ki.clearMenus),fe.on(document,$i,Mi,(function(t){t.preventDefault(),Ki.getOrCreateInstance(this).toggle()})),Qt(Ki);const Qi="backdrop",Xi="show",Ui=`mousedown.bs.${Qi}`,Gi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ji={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zi extends be{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return Qi}show(t){if(!this._config.isVisible)return void Xt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Xi),this._emulateAnimation((()=>{Xt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Xi),this._emulateAnimation((()=>{this.dispose(),Xt(t)}))):Xt(t)}dispose(){this._isAppended&&(fe.off(this._element,Ui),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),fe.on(t,Ui,(()=>{Xt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Ut(t,this._getElement(),this._config.isAnimated)}}const tn=".bs.focustrap",en=`focusin${tn}`,nn=`keydown.tab${tn}`,sn="backward",on={autofocus:!0,trapElement:null},rn={autofocus:"boolean",trapElement:"element"};class an extends be{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return on}static get DefaultType(){return rn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),fe.off(document,tn),fe.on(document,en,(t=>this._handleFocusin(t))),fe.on(document,nn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,fe.off(document,tn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=we.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===sn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?sn:"forward")}}const ln=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",cn=".sticky-top",hn="padding-right",dn="margin-right";class un{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hn,(e=>e+t)),this._setElementAttributes(ln,hn,(e=>e+t)),this._setElementAttributes(cn,dn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hn),this._resetElementAttributes(ln,hn),this._resetElementAttributes(cn,dn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&_e.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=_e.getDataAttribute(t,e);null!==i?(_e.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Ft(t))e(t);else for(const i of we.find(t,this._element))e(i)}}const fn=".bs.modal",pn=`hide${fn}`,mn=`hidePrevented${fn}`,gn=`hidden${fn}`,_n=`show${fn}`,bn=`shown${fn}`,vn=`resize${fn}`,yn=`click.dismiss${fn}`,wn=`mousedown.dismiss${fn}`,En=`keydown.dismiss${fn}`,An=`click${fn}.data-api`,Tn="modal-open",Cn="show",On="modal-static",xn={backdrop:!0,focus:!0,keyboard:!0},kn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ln extends ve{constructor(t,e){super(t,e),this._dialog=we.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new un,this._addEventListeners()}static get Default(){return xn}static get DefaultType(){return kn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||fe.trigger(this._element,_n,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(fe.trigger(this._element,pn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Cn),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){fe.off(window,fn),fe.off(this._dialog,fn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new an({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=we.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(Cn),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,fe.trigger(this._element,bn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){fe.on(this._element,En,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),fe.on(window,vn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),fe.on(this._element,wn,(t=>{fe.one(this._element,yn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),fe.trigger(this._element,gn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(fe.trigger(this._element,mn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(On)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(On),this._queueCallback((()=>{this._element.classList.remove(On),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Kt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Kt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}fe.on(document,An,'[data-bs-toggle="modal"]',(function(t){const e=we.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),fe.one(e,_n,(t=>{t.defaultPrevented||fe.one(e,gn,(()=>{Bt(this)&&this.focus()}))}));const i=we.findOne(".modal.show");i&&Ln.getInstance(i).hide(),Ln.getOrCreateInstance(e).toggle(this)})),Ee(Ln),Qt(Ln);const Sn=".bs.offcanvas",Dn=".data-api",$n=`load${Sn}${Dn}`,In="show",Nn="showing",Pn="hiding",Mn=".offcanvas.show",jn=`show${Sn}`,Fn=`shown${Sn}`,Hn=`hide${Sn}`,Bn=`hidePrevented${Sn}`,Wn=`hidden${Sn}`,zn=`resize${Sn}`,Rn=`click${Sn}${Dn}`,qn=`keydown.dismiss${Sn}`,Vn={backdrop:!0,keyboard:!0,scroll:!1},Yn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Kn extends ve{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Vn}static get DefaultType(){return Yn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||fe.trigger(this._element,jn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new un).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Nn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(In),this._element.classList.remove(Nn),fe.trigger(this._element,Fn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(fe.trigger(this._element,Hn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Pn),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(In,Pn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new un).reset(),fe.trigger(this._element,Wn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Zi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():fe.trigger(this._element,Bn)}:null})}_initializeFocusTrap(){return new an({trapElement:this._element})}_addEventListeners(){fe.on(this._element,qn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():fe.trigger(this._element,Bn))}))}static jQueryInterface(t){return this.each((function(){const e=Kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}fe.on(document,Rn,'[data-bs-toggle="offcanvas"]',(function(t){const e=we.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;fe.one(e,Wn,(()=>{Bt(this)&&this.focus()}));const i=we.findOne(Mn);i&&i!==e&&Kn.getInstance(i).hide(),Kn.getOrCreateInstance(e).toggle(this)})),fe.on(window,$n,(()=>{for(const t of we.find(Mn))Kn.getOrCreateInstance(t).show()})),fe.on(window,zn,(()=>{for(const t of we.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Kn.getOrCreateInstance(t).hide()})),Ee(Kn),Qt(Kn);const Qn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Un=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Gn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Xn.has(i)||Boolean(Un.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Jn={allowList:Qn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Zn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ts={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends be{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Jn}static get DefaultType(){return Zn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ts)}_setContent(t,e,i){const n=we.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ft(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Gn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Xt(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const is=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",ss="show",os=".tooltip-inner",rs=".modal",as="hide.bs.modal",ls="hover",cs="focus",hs={AUTO:"auto",TOP:"top",RIGHT:Kt()?"left":"right",BOTTOM:"bottom",LEFT:Kt()?"right":"left"},ds={allowList:Qn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},us={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class fs extends ve{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),fe.off(this._element.closest(rs),as,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=fe.trigger(this._element,this.constructor.eventName("show")),e=(zt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),fe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._queueCallback((()=>{fe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!fe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._activeTrigger.click=!1,this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),fe.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ns,ss),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ns),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new es({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[os]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(ss)}_createPopper(t){const e=Xt(this._config.placement,[this,t,this._element]),i=hs[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Xt(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Xt(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)fe.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ls?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ls?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");fe.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?cs:ls]=!0,e._enter()})),fe.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?cs:ls]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},fe.on(this._element.closest(rs),as,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=_e.getDataAttributes(this._element);for(const t of Object.keys(e))is.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(fs);const ps=".popover-header",ms=".popover-body",gs={...fs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},_s={...fs.DefaultType,content:"(null|string|element|function)"};class bs extends fs{static get Default(){return gs}static get DefaultType(){return _s}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ps]:this._getTitle(),[ms]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=bs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(bs);const vs=".bs.scrollspy",ys=`activate${vs}`,ws=`click${vs}`,Es=`load${vs}.data-api`,As="active",Ts="[href]",Cs=".nav-link",Os=`${Cs}, .nav-item > ${Cs}, .list-group-item`,xs={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ks={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ls extends ve{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xs}static get DefaultType(){return ks}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(fe.off(this._config.target,ws),fe.on(this._config.target,ws,Ts,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=we.find(Ts,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=we.findOne(decodeURI(e.hash),this._element);Bt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(As),this._activateParents(t),fe.trigger(this._element,ys,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))we.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(As);else for(const e of we.parents(t,".nav, .list-group"))for(const t of we.prev(e,Os))t.classList.add(As)}_clearActiveClass(t){t.classList.remove(As);const e=we.find(`${Ts}.${As}`,t);for(const t of e)t.classList.remove(As)}static jQueryInterface(t){return this.each((function(){const e=Ls.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(window,Es,(()=>{for(const t of we.find('[data-bs-spy="scroll"]'))Ls.getOrCreateInstance(t)})),Qt(Ls);const Ss=".bs.tab",Ds=`hide${Ss}`,$s=`hidden${Ss}`,Is=`show${Ss}`,Ns=`shown${Ss}`,Ps=`click${Ss}`,Ms=`keydown${Ss}`,js=`load${Ss}`,Fs="ArrowLeft",Hs="ArrowRight",Bs="ArrowUp",Ws="ArrowDown",zs="Home",Rs="End",qs="active",Vs="fade",Ys="show",Ks=".dropdown-toggle",Qs=`:not(${Ks})`,Xs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Us=`.nav-link${Qs}, .list-group-item${Qs}, [role="tab"]${Qs}, ${Xs}`,Gs=`.${qs}[data-bs-toggle="tab"], .${qs}[data-bs-toggle="pill"], .${qs}[data-bs-toggle="list"]`;class Js extends ve{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),fe.on(this._element,Ms,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?fe.trigger(e,Ds,{relatedTarget:t}):null;fe.trigger(t,Is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(qs),this._activate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),fe.trigger(t,Ns,{relatedTarget:e})):t.classList.add(Ys)}),t,t.classList.contains(Vs)))}_deactivate(t,e){t&&(t.classList.remove(qs),t.blur(),this._deactivate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),fe.trigger(t,$s,{relatedTarget:e})):t.classList.remove(Ys)}),t,t.classList.contains(Vs)))}_keydown(t){if(![Fs,Hs,Bs,Ws,zs,Rs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!Wt(t)));let i;if([zs,Rs].includes(t.key))i=e[t.key===zs?0:e.length-1];else{const n=[Hs,Ws].includes(t.key);i=Gt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Js.getOrCreateInstance(i).show())}_getChildren(){return we.find(Us,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=we.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=we.findOne(t,i);s&&s.classList.toggle(n,e)};n(Ks,qs),n(".dropdown-menu",Ys),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(qs)}_getInnerElement(t){return t.matches(Us)?t:we.findOne(Us,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Js.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(document,Ps,Xs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Js.getOrCreateInstance(this).show()})),fe.on(window,js,(()=>{for(const t of we.find(Gs))Js.getOrCreateInstance(t)})),Qt(Js);const Zs=".bs.toast",to=`mouseover${Zs}`,eo=`mouseout${Zs}`,io=`focusin${Zs}`,no=`focusout${Zs}`,so=`hide${Zs}`,oo=`hidden${Zs}`,ro=`show${Zs}`,ao=`shown${Zs}`,lo="hide",co="show",ho="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},fo={animation:!0,autohide:!0,delay:5e3};class po extends ve{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return fo}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){fe.trigger(this._element,ro).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(lo),qt(this._element),this._element.classList.add(co,ho),this._queueCallback((()=>{this._element.classList.remove(ho),fe.trigger(this._element,ao),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(fe.trigger(this._element,so).defaultPrevented||(this._element.classList.add(ho),this._queueCallback((()=>{this._element.classList.add(lo),this._element.classList.remove(ho,co),fe.trigger(this._element,oo)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(co),super.dispose()}isShown(){return this._element.classList.contains(co)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){fe.on(this._element,to,(t=>this._onInteraction(t,!0))),fe.on(this._element,eo,(t=>this._onInteraction(t,!1))),fe.on(this._element,io,(t=>this._onInteraction(t,!0))),fe.on(this._element,no,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=po.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function mo(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ee(po),Qt(po),mo((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new fs(t,{delay:{show:500,hide:100}})}))})),mo((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),mo((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))})),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/unravel/docs/_build/html/_static/scripts/bootstrap.js.LICENSE.txt b/unravel/docs/_build/html/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 00000000..28755c2c --- /dev/null +++ b/unravel/docs/_build/html/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/unravel/docs/_build/html/_static/scripts/bootstrap.js.map b/unravel/docs/_build/html/_static/scripts/bootstrap.js.map new file mode 100644 index 00000000..e9e81589 --- /dev/null +++ b/unravel/docs/_build/html/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":"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","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.3';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n }\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both