From 1ba0a6c266bf9877369922147c48a2abfa6f306c Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:13:53 -0500 Subject: [PATCH 01/27] Initial commit --- src/kbmod/search/kernels.cu | 62 ++++--- src/kbmod/search/psi_phi_array_utils.h | 33 ---- src/kbmod/search/pydocs/psi_phi_array_docs.h | 95 ++++++---- .../{psi_phi_array.cpp => search_data.cpp} | 163 ++++++++++++------ .../{psi_phi_array_ds.h => search_data_ds.h} | 53 +++--- src/kbmod/search/search_data_utils.h | 39 +++++ src/kbmod/search/stack_search.cpp | 11 +- 7 files changed, 287 insertions(+), 169 deletions(-) delete mode 100644 src/kbmod/search/psi_phi_array_utils.h rename src/kbmod/search/{psi_phi_array.cpp => search_data.cpp} (67%) rename src/kbmod/search/{psi_phi_array_ds.h => search_data_ds.h} (68%) create mode 100644 src/kbmod/search/search_data_utils.h diff --git a/src/kbmod/search/kernels.cu b/src/kbmod/search/kernels.cu index 84b6cf249..2accdb171 100644 --- a/src/kbmod/search/kernels.cu +++ b/src/kbmod/search/kernels.cu @@ -19,29 +19,45 @@ #include "common.h" #include "cuda_errors.h" -#include "psi_phi_array_ds.h" +#include "search_data_ds.h" namespace search { -extern "C" void device_allocate_psi_phi_array(PsiPhiArray *data) { - if (!data->cpu_array_allocated()) throw std::runtime_error("CPU data is not allocated."); - if (data->gpu_array_allocated()) throw std::runtime_error("GPU data is already allocated."); +extern "C" void device_allocate_search_data_arrays(SearchData *data) { + if (!data->cpu_array_allocated() || !data->cpu_time_array_allocated()) { + throw std::runtime_error("CPU data is not allocated."); + } + if (data->gpu_array_allocated() || data->gpu_time_array_allocated()) { + throw std::runtime_error("GPU data is already allocated."); + } + // Allocate space for the psi/phi data. void *device_array_ptr; checkCudaErrors(cudaMalloc((void **)&device_array_ptr, data->get_total_array_size())); checkCudaErrors(cudaMemcpy(device_array_ptr, data->get_cpu_array_ptr(), data->get_total_array_size(), cudaMemcpyHostToDevice)); data->set_gpu_array_ptr(device_array_ptr); + + // Allocate space for the times data. + float *device_times_ptr; + long unsigned time_bytes = data->get_num_times() * sizeof(float); + checkCudaErrors(cudaMalloc((void **)&device_times_ptr, time_bytes)); + checkCudaErrors(cudaMemcpy(device_times_ptr, data->get_cpu_time_array_ptr(), time_bytes, cudaMemcpyHostToDevice)); + data->set_gpu_time_array_ptr(device_times_ptr); } -extern "C" void device_free_psi_phi_array(PsiPhiArray *data) { +extern "C" void device_free_search_data_arrays(SearchData *data) { if (data->gpu_array_allocated()) { checkCudaErrors(cudaFree(data->get_gpu_array_ptr())); data->set_gpu_array_ptr(nullptr); } + if (data->gpu_time_array_allocated()) { + checkCudaErrors(cudaFree(data->get_gpu_time_array_ptr())); + data->set_gpu_time_array_ptr(nullptr); + } } -__forceinline__ __device__ PsiPhi read_encoded_psi_phi(PsiPhiArrayMeta ¶ms, void *psi_phi_vect, int time, +__forceinline__ __device__ PsiPhi read_encoded_psi_phi(SearchDataMeta ¶ms, void *psi_phi_vect, int time, int row, int col) { // Bounds checking. if ((row < 0) || (col < 0) || (row >= params.height) || (col >= params.width)) { @@ -130,7 +146,7 @@ extern "C" __device__ __host__ void SigmaGFilteredIndicesCU(float *values, int n * * Creates a local copy of psi_phi_meta and params in local memory space. */ -__global__ void searchFilterImages(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_vect, float *image_times, +__global__ void searchFilterImages(SearchDataMeta psi_phi_meta, void *psi_phi_vect, float *image_times, SearchParameters params, int num_trajectories, Trajectory *trajectories, Trajectory *results) { // Get the x and y coordinates within the search space. @@ -257,24 +273,25 @@ __global__ void searchFilterImages(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_v } } -extern "C" void deviceSearchFilter(PsiPhiArray &psi_phi_array, float *image_times, SearchParameters params, - int num_trajectories, Trajectory *trj_to_search, int num_results, - Trajectory *best_results) { +extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters params, int num_trajectories, + Trajectory *trj_to_search, int num_results, Trajectory *best_results) { // Allocate Device memory Trajectory *device_tests; - float *device_img_times; Trajectory *device_search_results; // Check the hard coded maximum number of images against the num_images. - int num_images = psi_phi_array.get_num_times(); + int num_images = search_data.get_num_times(); if (num_images > MAX_NUM_IMAGES) { throw std::runtime_error("Number of images exceeds GPU maximum."); } - // Check that the device psi_phi vector has been allocated. - if (psi_phi_array.gpu_array_allocated() == false) { + // Check that the device vectors have already been allocated. + if (search_data.gpu_array_allocated() == false) { throw std::runtime_error("PsiPhi data has not been created."); } + if (search_data.gpu_time_array_allocated() == false) { + throw std::runtime_error("GPU time data has not been created."); + } // Copy trajectories to search if (params.debug) { @@ -285,14 +302,6 @@ extern "C" void deviceSearchFilter(PsiPhiArray &psi_phi_array, float *image_time checkCudaErrors(cudaMemcpy(device_tests, trj_to_search, sizeof(Trajectory) * num_trajectories, cudaMemcpyHostToDevice)); - // Copy the time vector. - if (params.debug) { - printf("Allocating GPU memory for time data using %lu bytes.\n", sizeof(float) * num_images); - } - checkCudaErrors(cudaMalloc((void **)&device_img_times, sizeof(float) * num_images)); - checkCudaErrors( - cudaMemcpy(device_img_times, image_times, sizeof(float) * num_images, cudaMemcpyHostToDevice)); - // Allocate space for the results. if (params.debug) { printf("Allocating GPU memory for %i results using %lu bytes.\n", num_results, sizeof(Trajectory) * num_results); @@ -308,17 +317,16 @@ extern "C" void deviceSearchFilter(PsiPhiArray &psi_phi_array, float *image_time dim3 threads(THREAD_DIM_X, THREAD_DIM_Y); // Launch Search - searchFilterImages<<>>(psi_phi_array.get_meta_data(), psi_phi_array.get_gpu_array_ptr(), - device_img_times, params, num_trajectories, device_tests, - device_search_results); + searchFilterImages<<>>(search_data.get_meta_data(), search_data.get_gpu_array_ptr(), + static_cast(search_data.get_gpu_time_array_ptr()), params, + num_trajectories, device_tests, device_search_results); // Read back results checkCudaErrors(cudaMemcpy(best_results, device_search_results, sizeof(Trajectory) * num_results, cudaMemcpyDeviceToHost)); - // Free the on GPU memory. + // Free the on GPU memory for this specific search. checkCudaErrors(cudaFree(device_search_results)); - checkCudaErrors(cudaFree(device_img_times)); checkCudaErrors(cudaFree(device_tests)); } diff --git a/src/kbmod/search/psi_phi_array_utils.h b/src/kbmod/search/psi_phi_array_utils.h deleted file mode 100644 index c363f65d4..000000000 --- a/src/kbmod/search/psi_phi_array_utils.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * psi_phi_array_utils.h - * - * The utility functions for the psi/phi array. Broken out from the header - * data structure so that it can use packages that won't be imported into the - * CUDA kernel, such as Eigen. - * - * Created on: Dec 8, 2023 - */ - -#ifndef PSI_PHI_ARRAY_UTILS_ -#define PSI_PHI_ARRAY_UTILS_ - -#include -#include -#include -#include - -#include "common.h" -#include "psi_phi_array_ds.h" -#include "raw_image.h" - -namespace search { - -// Compute the min, max, and scale parameter from the a vector of image data. -std::array compute_scale_params_from_image_vect(const std::vector& imgs, int num_bytes); - -void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vector& psi_imgs, - const std::vector& phi_imgs, bool debug = false); - -} /* namespace search */ - -#endif /* PSI_PHI_ARRAY_UTILS_ */ diff --git a/src/kbmod/search/pydocs/psi_phi_array_docs.h b/src/kbmod/search/pydocs/psi_phi_array_docs.h index ffb7b009f..4f668e3ea 100644 --- a/src/kbmod/search/pydocs/psi_phi_array_docs.h +++ b/src/kbmod/search/pydocs/psi_phi_array_docs.h @@ -1,5 +1,5 @@ -#ifndef PSI_PHI_ARRAY_DOCS -#define PSI_PHI_ARRAY_DOCS +#ifndef SEARCH_DATA_DOCS +#define SEARCH_DATA_DOCS namespace pydocs { @@ -14,80 +14,88 @@ static const auto DOC_PsiPhi = R"doc( The phi value at a pixel. )doc"; -static const auto DOC_PsiPhiArray = R"doc( +static const auto DOC_SearchData = R"doc( An encoded array of Psi and Phi values along with their meta data. )doc"; -static const auto DOC_PsiPhiArray_get_num_bytes = R"doc( +static const auto DOC_SearchData_get_num_bytes = R"doc( The target number of bytes to use for encoding the data (1 for uint8, 2 for uint16, or 4 for float32). Might differ from actual number of bytes (block_size). )doc"; -static const auto DOC_PsiPhiArray_get_num_times = R"doc( +static const auto DOC_SearchData_get_num_times = R"doc( The number of times. )doc"; -static const auto DOC_PsiPhiArray_get_width = R"doc( +static const auto DOC_SearchData_get_width = R"doc( The image width. )doc"; -static const auto DOC_PsiPhiArray_get_height = R"doc( +static const auto DOC_SearchData_get_height = R"doc( The image height. )doc"; -static const auto DOC_PsiPhiArray_get_pixels_per_image = R"doc( +static const auto DOC_SearchData_get_pixels_per_image = R"doc( The number of pixels per each image. )doc"; -static const auto DOC_PsiPhiArray_get_num_entries = R"doc( +static const auto DOC_SearchData_get_num_entries = R"doc( The number of array entries. )doc"; -static const auto DOC_PsiPhiArray_get_total_array_size = R"doc( +static const auto DOC_SearchData_get_total_array_size = R"doc( The size of the array in bytes. )doc"; -static const auto DOC_PsiPhiArray_get_block_size = R"doc( +static const auto DOC_SearchData_get_block_size = R"doc( The size of a single entry in bytes. )doc"; -static const auto DOC_PsiPhiArray_get_psi_min_val = R"doc( +static const auto DOC_SearchData_get_psi_min_val = R"doc( The minimum value of psi used in the scaling computations. )doc"; -static const auto DOC_PsiPhiArray_get_psi_max_val = R"doc( +static const auto DOC_SearchData_get_psi_max_val = R"doc( The maximum value of psi used in the scaling computations. )doc"; -static const auto DOC_PsiPhiArray_get_psi_scale = R"doc( +static const auto DOC_SearchData_get_psi_scale = R"doc( The scaling parameter for psi. )doc"; -static const auto DOC_PsiPhiArray_get_phi_min_val = R"doc( +static const auto DOC_SearchData_get_phi_min_val = R"doc( The minimum value of phi used in the scaling computations. )doc"; -static const auto DOC_PsiPhiArray_get_phi_max_val = R"doc( +static const auto DOC_SearchData_get_phi_max_val = R"doc( The maximum value of phi used in the scaling computations. )doc"; -static const auto DOC_PsiPhiArray_get_phi_scale = R"doc( +static const auto DOC_SearchData_get_phi_scale = R"doc( The scaling parameter for phi. )doc"; -static const auto DOC_PsiPhiArray_get_cpu_array_allocated = R"doc( - A Boolean indicating whether the cpu array exists. +static const auto DOC_SearchData_get_cpu_array_allocated = R"doc( + A Boolean indicating whether the cpu data (psi/phi) array exists. )doc"; -static const auto DOC_PsiPhiArray_get_gpu_array_allocated = R"doc( - A Boolean indicating whether the gpu array exists. +static const auto DOC_SearchData_get_gpu_array_allocated = R"doc( + A Boolean indicating whether the gpu data (psi/phi) array exists. )doc"; -static const auto DOC_PsiPhiArray_clear = R"doc( +static const auto DOC_SearchData_get_cpu_time_array_allocated = R"doc( + A Boolean indicating whether the cpu time array exists. + )doc"; + +static const auto DOC_SearchData_get_gpu_time_array_allocated = R"doc( + A Boolean indicating whether the gpu time array exists. + )doc"; + +static const auto DOC_SearchData_clear = R"doc( Clear all data and free the arrays. )doc"; -static const auto DOC_PsiPhiArray_read_psi_phi = R"doc( +static const auto DOC_SearchData_read_psi_phi = R"doc( Read a PsiPhi value from the CPU array. Parameters @@ -105,9 +113,23 @@ static const auto DOC_PsiPhiArray_read_psi_phi = R"doc( The pixel values. )doc"; -static const auto DOC_PsiPhiArray_set_meta_data = R"doc( +static const auto DOC_SearchData_read_time = R"doc( + Read a zeroed time value from the CPU array. + + Parameters + ---------- + time : `int` + The timestep to read. + + Returns + ------- + `float` + The time. + )doc"; + +static const auto DOC_SearchData_set_meta_data = R"doc( Set the meta data for the array. Automatically called by - fill_psi_phi_array(). + fill_search_data(). Parameters ---------- @@ -121,12 +143,12 @@ static const auto DOC_PsiPhiArray_set_meta_data = R"doc( The width of each image in pixels. )doc"; -static const auto DOC_PsiPhiArray_fill_psi_phi_array = R"doc( - Fill the PsiPhiArray from Psi and Phi images. +static const auto DOC_SearchData_fill_search_data = R"doc( + Fill the SearchData from Psi and Phi images. Parameters ---------- - result_data : `PsiPhiArray` + result_data : `SearchData` The location to store the data. num_bytes : `int` The type of encoding to use (1, 2, or 4). @@ -134,8 +156,23 @@ static const auto DOC_PsiPhiArray_fill_psi_phi_array = R"doc( A list of psi images. phi_imgs : `list` A list of phi images. + zeroed_times : `list` + A list of floating point times starting at zero. + )doc"; + +static const auto DOC_SearchData_fill_search_data_from_image_stack = R"doc( + Fill the SearchData an ImageStack. + + Parameters + ---------- + result_data : `SearchData` + The location to store the data. + num_bytes : `int` + The type of encoding to use (1, 2, or 4). + stack : `ImageStack` + The stack of LayeredImages from which to build the psi and phi images. )doc"; } // namespace pydocs -#endif /* PSI_PHI_ARRAY_DOCS */ +#endif /* SEARCH_DATA_DOCS */ diff --git a/src/kbmod/search/psi_phi_array.cpp b/src/kbmod/search/search_data.cpp similarity index 67% rename from src/kbmod/search/psi_phi_array.cpp rename to src/kbmod/search/search_data.cpp index 3f2656680..4331f7792 100644 --- a/src/kbmod/search/psi_phi_array.cpp +++ b/src/kbmod/search/search_data.cpp @@ -1,43 +1,41 @@ -#include "psi_phi_array_ds.h" -#include "psi_phi_array_utils.h" -#include "pydocs/psi_phi_array_docs.h" +#include "search_data_ds.h" +#include "search_data_utils.h" +#include "pydocs/search_data_docs.h" namespace search { // Declaration of CUDA functions that will be linked in. #ifdef HAVE_CUDA -extern "C" void device_allocate_psi_phi_array(PsiPhiArray* data); +extern "C" void device_allocate_search_data_arrays(SearchData* data); -extern "C" void device_free_psi_phi_array(PsiPhiArray* data); +extern "C" void device_free_search_data_arrays(SearchData* data); #endif // ------------------------------------------------------- // --- Implementation of core data structure functions --- // ------------------------------------------------------- -PsiPhiArray::PsiPhiArray() {} +SearchData::SearchData() {} -PsiPhiArray::~PsiPhiArray() { - if (cpu_array_ptr != nullptr) { - free(cpu_array_ptr); - } -#ifdef HAVE_CUDA - if (gpu_array_ptr != nullptr) { - device_free_psi_phi_array(this); - } -#endif +SearchData::~SearchData() { + clear(); } -void PsiPhiArray::clear() { +void SearchData::clear() { // Free all used memory on CPU and GPU. if (cpu_array_ptr != nullptr) { free(cpu_array_ptr); cpu_array_ptr = nullptr; } + if (cpu_time_array != nullptr) { + free(cpu_time_array); + cpu_array_ptr = nullptr; + } #ifdef HAVE_CUDA - if (gpu_array_ptr != nullptr) { - device_free_psi_phi_array(this); + if ((gpu_array_ptr != nullptr) || (gpu_time_array != nullptr)) { + device_free_search_data_arrays(this); gpu_array_ptr = nullptr; + gpu_time_array = nullptr; } #endif @@ -58,7 +56,7 @@ void PsiPhiArray::clear() { meta_data.phi_scale = 1.0; } -void PsiPhiArray::set_meta_data(int new_num_bytes, int new_num_times, int new_height, int new_width) { +void SearchData::set_meta_data(int new_num_bytes, int new_num_times, int new_height, int new_width) { // Validity checking of parameters. if (new_num_bytes != -1 && new_num_bytes != 1 && new_num_bytes != 2 && new_num_bytes != 4) { throw std::runtime_error("Invalid setting of num_bytes. Must be (-1 [use default], 1, 2, or 4)."); @@ -90,7 +88,7 @@ void PsiPhiArray::set_meta_data(int new_num_bytes, int new_num_times, int new_he meta_data.total_array_size = meta_data.block_size * meta_data.num_entries; } -void PsiPhiArray::set_psi_scaling(float min_val, float max_val, float scale_val) { +void SearchData::set_psi_scaling(float min_val, float max_val, float scale_val) { if (min_val > max_val) throw std::runtime_error("Min value needs to be < max value"); if (scale_val <= 0) throw std::runtime_error("Scale value must be greater than zero."); meta_data.psi_min_val = min_val; @@ -98,7 +96,7 @@ void PsiPhiArray::set_psi_scaling(float min_val, float max_val, float scale_val) meta_data.psi_scale = scale_val; } -void PsiPhiArray::set_phi_scaling(float min_val, float max_val, float scale_val) { +void SearchData::set_phi_scaling(float min_val, float max_val, float scale_val) { if (min_val > max_val) throw std::runtime_error("Min value needs to be < max value"); if (scale_val <= 0) throw std::runtime_error("Scale value must be greater than zero."); meta_data.phi_min_val = min_val; @@ -106,7 +104,7 @@ void PsiPhiArray::set_phi_scaling(float min_val, float max_val, float scale_val) meta_data.phi_scale = scale_val; } -PsiPhi PsiPhiArray::read_psi_phi(int time, int row, int col) { +PsiPhi SearchData::read_psi_phi(int time, int row, int col) { PsiPhi result = {NO_DATA, NO_DATA}; // Array allocation and bounds checking. @@ -140,6 +138,15 @@ PsiPhi PsiPhiArray::read_psi_phi(int time, int row, int col) { return result; } +float SearchData::read_time(int time_index) { + if (cpu_time_array == nullptr) throw std::runtime_error("Read from unallocated times array."); + if ((time_index < 0 )|| (time_index >= meta_data.num_times)) { + throw std::runtime_error("Out of bounds read for time step=%i", time_index); + } + return cpu_time_array[time_index]; +} + + // ------------------------------------------- // --- Implementation of utility functions --- // ------------------------------------------- @@ -171,7 +178,7 @@ std::array compute_scale_params_from_image_vect(const std::vector -void set_encode_cpu_psi_phi_array(PsiPhiArray& data, const std::vector& psi_imgs, +void set_encode_cpu_search_data(SearchData& data, const std::vector& psi_imgs, const std::vector& phi_imgs, bool debug) { if (data.get_cpu_array_ptr() != nullptr) { throw std::runtime_error("CPU PsiPhi already allocated."); @@ -215,7 +222,7 @@ void set_encode_cpu_psi_phi_array(PsiPhiArray& data, const std::vector data.set_cpu_array_ptr((void*)encoded); } -void set_float_cpu_psi_phi_array(PsiPhiArray& data, const std::vector& psi_imgs, +void set_float_cpu_search_data(SearchData& data, const std::vector& psi_imgs, const std::vector& phi_imgs, bool debug) { if (data.get_cpu_array_ptr() != nullptr) { throw std::runtime_error("CPU PsiPhi already allocated."); @@ -241,8 +248,9 @@ void set_float_cpu_psi_phi_array(PsiPhiArray& data, const std::vector& data.set_cpu_array_ptr((void*)encoded); } -void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vector& psi_imgs, - const std::vector& phi_imgs, bool debug) { +void fill_search_data(SearchData& result_data, int num_bytes, const std::vector& psi_imgs, + const std::vector& phi_imgs, const std::vector zeroed_times, + bool debug) { if (result_data.get_cpu_array_ptr() != nullptr) { return; } @@ -251,6 +259,7 @@ void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vect int num_times = psi_imgs.size(); if (num_times <= 0) throw std::runtime_error("Trying to fill PsiPhi from empty vectors."); if (num_times != phi_imgs.size()) throw std::runtime_error("Size mismatch between psi and phi."); + if (num_times != zeroed_times.size()) throw std::runtime_error("Size mismatch between psi and zeroed times."); int width = phi_imgs[0].get_width(); int height = phi_imgs[0].get_height(); @@ -275,78 +284,126 @@ void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vect // Do the local encoding. if (result_data.get_num_bytes() == 1) { - set_encode_cpu_psi_phi_array(result_data, psi_imgs, phi_imgs, debug); + set_encode_cpu_search_data(result_data, psi_imgs, phi_imgs, debug); } else { - set_encode_cpu_psi_phi_array(result_data, psi_imgs, phi_imgs, debug); + set_encode_cpu_search_data(result_data, psi_imgs, phi_imgs, debug); } } else { if (debug) { printf("Encoding psi and phi as floats.\n"); } // Just interleave psi and phi images. - set_float_cpu_psi_phi_array(result_data, psi_imgs, phi_imgs, debug); + set_float_cpu_search_data(result_data, psi_imgs, phi_imgs, debug); } + // Copy the time array. + const long unsigned times_bytes = result_data.get_num_times() * sizeof(float); + if (debug) printf("Allocating %lu bytes on the CPU for times.\n"); + + float* times_array = (float*)malloc(data.get_total_array_size()); + if (times_array == nullptr) throw std::runtime_error("Unable to allocate space for CPU times."); + for (int i = 0; i < result_data.get_num_times(); ++i) { + times_array[i] = zeroed_times[i]; + } + data.set_cpu_time_array_ptr((void*)times_array); + #ifdef HAVE_CUDA // Create a copy of the encoded data in GPU memory. if (debug) { printf("Allocating GPU memory for PsiPhi array using %lu bytes.\n", result_data.get_total_array_size()); + printf("Allocating GPU memory for times array using %lu bytes.\n", times_bytes); } - device_allocate_psi_phi_array(&result_data); + + device_allocate_search_data_arrays(&result_data); if (result_data.get_gpu_array_ptr() == nullptr) { throw std::runtime_error("Unable to allocate GPU PsiPhi array."); } + if (result_data.get_gpu_time_array_ptr() == nullptr) { + throw std::runtime_error("Unable to allocate GPU time array."); + } #endif } +void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, bool debug) { + // Compute Phi and Psi from convolved images while leaving masked pixels alone + // Reinsert 0s for NO_DATA? + std::vector psi_images; + std::vector phi_images; + const int num_images = stack.img_count(); + if (debug) { + unsigned long num_bytes = 2 * stack.get_height() * stack.get_width() * num_images * sizeof(float); + printf("Building %i temporary %i by %i images (psi and phi), requiring %lu bytes", + (num_images * 2), stack.get_width(), stack.get_height(), num_bytes); + } + + // Build the psi and phi images first. + for (int i = 0; i < num_images; ++i) { + LayeredImage& img = stack.get_single_image(i); + psi_images.push_back(img.generate_psi_image()); + phi_images.push_back(img.generate_phi_image()); + } + + // Convert these into an array form. Needs the full psi and phi computed first so the + // encoding can compute the bounds of each array. + std::vector zeroed_times = stack.build_zeroed_times(); + fill_search_data(psi_phi_data, num_bytes, psi_images, phi_images, zeroed_times, debug); +} + // ------------------------------------------- // --- Python definitions -------------------- // ------------------------------------------- #ifdef Py_PYTHON_H -static void psi_phi_array_binding(py::module& m) { - using ppa = search::PsiPhiArray; +static void search_data_binding(py::module& m) { + using ppa = search::SearchData; py::class_(m, "PsiPhi", pydocs::DOC_PsiPhi) .def(py::init<>()) .def_readwrite("psi", &search::PsiPhi::psi) .def_readwrite("phi", &search::PsiPhi::phi); - py::class_(m, "PsiPhiArray", pydocs::DOC_PsiPhiArray) + py::class_(m, "SearchData", pydocs::DOC_SearchData) .def(py::init<>()) - .def_property_readonly("num_bytes", &ppa::get_num_bytes, pydocs::DOC_PsiPhiArray_get_num_bytes) - .def_property_readonly("num_times", &ppa::get_num_times, pydocs::DOC_PsiPhiArray_get_num_times) - .def_property_readonly("width", &ppa::get_width, pydocs::DOC_PsiPhiArray_get_width) - .def_property_readonly("height", &ppa::get_height, pydocs::DOC_PsiPhiArray_get_height) + .def_property_readonly("num_bytes", &ppa::get_num_bytes, pydocs::DOC_SearchData_get_num_bytes) + .def_property_readonly("num_times", &ppa::get_num_times, pydocs::DOC_SearchData_get_num_times) + .def_property_readonly("width", &ppa::get_width, pydocs::DOC_SearchData_get_width) + .def_property_readonly("height", &ppa::get_height, pydocs::DOC_SearchData_get_height) .def_property_readonly("pixels_per_image", &ppa::get_pixels_per_image, - pydocs::DOC_PsiPhiArray_get_pixels_per_image) + pydocs::DOC_SearchData_get_pixels_per_image) .def_property_readonly("num_entries", &ppa::get_num_entries, - pydocs::DOC_PsiPhiArray_get_num_entries) + pydocs::DOC_SearchData_get_num_entries) .def_property_readonly("total_array_size", &ppa::get_total_array_size, - pydocs::DOC_PsiPhiArray_get_total_array_size) - .def_property_readonly("block_size", &ppa::get_block_size, pydocs::DOC_PsiPhiArray_get_block_size) + pydocs::DOC_SearchData_get_total_array_size) + .def_property_readonly("block_size", &ppa::get_block_size, pydocs::DOC_SearchData_get_block_size) .def_property_readonly("psi_min_val", &ppa::get_psi_min_val, - pydocs::DOC_PsiPhiArray_get_psi_min_val) + pydocs::DOC_SearchData_get_psi_min_val) .def_property_readonly("psi_max_val", &ppa::get_psi_max_val, - pydocs::DOC_PsiPhiArray_get_psi_max_val) - .def_property_readonly("psi_scale", &ppa::get_psi_scale, pydocs::DOC_PsiPhiArray_get_psi_scale) + pydocs::DOC_SearchData_get_psi_max_val) + .def_property_readonly("psi_scale", &ppa::get_psi_scale, pydocs::DOC_SearchData_get_psi_scale) .def_property_readonly("phi_min_val", &ppa::get_phi_min_val, - pydocs::DOC_PsiPhiArray_get_phi_min_val) + pydocs::DOC_SearchData_get_phi_min_val) .def_property_readonly("phi_max_val", &ppa::get_phi_max_val, - pydocs::DOC_PsiPhiArray_get_phi_max_val) - .def_property_readonly("phi_scale", &ppa::get_phi_scale, pydocs::DOC_PsiPhiArray_get_phi_scale) + pydocs::DOC_SearchData_get_phi_max_val) + .def_property_readonly("phi_scale", &ppa::get_phi_scale, pydocs::DOC_SearchData_get_phi_scale) .def_property_readonly("cpu_array_allocated", &ppa::cpu_array_allocated, - pydocs::DOC_PsiPhiArray_get_cpu_array_allocated) + pydocs::DOC_SearchData_get_cpu_array_allocated) .def_property_readonly("gpu_array_allocated", &ppa::gpu_array_allocated, - pydocs::DOC_PsiPhiArray_get_gpu_array_allocated) - .def("set_meta_data", &ppa::set_meta_data, pydocs::DOC_PsiPhiArray_set_meta_data) - .def("clear", &ppa::clear, pydocs::DOC_PsiPhiArray_clear) - .def("read_psi_phi", &ppa::read_psi_phi, pydocs::DOC_PsiPhiArray_read_psi_phi); + pydocs::DOC_SearchData_get_gpu_array_allocated) + .def_property_readonly("cpu_time_array_allocated", &ppa::cpu_time_array_allocated, + pydocs::DOC_SearchData_get_cpu_time_array_allocated) + .def_property_readonly("gpu_time_array_allocated", &ppa::gpu_time_array_allocated, + pydocs::DOC_SearchData_get_gpu_time_array_allocated) + .def("set_meta_data", &ppa::set_meta_data, pydocs::DOC_SearchData_set_meta_data) + .def("clear", &ppa::clear, pydocs::DOC_SearchData_clear) + .def("read_psi_phi", &ppa::read_psi_phi, pydocs::DOC_SearchData_read_psi_phi); + .def("read_time", &ppa::read_time, pydocs::DOC_SearchData_read_time); m.def("compute_scale_params_from_image_vect", &search::compute_scale_params_from_image_vect); m.def("decode_uint_scalar", &search::decode_uint_scalar); m.def("encode_uint_scalar", &search::encode_uint_scalar); - m.def("fill_psi_phi_array", &search::fill_psi_phi_array, pydocs::DOC_PsiPhiArray_fill_psi_phi_array); + m.def("fill_search_data", &search::fill_search_data, pydocs::DOC_SearchData_fill_search_data); + m.def("fill_search_data_from_image_stack", &search::fill_search_data_from_image_stack, + pydocs::DOC_SearchData_fill_search_data_from_image_stack); } #endif diff --git a/src/kbmod/search/psi_phi_array_ds.h b/src/kbmod/search/search_data_ds.h similarity index 68% rename from src/kbmod/search/psi_phi_array_ds.h rename to src/kbmod/search/search_data_ds.h index 719ec8687..900c3cf51 100644 --- a/src/kbmod/search/psi_phi_array_ds.h +++ b/src/kbmod/search/search_data_ds.h @@ -1,22 +1,23 @@ /* - * psi_phi_array_ds.h + * search_data_ds.h * - * The data structure for the interleaved psi/phi array. The the data + * The data structure for the raw data needed for the search algorith, + * including the psi/phi values and the zeroed times. The the data * structure and core functions are included in the header (and separated out * from the rest of the utility functions) to allow the CUDA files to import * only what they need. * * The data structure allocates memory on both the CPU and GPU for the - * interleaved psi/phi array and maintains ownership of the pointers - * until clear() is called or the PsiPhiArray's destructor is called. This allows - * the object to be passed repeatedly to the on-device search without reallocating - * and copying the memory on the GPU. + * arraysand maintains ownership of the pointers until clear() is called + * the object's destructor is called. This allows the object to be passed + * repeatedly to the on-device search without reallocating and copying the + * memory on the GPU. * * Created on: Dec 5, 2023 */ -#ifndef PSI_PHI_ARRAY_DS_ -#define PSI_PHI_ARRAY_DS_ +#ifndef SEARCH_DATA_DS_ +#define SEARCH_DATA_DS_ #include #include @@ -42,8 +43,8 @@ inline float decode_uint_scalar(float value, float min_val, float scale) { return (value == 0.0) ? NO_DATA : (value - 1.0) * scale + min_val; } -// The struct of meta data for the PsiPhiArray. -struct PsiPhiArrayMeta { +// The struct of meta data for the SearchData. +struct SearchDataMeta { int num_times = 0; int width = 0; int height = 0; @@ -64,17 +65,17 @@ struct PsiPhiArrayMeta { float phi_scale = 1.0; }; -/* PsiPhiArray is a class to hold the psi and phi arrays for the CPU and GPU as well as +/* SearchData is a class to hold the psi and phi arrays for the CPU and GPU as well as the meta data and functions to do encoding and decoding on CPU. */ -class PsiPhiArray { +class SearchData { public: - explicit PsiPhiArray(); - virtual ~PsiPhiArray(); + explicit SearchData(); + virtual ~SearchData(); void clear(); - inline PsiPhiArrayMeta& get_meta_data() { return meta_data; } + inline SearchDataMeta& get_meta_data() { return meta_data; } // --- Getter functions (for Python interface) ---------------- inline int get_num_bytes() { return meta_data.num_bytes; } @@ -95,9 +96,12 @@ class PsiPhiArray { inline bool cpu_array_allocated() { return cpu_array_ptr != nullptr; } inline bool gpu_array_allocated() { return gpu_array_ptr != nullptr; } + inline bool cpu_time_array_allocated() { return cpu_time_array != nullptr; } + inline bool gpu_time_array_allocated() { return gpu_time_array != nullptr; } - // Primary getter function for interaction (read the data). - PsiPhi read_psi_phi(int time, int row, int col); + // Primary getter functions for interaction (read the data). + PsiPhi read_psi_phi(int time_index, int row, int col); + float read_time_value(int time_index); // Setters for the utility functions to allocate the data. void set_meta_data(int new_num_bytes, int new_num_times, int new_height, int new_width); @@ -110,14 +114,21 @@ class PsiPhiArray { inline void set_cpu_array_ptr(void* new_ptr) { cpu_array_ptr = new_ptr; } inline void set_gpu_array_ptr(void* new_ptr) { gpu_array_ptr = new_ptr; } + inline float* get_cpu_time_array_ptr() { return cpu_time_array; } + inline float* get_gpu_time_array_ptr() { return gpu_time_array; } + inline void set_cpu_time_array_ptr(float* new_ptr) { cpu_time_array = new_ptr; } + inline void set_gpu_time_array_ptr(float* new_ptr) { gpu_time_array = new_ptr; } + private: - PsiPhiArrayMeta meta_data; + SearchDataMeta meta_data; - // Pointers the array (CPU space). - void* cpu_array_ptr = nullptr; + // Pointers to the arrays + void* cpu_array_ptr = nullptr; void* gpu_array_ptr = nullptr; + float* cpu_time_array = nullptr; + float* gpu_time_array = nullptr; }; } /* namespace search */ -#endif /* PSI_PHI_ARRAY_DS_ */ +#endif /* SEARCH_DATA_DS_ */ diff --git a/src/kbmod/search/search_data_utils.h b/src/kbmod/search/search_data_utils.h new file mode 100644 index 000000000..50538e990 --- /dev/null +++ b/src/kbmod/search/search_data_utils.h @@ -0,0 +1,39 @@ +/* + * search_data_utils.h + * + * The utility functions for the psi/phi array. Broken out from the header + * data structure so that it can use packages that won't be imported into the + * CUDA kernel, such as Eigen. + * + * Created on: Dec 8, 2023 + */ + +#ifndef SEARCH_DATA_UTILS_ +#define SEARCH_DATA_UTILS_ + +#include +#include +#include +#include + +#include "common.h" +#include "image_stack.h" +#include "layered_image.h" +#include "search_data_ds.h" +#include "raw_image.h" + +namespace search { + +// Compute the min, max, and scale parameter from the a vector of image data. +std::array compute_scale_params_from_image_vect(const std::vector& imgs, int num_bytes); + +void fill_search_data(SearchData& result_data, int num_bytes, const std::vector& psi_imgs, + const std::vector& phi_imgs, const std::vector zeroed_times, + bool debug = false); + +void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, + bool debug = false); + +} /* namespace search */ + +#endif /* SEARCH_DATA_UTILS_ */ diff --git a/src/kbmod/search/stack_search.cpp b/src/kbmod/search/stack_search.cpp index ba31ef247..60ff1611c 100644 --- a/src/kbmod/search/stack_search.cpp +++ b/src/kbmod/search/stack_search.cpp @@ -2,9 +2,8 @@ namespace search { #ifdef HAVE_CUDA -extern "C" void deviceSearchFilter(PsiPhiArray& psi_phi_data, float* image_times, SearchParameters params, - int num_trajectories, Trajectory* trj_to_search, int num_results, - Trajectory* best_results); +extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters params, int num_trajectories, + Trajectory *trj_to_search, int num_results, Trajectory *best_results); #endif StackSearch::StackSearch(ImageStack& imstack) : stack(imstack) { @@ -76,8 +75,8 @@ void StackSearch::search(int ang_steps, int vel_steps, float min_ang, float max_ DebugTimer psi_phi_timer = DebugTimer("Creating psi/phi buffers", debug_info); prepare_psi_phi(); - PsiPhiArray psi_phi_data; - fill_psi_phi_array(psi_phi_data, params.encode_num_bytes, psi_images, phi_images, debug_info); + SearchData psi_phi_data; + fill_search_data(psi_phi_data, params.encode_num_bytes, psi_images, phi_images, debug_info); psi_phi_timer.stop(); // Allocate a vector for the results. @@ -98,7 +97,7 @@ void StackSearch::search(int ang_steps, int vel_steps, float min_ang, float max_ // Do the actual search on the GPU. DebugTimer search_timer = DebugTimer("Running search", debug_info); #ifdef HAVE_CUDA - deviceSearchFilter(psi_phi_data, image_times.data(), params, search_list.size(), search_list.data(), + deviceSearchFilter(psi_phi_data, params, search_list.size(), search_list.data(), max_results, results.data()); #else throw std::runtime_error("Non-GPU search is not implemented."); From 881a058f466acd19982b1b5f60bd4e94da87d44a Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:14:43 -0500 Subject: [PATCH 02/27] File rename --- .../search/pydocs/{psi_phi_array_docs.h => search_data_docs.h} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/kbmod/search/pydocs/{psi_phi_array_docs.h => search_data_docs.h} (100%) diff --git a/src/kbmod/search/pydocs/psi_phi_array_docs.h b/src/kbmod/search/pydocs/search_data_docs.h similarity index 100% rename from src/kbmod/search/pydocs/psi_phi_array_docs.h rename to src/kbmod/search/pydocs/search_data_docs.h From ce99d654d67b6afc66c3237340979918122f4e33 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:29:48 -0500 Subject: [PATCH 03/27] Add tests --- src/kbmod/search/search_data_utils.h | 2 +- ...t_psi_phi_array.py => test_search_data.py} | 77 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) rename tests/{test_psi_phi_array.py => test_search_data.py} (72%) diff --git a/src/kbmod/search/search_data_utils.h b/src/kbmod/search/search_data_utils.h index 50538e990..4d300af66 100644 --- a/src/kbmod/search/search_data_utils.h +++ b/src/kbmod/search/search_data_utils.h @@ -32,7 +32,7 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< bool debug = false); void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, - bool debug = false); + bool debug = false); } /* namespace search */ diff --git a/tests/test_psi_phi_array.py b/tests/test_search_data.py similarity index 72% rename from tests/test_psi_phi_array.py rename to tests/test_search_data.py index 8260fffaf..6e3e234e2 100644 --- a/tests/test_psi_phi_array.py +++ b/tests/test_search_data.py @@ -3,18 +3,23 @@ import numpy as np from kbmod.search import ( + HAS_GPU, KB_NO_DATA, + PSF, + ImageStack, + LayeredImage, PsiPhi, - PsiPhiArray, + SearchData, RawImage, compute_scale_params_from_image_vect, decode_uint_scalar, encode_uint_scalar, - fill_psi_phi_array, + fill_search_data, + fill_search_data_from_image_stack, ) -class test_psi_phi_array(unittest.TestCase): +class test_search_data(unittest.TestCase): def setUp(self): self.num_times = 2 self.width = 4 @@ -31,8 +36,10 @@ def setUp(self): self.phi_1 = RawImage(np.full((self.height, self.width), 0.1, dtype=np.single), obs_time=1.0) self.phi_2 = RawImage(np.full((self.height, self.width), 0.2, dtype=np.single), obs_time=2.0) + self.zeroed_times = [0.0, 1.0] + def test_set_meta_data(self): - arr = PsiPhiArray() + arr = SearchData() self.assertEqual(arr.num_times, 0) self.assertEqual(arr.num_bytes, 4) self.assertEqual(arr.width, 0) @@ -121,10 +128,10 @@ def test_compute_scale_params_from_image_vect(self): self.assertAlmostEqual(result_uint16[1], max_val, delta=1e-5) self.assertAlmostEqual(result_uint16[2], max_val / 65535.0, delta=1e-5) - def test_fill_psi_phi_array(self): + def test_fill_search_data(self): for num_bytes in [2, 4]: - arr = PsiPhiArray() - fill_psi_phi_array(arr, num_bytes, [self.psi_1, self.psi_2], [self.phi_1, self.phi_2], False) + arr = SearchData() + fill_search_data(arr, num_bytes, [self.psi_1, self.psi_2], [self.phi_1, self.phi_2], self.zeroed_times, False) # Check the meta data. self.assertEqual(arr.num_times, self.num_times) @@ -139,9 +146,16 @@ def test_fill_psi_phi_array(self): self.assertEqual(arr.block_size, num_bytes) self.assertEqual(arr.total_array_size, arr.num_entries * arr.block_size) - # Check that we can correctly read the values from the CPU. + # Check that we allocate the arrays self.assertTrue(arr.cpu_array_allocated) + self.assertTrue(arr.cpu_time_array_allocated) + if (HAS_GPU): + self.assertTrue(arr.gpu_array_allocated) + self.assertTrue(arr.gpu_time_array_allocated) + + # Check that we can correctly read the values from the CPU. for time in range(self.num_times): + self.assertAlmostEqual(arr.read_time(time), self.zeroed_times[time]) offset = time * self.width * self.height for row in range(self.height): for col in range(self.width): @@ -152,6 +166,53 @@ def test_fill_psi_phi_array(self): # Check that the arrays are set to NULL after we clear it (memory should be freed too). arr.clear() self.assertFalse(arr.cpu_array_allocated) + self.assertFalse(arr.cpu_time_array_allocated) + if (HAS_GPU): + self.assertFalse(arr.gpu_array_allocated) + self.assertFalse(arr.gpu_time_array_allocated) + + def test_fill_search_data_from_image_stack(self): + # Build a fake image stack. + num_images = 5 + width = 21 + height = 15 + images = [None] * num_images + p = PSF(1.0) + for i in range(num_images): + self.images[i] = kb.LayeredImage( + width, + height, + 2.0, # noise_level + 4.0, # variance + 2.0 * i + 1.0, # time + p, + ) + im_stack = ImageStack(images) + + # Create the SearchData from the ImageStack. + arr = SearchData() + fill_search_data_from_image_stack(arr, im_stack, 4, False) + + # Check the meta data. + self.assertEqual(arr.num_times, num_images) + self.assertEqual(arr.num_bytes, 4) + self.assertEqual(arr.width, width) + self.assertEqual(arr.height, height) + self.assertEqual(arr.pixels_per_image, width * height) + self.assertEqual(arr.num_entries, 2 * arr.pixels_per_image * num_times) + self.assertEqual(arr.block_size, 4) + self.assertEqual(arr.total_array_size, arr.num_entries * arr.block_size) + + # Check that we allocated the arrays. + self.assertTrue(arr.cpu_array_allocated) + self.assertTrue(arr.cpu_time_array_allocated) + if (HAS_GPU): + self.assertTrue(arr.gpu_array_allocated) + self.assertTrue(arr.gpu_time_array_allocated) + + # Since we filled the images with random data, we only test the times. + for time in range(self.num_times): + self.assertAlmostEqual(arr.read_time(time), 2.0 * time) if __name__ == "__main__": From 0b2b94124926e4776757561a1cc77d99fc8870a8 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:13:19 -0500 Subject: [PATCH 04/27] Bug fixes and linting --- src/kbmod/search/bindings.cpp | 4 +-- src/kbmod/search/kernels.cu | 12 ++++---- src/kbmod/search/search_data.cpp | 41 ++++++++++++++-------------- src/kbmod/search/search_data_ds.h | 9 +++--- src/kbmod/search/search_data_utils.h | 4 +-- src/kbmod/search/stack_search.cpp | 10 +++---- src/kbmod/search/stack_search.h | 4 +-- tests/test_search_data.py | 24 ++++++++-------- 8 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/kbmod/search/bindings.cpp b/src/kbmod/search/bindings.cpp index 4175c9639..a76eb0645 100644 --- a/src/kbmod/search/bindings.cpp +++ b/src/kbmod/search/bindings.cpp @@ -16,7 +16,7 @@ namespace py = pybind11; #include "stack_search.cpp" #include "stamp_creator.cpp" #include "kernel_testing_helpers.cpp" -#include "psi_phi_array.cpp" +#include "search_data.cpp" PYBIND11_MODULE(search, m) { m.attr("KB_NO_DATA") = pybind11::float_(search::NO_DATA); @@ -40,7 +40,7 @@ PYBIND11_MODULE(search, m) { search::pixel_pos_bindings(m); search::image_moments_bindings(m); search::stamp_parameters_bindings(m); - search::psi_phi_array_binding(m); + search::search_data_binding(m); // Functions from raw_image.cpp m.def("create_median_image", &search::create_median_image); m.def("create_summed_image", &search::create_summed_image); diff --git a/src/kbmod/search/kernels.cu b/src/kbmod/search/kernels.cu index 2accdb171..c5036a0ac 100644 --- a/src/kbmod/search/kernels.cu +++ b/src/kbmod/search/kernels.cu @@ -42,7 +42,8 @@ extern "C" void device_allocate_search_data_arrays(SearchData *data) { float *device_times_ptr; long unsigned time_bytes = data->get_num_times() * sizeof(float); checkCudaErrors(cudaMalloc((void **)&device_times_ptr, time_bytes)); - checkCudaErrors(cudaMemcpy(device_times_ptr, data->get_cpu_time_array_ptr(), time_bytes, cudaMemcpyHostToDevice)); + checkCudaErrors( + cudaMemcpy(device_times_ptr, data->get_cpu_time_array_ptr(), time_bytes, cudaMemcpyHostToDevice)); data->set_gpu_time_array_ptr(device_times_ptr); } @@ -296,7 +297,7 @@ extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters par // Copy trajectories to search if (params.debug) { printf("Allocating GPU memory for testing grid with %i elements using %lu bytes.\n", num_trajectories, - sizeof(Trajectory) * num_trajectories); + sizeof(Trajectory) * num_trajectories); } checkCudaErrors(cudaMalloc((void **)&device_tests, sizeof(Trajectory) * num_trajectories)); checkCudaErrors(cudaMemcpy(device_tests, trj_to_search, sizeof(Trajectory) * num_trajectories, @@ -304,7 +305,8 @@ extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters par // Allocate space for the results. if (params.debug) { - printf("Allocating GPU memory for %i results using %lu bytes.\n", num_results, sizeof(Trajectory) * num_results); + printf("Allocating GPU memory for %i results using %lu bytes.\n", num_results, + sizeof(Trajectory) * num_results); } checkCudaErrors(cudaMalloc((void **)&device_search_results, sizeof(Trajectory) * num_results)); @@ -318,8 +320,8 @@ extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters par // Launch Search searchFilterImages<<>>(search_data.get_meta_data(), search_data.get_gpu_array_ptr(), - static_cast(search_data.get_gpu_time_array_ptr()), params, - num_trajectories, device_tests, device_search_results); + static_cast(search_data.get_gpu_time_array_ptr()), + params, num_trajectories, device_tests, device_search_results); // Read back results checkCudaErrors(cudaMemcpy(best_results, device_search_results, sizeof(Trajectory) * num_results, diff --git a/src/kbmod/search/search_data.cpp b/src/kbmod/search/search_data.cpp index 4331f7792..ae2e1c425 100644 --- a/src/kbmod/search/search_data.cpp +++ b/src/kbmod/search/search_data.cpp @@ -17,9 +17,7 @@ extern "C" void device_free_search_data_arrays(SearchData* data); SearchData::SearchData() {} -SearchData::~SearchData() { - clear(); -} +SearchData::~SearchData() { clear(); } void SearchData::clear() { // Free all used memory on CPU and GPU. @@ -29,7 +27,7 @@ void SearchData::clear() { } if (cpu_time_array != nullptr) { free(cpu_time_array); - cpu_array_ptr = nullptr; + cpu_time_array = nullptr; } #ifdef HAVE_CUDA if ((gpu_array_ptr != nullptr) || (gpu_time_array != nullptr)) { @@ -140,13 +138,12 @@ PsiPhi SearchData::read_psi_phi(int time, int row, int col) { float SearchData::read_time(int time_index) { if (cpu_time_array == nullptr) throw std::runtime_error("Read from unallocated times array."); - if ((time_index < 0 )|| (time_index >= meta_data.num_times)) { - throw std::runtime_error("Out of bounds read for time step=%i", time_index); + if ((time_index < 0) || (time_index >= meta_data.num_times)) { + throw std::runtime_error("Out of bounds read for time step."); } return cpu_time_array[time_index]; } - // ------------------------------------------- // --- Implementation of utility functions --- // ------------------------------------------- @@ -179,7 +176,7 @@ std::array compute_scale_params_from_image_vect(const std::vector void set_encode_cpu_search_data(SearchData& data, const std::vector& psi_imgs, - const std::vector& phi_imgs, bool debug) { + const std::vector& phi_imgs, bool debug) { if (data.get_cpu_array_ptr() != nullptr) { throw std::runtime_error("CPU PsiPhi already allocated."); } @@ -223,7 +220,7 @@ void set_encode_cpu_search_data(SearchData& data, const std::vector& p } void set_float_cpu_search_data(SearchData& data, const std::vector& psi_imgs, - const std::vector& phi_imgs, bool debug) { + const std::vector& phi_imgs, bool debug) { if (data.get_cpu_array_ptr() != nullptr) { throw std::runtime_error("CPU PsiPhi already allocated."); } @@ -249,8 +246,8 @@ void set_float_cpu_search_data(SearchData& data, const std::vector& ps } void fill_search_data(SearchData& result_data, int num_bytes, const std::vector& psi_imgs, - const std::vector& phi_imgs, const std::vector zeroed_times, - bool debug) { + const std::vector& phi_imgs, const std::vector zeroed_times, + bool debug) { if (result_data.get_cpu_array_ptr() != nullptr) { return; } @@ -259,7 +256,8 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< int num_times = psi_imgs.size(); if (num_times <= 0) throw std::runtime_error("Trying to fill PsiPhi from empty vectors."); if (num_times != phi_imgs.size()) throw std::runtime_error("Size mismatch between psi and phi."); - if (num_times != zeroed_times.size()) throw std::runtime_error("Size mismatch between psi and zeroed times."); + if (num_times != zeroed_times.size()) + throw std::runtime_error("Size mismatch between psi and zeroed times."); int width = phi_imgs[0].get_width(); int height = phi_imgs[0].get_height(); @@ -298,14 +296,14 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< // Copy the time array. const long unsigned times_bytes = result_data.get_num_times() * sizeof(float); - if (debug) printf("Allocating %lu bytes on the CPU for times.\n"); + if (debug) printf("Allocating %lu bytes on the CPU for times.\n", times_bytes); - float* times_array = (float*)malloc(data.get_total_array_size()); + float* times_array = (float*)malloc(times_bytes); if (times_array == nullptr) throw std::runtime_error("Unable to allocate space for CPU times."); for (int i = 0; i < result_data.get_num_times(); ++i) { times_array[i] = zeroed_times[i]; } - data.set_cpu_time_array_ptr((void*)times_array); + result_data.set_cpu_time_array_ptr(times_array); #ifdef HAVE_CUDA // Create a copy of the encoded data in GPU memory. @@ -314,7 +312,7 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< result_data.get_total_array_size()); printf("Allocating GPU memory for times array using %lu bytes.\n", times_bytes); } - + device_allocate_search_data_arrays(&result_data); if (result_data.get_gpu_array_ptr() == nullptr) { throw std::runtime_error("Unable to allocate GPU PsiPhi array."); @@ -325,7 +323,8 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< #endif } -void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, bool debug) { +void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, + bool debug) { // Compute Phi and Psi from convolved images while leaving masked pixels alone // Reinsert 0s for NO_DATA? std::vector psi_images; @@ -333,8 +332,8 @@ void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stac const int num_images = stack.img_count(); if (debug) { unsigned long num_bytes = 2 * stack.get_height() * stack.get_width() * num_images * sizeof(float); - printf("Building %i temporary %i by %i images (psi and phi), requiring %lu bytes", - (num_images * 2), stack.get_width(), stack.get_height(), num_bytes); + printf("Building %i temporary %i by %i images (psi and phi), requiring %lu bytes", (num_images * 2), + stack.get_width(), stack.get_height(), num_bytes); } // Build the psi and phi images first. @@ -347,7 +346,7 @@ void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stac // Convert these into an array form. Needs the full psi and phi computed first so the // encoding can compute the bounds of each array. std::vector zeroed_times = stack.build_zeroed_times(); - fill_search_data(psi_phi_data, num_bytes, psi_images, phi_images, zeroed_times, debug); + fill_search_data(result_data, num_bytes, psi_images, phi_images, zeroed_times, debug); } // ------------------------------------------- @@ -396,7 +395,7 @@ static void search_data_binding(py::module& m) { pydocs::DOC_SearchData_get_gpu_time_array_allocated) .def("set_meta_data", &ppa::set_meta_data, pydocs::DOC_SearchData_set_meta_data) .def("clear", &ppa::clear, pydocs::DOC_SearchData_clear) - .def("read_psi_phi", &ppa::read_psi_phi, pydocs::DOC_SearchData_read_psi_phi); + .def("read_psi_phi", &ppa::read_psi_phi, pydocs::DOC_SearchData_read_psi_phi) .def("read_time", &ppa::read_time, pydocs::DOC_SearchData_read_time); m.def("compute_scale_params_from_image_vect", &search::compute_scale_params_from_image_vect); m.def("decode_uint_scalar", &search::decode_uint_scalar); diff --git a/src/kbmod/search/search_data_ds.h b/src/kbmod/search/search_data_ds.h index 900c3cf51..7a64842ce 100644 --- a/src/kbmod/search/search_data_ds.h +++ b/src/kbmod/search/search_data_ds.h @@ -8,10 +8,11 @@ * only what they need. * * The data structure allocates memory on both the CPU and GPU for the - * arraysand maintains ownership of the pointers until clear() is called + * arrays and maintains ownership of the pointers until clear() is called * the object's destructor is called. This allows the object to be passed * repeatedly to the on-device search without reallocating and copying the - * memory on the GPU. + * memory on the GPU. All arrays are stored as pointers (instead of vectors) + * for compatibility with CUDA. * * Created on: Dec 5, 2023 */ @@ -101,7 +102,7 @@ class SearchData { // Primary getter functions for interaction (read the data). PsiPhi read_psi_phi(int time_index, int row, int col); - float read_time_value(int time_index); + float read_time(int time_index); // Setters for the utility functions to allocate the data. void set_meta_data(int new_num_bytes, int new_num_times, int new_height, int new_width); @@ -123,7 +124,7 @@ class SearchData { SearchDataMeta meta_data; // Pointers to the arrays - void* cpu_array_ptr = nullptr; + void* cpu_array_ptr = nullptr; void* gpu_array_ptr = nullptr; float* cpu_time_array = nullptr; float* gpu_time_array = nullptr; diff --git a/src/kbmod/search/search_data_utils.h b/src/kbmod/search/search_data_utils.h index 4d300af66..683895ce4 100644 --- a/src/kbmod/search/search_data_utils.h +++ b/src/kbmod/search/search_data_utils.h @@ -28,8 +28,8 @@ namespace search { std::array compute_scale_params_from_image_vect(const std::vector& imgs, int num_bytes); void fill_search_data(SearchData& result_data, int num_bytes, const std::vector& psi_imgs, - const std::vector& phi_imgs, const std::vector zeroed_times, - bool debug = false); + const std::vector& phi_imgs, const std::vector zeroed_times, + bool debug = false); void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, bool debug = false); diff --git a/src/kbmod/search/stack_search.cpp b/src/kbmod/search/stack_search.cpp index 60ff1611c..64c6741b7 100644 --- a/src/kbmod/search/stack_search.cpp +++ b/src/kbmod/search/stack_search.cpp @@ -2,8 +2,8 @@ namespace search { #ifdef HAVE_CUDA -extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters params, int num_trajectories, - Trajectory *trj_to_search, int num_results, Trajectory *best_results); +extern "C" void deviceSearchFilter(SearchData& search_data, SearchParameters params, int num_trajectories, + Trajectory* trj_to_search, int num_results, Trajectory* best_results); #endif StackSearch::StackSearch(ImageStack& imstack) : stack(imstack) { @@ -76,7 +76,7 @@ void StackSearch::search(int ang_steps, int vel_steps, float min_ang, float max_ DebugTimer psi_phi_timer = DebugTimer("Creating psi/phi buffers", debug_info); prepare_psi_phi(); SearchData psi_phi_data; - fill_search_data(psi_phi_data, params.encode_num_bytes, psi_images, phi_images, debug_info); + fill_search_data(psi_phi_data, params.encode_num_bytes, psi_images, phi_images, image_times, debug_info); psi_phi_timer.stop(); // Allocate a vector for the results. @@ -97,8 +97,8 @@ void StackSearch::search(int ang_steps, int vel_steps, float min_ang, float max_ // Do the actual search on the GPU. DebugTimer search_timer = DebugTimer("Running search", debug_info); #ifdef HAVE_CUDA - deviceSearchFilter(psi_phi_data, params, search_list.size(), search_list.data(), - max_results, results.data()); + deviceSearchFilter(psi_phi_data, params, search_list.size(), search_list.data(), max_results, + results.data()); #else throw std::runtime_error("Non-GPU search is not implemented."); #endif diff --git a/src/kbmod/search/stack_search.h b/src/kbmod/search/stack_search.h index 597f78850..5b5af451e 100644 --- a/src/kbmod/search/stack_search.h +++ b/src/kbmod/search/stack_search.h @@ -16,8 +16,8 @@ #include "geom.h" #include "image_stack.h" #include "psf.h" -#include "psi_phi_array_ds.h" -#include "psi_phi_array_utils.h" +#include "search_data_ds.h" +#include "search_data_utils.h" #include "pydocs/stack_search_docs.h" #include "stamp_creator.h" diff --git a/tests/test_search_data.py b/tests/test_search_data.py index 6e3e234e2..80b847365 100644 --- a/tests/test_search_data.py +++ b/tests/test_search_data.py @@ -131,7 +131,9 @@ def test_compute_scale_params_from_image_vect(self): def test_fill_search_data(self): for num_bytes in [2, 4]: arr = SearchData() - fill_search_data(arr, num_bytes, [self.psi_1, self.psi_2], [self.phi_1, self.phi_2], self.zeroed_times, False) + fill_search_data( + arr, num_bytes, [self.psi_1, self.psi_2], [self.phi_1, self.phi_2], self.zeroed_times, False + ) # Check the meta data. self.assertEqual(arr.num_times, self.num_times) @@ -149,7 +151,7 @@ def test_fill_search_data(self): # Check that we allocate the arrays self.assertTrue(arr.cpu_array_allocated) self.assertTrue(arr.cpu_time_array_allocated) - if (HAS_GPU): + if HAS_GPU: self.assertTrue(arr.gpu_array_allocated) self.assertTrue(arr.gpu_time_array_allocated) @@ -167,19 +169,19 @@ def test_fill_search_data(self): arr.clear() self.assertFalse(arr.cpu_array_allocated) self.assertFalse(arr.cpu_time_array_allocated) - if (HAS_GPU): + if HAS_GPU: self.assertFalse(arr.gpu_array_allocated) self.assertFalse(arr.gpu_time_array_allocated) def test_fill_search_data_from_image_stack(self): # Build a fake image stack. - num_images = 5 + num_times = 5 width = 21 height = 15 - images = [None] * num_images + images = [None] * num_times p = PSF(1.0) - for i in range(num_images): - self.images[i] = kb.LayeredImage( + for i in range(num_times): + images[i] = LayeredImage( width, height, 2.0, # noise_level @@ -192,9 +194,9 @@ def test_fill_search_data_from_image_stack(self): # Create the SearchData from the ImageStack. arr = SearchData() fill_search_data_from_image_stack(arr, im_stack, 4, False) - + # Check the meta data. - self.assertEqual(arr.num_times, num_images) + self.assertEqual(arr.num_times, num_times) self.assertEqual(arr.num_bytes, 4) self.assertEqual(arr.width, width) self.assertEqual(arr.height, height) @@ -206,12 +208,12 @@ def test_fill_search_data_from_image_stack(self): # Check that we allocated the arrays. self.assertTrue(arr.cpu_array_allocated) self.assertTrue(arr.cpu_time_array_allocated) - if (HAS_GPU): + if HAS_GPU: self.assertTrue(arr.gpu_array_allocated) self.assertTrue(arr.gpu_time_array_allocated) # Since we filled the images with random data, we only test the times. - for time in range(self.num_times): + for time in range(num_times): self.assertAlmostEqual(arr.read_time(time), 2.0 * time) From a2bcbc0b0a24eab9aa01a51b84c9652d9699f3ae Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:37:30 -0500 Subject: [PATCH 05/27] Break out core evaluation into its own function --- src/kbmod/search/kernels.cu | 243 ++++++++++++------------------------ 1 file changed, 80 insertions(+), 163 deletions(-) diff --git a/src/kbmod/search/kernels.cu b/src/kbmod/search/kernels.cu index 84b6cf249..4a798155b 100644 --- a/src/kbmod/search/kernels.cu +++ b/src/kbmod/search/kernels.cu @@ -41,7 +41,7 @@ extern "C" void device_free_psi_phi_array(PsiPhiArray *data) { } } -__forceinline__ __device__ PsiPhi read_encoded_psi_phi(PsiPhiArrayMeta ¶ms, void *psi_phi_vect, int time, +__host__ __device__ PsiPhi read_encoded_psi_phi(PsiPhiArrayMeta ¶ms, void *psi_phi_vect, int time, int row, int col) { // Bounds checking. if ((row < 0) || (col < 0) || (row >= params.height) || (col >= params.width)) { @@ -122,6 +122,81 @@ extern "C" __device__ __host__ void SigmaGFilteredIndicesCU(float *values, int n *max_keep_idx = end - 1; } +/* + * Evaluate the likelihood score (as computed with from the psi and phi values) for a single + * given candidate trajectory. Modifies the trajectory in place to update the number of + * observations, likelihood, and flux. + */ +extern "C" __device__ __host__ void evaluateTrajectory(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_vect, float *image_times, + SearchParameters params, Trajectory *candidate) { + // Data structures used for filtering. We fill in only what we need. + float psi_array[MAX_NUM_IMAGES]; + float phi_array[MAX_NUM_IMAGES]; + float psi_sum = 0.0; + float phi_sum = 0.0; + + // Reset the statistics for the candidate. + candidate->obs_count = 0; + candidate->lh = -1.0; + candidate->flux = -1.0; + + // Loop over each image and sample the appropriate pixel + int num_seen = 0; + for (int i = 0; i < psi_phi_meta.num_times; ++i) { + // Predict the trajectory's position. + float curr_time = image_times[i]; + int current_x = candidate->x + int(candidate->vx * curr_time + 0.5); + int current_y = candidate->y + int(candidate->vy * curr_time + 0.5); + + // Get the Psi and Phi pixel values. + PsiPhi pixel_vals = read_encoded_psi_phi(psi_phi_meta, psi_phi_vect, i, current_y, current_x); + if (pixel_vals.psi != NO_DATA && pixel_vals.phi != NO_DATA) { + psi_sum += pixel_vals.psi; + phi_sum += pixel_vals.phi; + psi_array[num_seen] = pixel_vals.psi; + phi_array[num_seen] = pixel_vals.phi; + num_seen += 1; + } + } + candidate->obs_count = num_seen; + candidate->lh = psi_sum / sqrt(phi_sum); + candidate->flux = psi_sum / phi_sum; + + // If we do not have enough observations or a good enough LH score, + // do not bother with any of the following steps. + if ((candidate->obs_count < params.min_observations) || + (params.do_sigmag_filter && candidate->lh < params.min_lh)) + return; + + // If we are doing on GPU filtering, run the sigma_g filter and recompute the likelihoods. + if (params.do_sigmag_filter) { + // Fill in a likelihood and index array for sorting. + float lc_array[MAX_NUM_IMAGES]; + int idx_array[MAX_NUM_IMAGES]; + for (int i = 0; i < num_seen; ++i) { + lc_array[i] = (phi_array[i] != 0) ? (psi_array[i] / phi_array[i]) : 0; + idx_array[i] = i; + } + + int min_keep_idx = 0; + int max_keep_idx = num_seen - 1; + SigmaGFilteredIndicesCU(lc_array, num_seen, params.sgl_L, params.sgl_H, params.sigmag_coeff, 2.0, + idx_array, &min_keep_idx, &max_keep_idx); + + // Compute the likelihood and flux of the track based on the filtered + // observations (ones in [min_keep_idx, max_keep_idx]). + float new_psi_sum = 0.0; + float new_phi_sum = 0.0; + for (int i = min_keep_idx; i <= max_keep_idx; i++) { + int idx = idx_array[i]; + new_psi_sum += psi_array[idx]; + new_phi_sum += phi_array[idx]; + } + candidate->lh = new_psi_sum / sqrt(new_phi_sum); + candidate->flux = new_psi_sum / new_phi_sum; + } +} + /* * Searches through images (represented as a flat array of floats) looking for most likely * trajectories in the given list. Outputs a results image of best trajectories. Returns a @@ -148,12 +223,6 @@ __global__ void searchFilterImages(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_v const int x = x_i + params.x_start_min; const int y = y_i + params.y_start_min; - // Data structures used for filtering. - float lc_array[MAX_NUM_IMAGES]; - float psi_array[MAX_NUM_IMAGES]; - float phi_array[MAX_NUM_IMAGES]; - int idx_array[MAX_NUM_IMAGES]; - // Create an initial set of best results with likelihood -1.0. // We also set (x, y) because they are used in the later python // functions. @@ -174,67 +243,15 @@ __global__ void searchFilterImages(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_v curr_trj.vy = trajectories[t].vy; curr_trj.obs_count = 0; - float psi_sum = 0.0; - float phi_sum = 0.0; - - // Loop over each image and sample the appropriate pixel - for (int i = 0; i < psi_phi_meta.num_times; ++i) { - lc_array[i] = 0; - psi_array[i] = 0; - phi_array[i] = 0; - idx_array[i] = i; - } - - // Loop over each image and sample the appropriate pixel - int num_seen = 0; - for (int i = 0; i < psi_phi_meta.num_times; ++i) { - // Predict the trajectory's position. - float curr_time = image_times[i]; - int current_x = x + int(curr_trj.vx * curr_time + 0.5); - int current_y = y + int(curr_trj.vy * curr_time + 0.5); - - // Get the Psi and Phi pixel values. - PsiPhi pixel_vals = read_encoded_psi_phi(psi_phi_meta, psi_phi_vect, i, current_y, current_x); - if (pixel_vals.psi != NO_DATA && pixel_vals.phi != NO_DATA) { - curr_trj.obs_count++; - psi_sum += pixel_vals.psi; - phi_sum += pixel_vals.phi; - psi_array[num_seen] = pixel_vals.psi; - phi_array[num_seen] = pixel_vals.phi; - if (pixel_vals.phi != 0.0) lc_array[num_seen] = pixel_vals.psi / pixel_vals.phi; - num_seen += 1; - } - } - curr_trj.lh = psi_sum / sqrt(phi_sum); - curr_trj.flux = psi_sum / phi_sum; + // Evaluate the trajectory. + evaluateTrajectory(psi_phi_meta, psi_phi_vect, image_times, params, &curr_trj); // If we do not have enough observations or a good enough LH score, - // do not bother with any of the following steps. + // do not bother inserting it into the sorted list of results. if ((curr_trj.obs_count < params.min_observations) || (params.do_sigmag_filter && curr_trj.lh < params.min_lh)) continue; - // If we are doing on GPU filtering, run the sigma_g filter - // and recompute the likelihoods. - if (params.do_sigmag_filter) { - int min_keep_idx = 0; - int max_keep_idx = num_seen - 1; - SigmaGFilteredIndicesCU(lc_array, num_seen, params.sgl_L, params.sgl_H, params.sigmag_coeff, 2.0, - idx_array, &min_keep_idx, &max_keep_idx); - - // Compute the likelihood and flux of the track based on the filtered - // observations (ones in [min_keep_idx, max_keep_idx]). - float new_psi_sum = 0.0; - float new_phi_sum = 0.0; - for (int i = min_keep_idx; i <= max_keep_idx; i++) { - int idx = idx_array[i]; - new_psi_sum += psi_array[idx]; - new_phi_sum += phi_array[idx]; - } - curr_trj.lh = new_psi_sum / sqrt(new_phi_sum); - curr_trj.flux = new_psi_sum / new_phi_sum; - } - // Insert the new trajectory into the sorted list of results. // Only sort the values with valid likelihoods. Trajectory temp; @@ -311,6 +328,7 @@ extern "C" void deviceSearchFilter(PsiPhiArray &psi_phi_array, float *image_time searchFilterImages<<>>(psi_phi_array.get_meta_data(), psi_phi_array.get_gpu_array_ptr(), device_img_times, params, num_trajectories, device_tests, device_search_results); + cudaDeviceSynchronize(); // Read back results checkCudaErrors(cudaMemcpy(best_results, device_search_results, sizeof(Trajectory) * num_results, @@ -502,107 +520,6 @@ void deviceGetCoadds(const unsigned int num_images, const unsigned int width, co checkCudaErrors(cudaFree(device_res)); } -/* - void deviceGetCoadds(ImageStack &stack, PerImageData image_data, int num_trajectories, - Trajectory *trajectories, StampParameters params, - std::vector> &use_index_vect, float *results) { - // Allocate Device memory - Trajectory *device_trjs; - int *device_use_index = nullptr; - float *device_times; - float *device_img; - float *device_res; - - // Compute the dimensions for the data. - const unsigned int num_images = stack.img_count(); - const unsigned int width = stack.get_width(); - const unsigned int height = stack.get_height(); - const unsigned int num_image_pixels = num_images * width * height; - const unsigned int stamp_width = 2 * params.radius + 1; - const unsigned int stamp_ppi = (2 * params.radius + 1) * (2 * params.radius + 1); - const unsigned int num_stamp_pixels = num_trajectories * stamp_ppi; - - // Allocate and copy the trajectories. - checkCudaErrors(cudaMalloc((void **)&device_trjs, sizeof(Trajectory) * num_trajectories)); - checkCudaErrors(cudaMemcpy(device_trjs, trajectories, sizeof(Trajectory) * num_trajectories, - cudaMemcpyHostToDevice)); - - // Check if we need to create a vector of per-trajectory, per-image use. - // Convert the vector of booleans into an integer array so we do a cudaMemcpy. - if (use_index_vect.size() == num_trajectories) { - checkCudaErrors(cudaMalloc((void **)&device_use_index, sizeof(int) * num_images * num_trajectories)); - - int *start_ptr = device_use_index; - std::vector int_vect(num_images, 0); - for (unsigned i = 0; i < num_trajectories; ++i) { - assert(use_index_vect[i].size() == num_images); - for (unsigned t = 0; t < num_images; ++t) { - int_vect[t] = use_index_vect[i][t] ? 1 : 0; - } - - checkCudaErrors( - cudaMemcpy(start_ptr, int_vect.data(), sizeof(int) * num_images, cudaMemcpyHostToDevice)); - start_ptr += num_images; - } - } - - // Allocate and copy the times. - checkCudaErrors(cudaMalloc((void **)&device_times, sizeof(float) * num_images)); - checkCudaErrors(cudaMemcpy(device_times, image_data.image_times, sizeof(float) * num_images, - cudaMemcpyHostToDevice)); - - // Allocate and copy the images. - checkCudaErrors(cudaMalloc((void **)&device_img, sizeof(float) * num_image_pixels)); - float *next_ptr = device_img; - for (unsigned t = 0; t < num_images; ++t) { - // Used to be a vector of floats, now is an eigen::vector of floats or something - // but that's ok because all we use it for is the .data() -> float* - // I think this can also just directly go to .data because of RowMajor layout - auto& data_ref = stack.get_single_image(t).get_science().get_image(); - - assert(data_ref.size() == width * height); - checkCudaErrors(cudaMemcpy(next_ptr, data_ref.data(), sizeof(float) * width * height, - cudaMemcpyHostToDevice)); - next_ptr += width * height; - } - - // Allocate space for the results. - checkCudaErrors(cudaMalloc((void **)&device_res, sizeof(float) * num_stamp_pixels)); - - // Wrap the per-image data into a struct. This struct will be copied by value - // during the function call, so we don't need to allocate memory for the - // struct itself. We just set the pointers to the on device vectors. - PerImageData device_image_data; - device_image_data.num_images = num_images; - device_image_data.image_times = device_times; - device_image_data.psi_params = nullptr; - device_image_data.phi_params = nullptr; - - dim3 blocks(num_trajectories, 1, 1); - dim3 threads(1, stamp_width, stamp_width); - - // Create the stamps. - deviceGetCoaddStamp<<>>(num_images, width, height, device_img, device_image_data, - num_trajectories, device_trjs, params, device_use_index, - device_res); - cudaDeviceSynchronize(); - - // Free up the unneeded memory (everything except for the on-device results). - checkCudaErrors(cudaFree(device_img)); - if (device_use_index != nullptr) checkCudaErrors(cudaFree(device_use_index)); - checkCudaErrors(cudaFree(device_times)); - checkCudaErrors(cudaFree(device_trjs)); - cudaDeviceSynchronize(); - - // Read back results - checkCudaErrors( - cudaMemcpy(results, device_res, sizeof(float) * num_stamp_pixels, cudaMemcpyDeviceToHost)); - cudaDeviceSynchronize(); - - // Free the rest of the on GPU memory. - checkCudaErrors(cudaFree(device_res)); - } -*/ } /* namespace search */ #endif /* KERNELS_CU_ */ From 0839114ec797a8869f94cbd5e5821a9b1e3d9eb2 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 07:58:44 -0500 Subject: [PATCH 06/27] Fix bad merge --- src/kbmod/search/kernels.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kbmod/search/kernels.cu b/src/kbmod/search/kernels.cu index a70e41bcc..21bf19a9c 100644 --- a/src/kbmod/search/kernels.cu +++ b/src/kbmod/search/kernels.cu @@ -337,8 +337,8 @@ extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters par // Launch Search searchFilterImages<<>>(search_data.get_meta_data(), search_data.get_gpu_array_ptr(), - device_img_times, params, num_trajectories, device_tests, - device_search_results); + static_cast(search_data.get_gpu_time_array_ptr()), + params, num_trajectories, device_tests, device_search_results); cudaDeviceSynchronize(); // Read back results From ca87995c15e4438b64c947ed8d6b51a6e50f1bf9 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:11:29 -0500 Subject: [PATCH 07/27] Remove unused functions --- src/kbmod/search/common.h | 22 ---------------------- src/kbmod/search/stack_search.cpp | 24 ++++-------------------- src/kbmod/search/stack_search.h | 4 ---- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/src/kbmod/search/common.h b/src/kbmod/search/common.h index 394497930..b1e47eb66 100644 --- a/src/kbmod/search/common.h +++ b/src/kbmod/search/common.h @@ -26,18 +26,6 @@ constexpr float NO_DATA = -9999.0; enum StampType { STAMP_SUM = 0, STAMP_MEAN, STAMP_MEDIAN }; -// The position (in pixels) of a trajectory. -struct PixelPos { - float x; - float y; - - const std::string to_string() const { return "x: " + std::to_string(x) + " y: " + std::to_string(y); } - - const std::string to_yaml() const { - return "{x: " + std::to_string(x) + ", y: " + std::to_string(y) + "}"; - } -}; - /* * Data structure to represent an objects trajectory * through a stack of images @@ -59,7 +47,6 @@ struct Trajectory { // Get pixel positions from a zero-shifted time. float get_x_pos(float time) const { return x + time * vx; } float get_y_pos(float time) const { return y + time * vy; } - PixelPos get_pos(float time) const { return {x + time * vx, y + time * vy}; } // I can't believe string::format is not a thing until C++ 20 const std::string to_string() const { @@ -162,15 +149,6 @@ static void trajectory_bindings(py::module &m) { })); } -static void pixel_pos_bindings(py::module &m) { - py::class_(m, "PixelPos", pydocs::DOC_PixelPos) - .def(py::init<>()) - .def_readwrite("x", &PixelPos::x) - .def_readwrite("y", &PixelPos::y) - .def("__repr__", [](const PixelPos &p) { return "PixelPos(" + p.to_string() + ")"; }) - .def("__str__", &PixelPos::to_string); -} - static void image_moments_bindings(py::module &m) { py::class_(m, "ImageMoments", pydocs::DOC_ImageMoments) .def(py::init<>()) diff --git a/src/kbmod/search/stack_search.cpp b/src/kbmod/search/stack_search.cpp index ba31ef247..4c8777c84 100644 --- a/src/kbmod/search/stack_search.cpp +++ b/src/kbmod/search/stack_search.cpp @@ -159,21 +159,6 @@ void StackSearch::create_search_list(int angle_steps, int velocity_steps, float timer.stop(); } -Point StackSearch::get_trajectory_position(const Trajectory& t, int i) const { - float time = stack.get_zeroed_time(i); - return {t.x + time * t.vx, t.y + time * t.vy}; -} - -std::vector StackSearch::get_trajectory_positions(Trajectory& t) const { - std::vector results; - int num_times = stack.img_count(); - for (int i = 0; i < num_times; ++i) { - Point pos = get_trajectory_position(t, i); - results.push_back(pos); - } - return results; -} - std::vector StackSearch::create_curves(Trajectory t, const std::vector& imgs) { /*Create a lightcurve from an image along a trajectory * @@ -188,12 +173,13 @@ std::vector StackSearch::create_curves(Trajectory t, const std::vector lightcurve; lightcurve.reserve(img_size); - std::vector times = stack.build_zeroed_times(); for (int i = 0; i < img_size; ++i) { /* Do not use get_pixel_interp(), because results from create_curves must * be able to recover the same likelihoods as the ones reported by the - * gpu search.*/ - Point p({t.x + times[i] * t.vx + 0.5f, t.y + times[i] * t.vy + 0.5f}); + * gpu search. Shift by 0.5 pixels to center as done on GPU. */ + float time = stack.get_zeroed_time(i); + Point p{t.get_x_pos(time) + 0.5f, t.get_y_pos(time) + 0.5f}; + float pix_val = imgs[i].get_pixel(p.to_index()); if (pix_val == NO_DATA) pix_val = 0.0; lightcurve.push_back(pix_val); @@ -282,8 +268,6 @@ static void stack_search_bindings(py::module& m) { .def("get_imagestack", &ks::get_imagestack, py::return_value_policy::reference_internal, pydocs::DOC_StackSearch_get_imagestack) // For testings - .def("get_trajectory_position", &ks::get_trajectory_position, - pydocs::DOC_StackSearch_get_trajectory_position) .def("get_psi_curves", (std::vector(ks::*)(tj&)) & ks::get_psi_curves, pydocs::DOC_StackSearch_get_psi_curves) .def("get_phi_curves", (std::vector(ks::*)(tj&)) & ks::get_phi_curves, diff --git a/src/kbmod/search/stack_search.h b/src/kbmod/search/stack_search.h index 597f78850..fbb9568a1 100644 --- a/src/kbmod/search/stack_search.h +++ b/src/kbmod/search/stack_search.h @@ -50,10 +50,6 @@ class StackSearch { // Gets the vector of result trajectories. std::vector get_results(int start, int end); - // Get the predicted (pixel) positions for a given trajectory. - Point get_trajectory_position(const Trajectory& t, int i) const; - std::vector get_trajectory_positions(Trajectory& t) const; - // Filters the results based on various parameters. void filter_results(int min_observations); void filter_results_lh(float min_lh); From ddb56d7d4e3d7db2526be238eeaedd71c8b18ae4 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:27:57 -0500 Subject: [PATCH 08/27] small fixes --- src/kbmod/search/bindings.cpp | 1 - src/kbmod/search/common.h | 1 - 2 files changed, 2 deletions(-) diff --git a/src/kbmod/search/bindings.cpp b/src/kbmod/search/bindings.cpp index 4175c9639..225bf477b 100644 --- a/src/kbmod/search/bindings.cpp +++ b/src/kbmod/search/bindings.cpp @@ -37,7 +37,6 @@ PYBIND11_MODULE(search, m) { search::stack_search_bindings(m); search::stamp_creator_bindings(m); search::trajectory_bindings(m); - search::pixel_pos_bindings(m); search::image_moments_bindings(m); search::stamp_parameters_bindings(m); search::psi_phi_array_binding(m); diff --git a/src/kbmod/search/common.h b/src/kbmod/search/common.h index b1e47eb66..b7c89663e 100644 --- a/src/kbmod/search/common.h +++ b/src/kbmod/search/common.h @@ -133,7 +133,6 @@ static void trajectory_bindings(py::module &m) { .def_readwrite("obs_count", &tj::obs_count) .def("get_x_pos", &tj::get_x_pos, pydocs::DOC_Trajectory_get_x_pos) .def("get_y_pos", &tj::get_y_pos, pydocs::DOC_Trajectory_get_y_pos) - .def("get_pos", &tj::get_pos, pydocs::DOC_Trajectory_get_pos) .def("__repr__", [](const tj &t) { return "Trajectory(" + t.to_string() + ")"; }) .def("__str__", &tj::to_string) .def(py::pickle( From ea0dee6a5e12600dc5df9649431f8d3d163b17bc Mon Sep 17 00:00:00 2001 From: DinoBektesevic Date: Fri, 2 Feb 2024 12:14:00 -0800 Subject: [PATCH 09/27] Skip regression test when no GPU exists. --- tests/test_regression_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_regression_test.py b/tests/test_regression_test.py index fe8a08e0e..56c099a6e 100644 --- a/tests/test_regression_test.py +++ b/tests/test_regression_test.py @@ -511,6 +511,7 @@ def run_full_test(): # The unit test runner class test_regression_test(unittest.TestCase): + @unittest.skipIf(not HAS_GPU, "Skipping test (no GPU detected)") def test_run_test(self): self.assertTrue(run_full_test()) From e4a4a089a85e85ac1ab0f715caf9e6267f3855e5 Mon Sep 17 00:00:00 2001 From: DinoBektesevic Date: Fri, 2 Feb 2024 12:38:20 -0800 Subject: [PATCH 10/27] Fix trajectory position predictions in results. --- src/kbmod/trajectory_utils.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/kbmod/trajectory_utils.py b/src/kbmod/trajectory_utils.py index e05fde489..0459e1376 100644 --- a/src/kbmod/trajectory_utils.py +++ b/src/kbmod/trajectory_utils.py @@ -67,16 +67,23 @@ def trajectory_predict_skypos(trj, wcs, times): times : `list` or `numpy.ndarray` The times at which to predict the positions. + .. note:: + The motion is approximated as linear and will be approximately correct + only for small temporal range and spatial region. In essence, the new + coordinates are calculated as: + :math: x_new = x_old + v * (t_new - t_old) + Returns ------- result : `astropy.coordinates.SkyCoord` A SkyCoord with the transformed locations. """ - np_times = np.array(times) + dt = np.array(times) + dt -= dt[0] # Predict locations in pixel space. - x_vals = trj.x + trj.vx * np_times - y_vals = trj.y + trj.vy * np_times + x_vals = trj.x + trj.vx * dt + y_vals = trj.y + trj.vy * dt result = wcs.pixel_to_world(x_vals, y_vals) return result From 86a080fbc56c5411009bfde5492a34231c5e14ae Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:56:08 -0500 Subject: [PATCH 11/27] Initial cleanups --- src/kbmod/search/pydocs/raw_image_docs.h | 1 - src/kbmod/search/pydocs/stack_search_docs.h | 79 ++++++++++++++------- src/kbmod/search/stack_search.cpp | 6 -- src/kbmod/search/stack_search.h | 2 - tests/test_search.py | 50 ------------- 5 files changed, 54 insertions(+), 84 deletions(-) diff --git a/src/kbmod/search/pydocs/raw_image_docs.h b/src/kbmod/search/pydocs/raw_image_docs.h index 7fd2e6c88..09ad76190 100644 --- a/src/kbmod/search/pydocs/raw_image_docs.h +++ b/src/kbmod/search/pydocs/raw_image_docs.h @@ -195,7 +195,6 @@ static const auto DOC_RawImage_center_is_local_max = R"doc( Whether or not the stamp passes the check. )doc"; - static const auto DOC_RawImage_create_stamp = R"doc( Create an image stamp around a given region. diff --git a/src/kbmod/search/pydocs/stack_search_docs.h b/src/kbmod/search/pydocs/stack_search_docs.h index 865ff6103..f52e2aece 100644 --- a/src/kbmod/search/pydocs/stack_search_docs.h +++ b/src/kbmod/search/pydocs/stack_search_docs.h @@ -15,23 +15,48 @@ static const auto DOC_StackSearch_enable_gpu_sigmag_filter = R"doc( )doc"; static const auto DOC_StackSearch_enable_gpu_encoding = R"doc( - todo - )doc"; + Set the encoding level for the data copied to the GPU. + 1 = uint8 + 2 = uint16 + 4 or -1 = float -static const auto DOC_StackSearch_enable_corr = R"doc( - todo + Parameters + ---------- + encode_num_bytes : `int` + The number of bytes to use for encoding the data. )doc"; static const auto DOC_StackSearch_set_start_bounds_x = R"doc( - todo + Set the starting and ending bounds in the x direction for a grid search. + The grid search will test all pixels [x_min, x_max). + + Parameters + ---------- + x_min : `int` + The inclusive lower bound of the search. + x_max : `int` + The exclusive upper bound of the search. )doc"; static const auto DOC_StackSearch_set_start_bounds_y = R"doc( - todo + Set the starting and ending bounds in the y direction for a grid search. + The grid search will test all pixels [y_min, y_max). + + Parameters + ---------- + y_min : `int` + The inclusive lower bound of the search. + y_max : `int` + The exclusive upper bound of the search. )doc"; static const auto DOC_StackSearch_set_debug = R"doc( - todo + Set whether to dislpay debug output. + + Parameters + ---------- + d : `bool` + Set to ``True`` to turn on debug output and ``False`` to turn it off. )doc"; static const auto DOC_StackSearch_filter_min_obs = R"doc( @@ -55,35 +80,39 @@ static const auto DOC_StackSearch_get_image_npixels = R"doc( ")doc"; static const auto DOC_StackSearch_get_imagestack = R"doc( - todo + Return the `kb.ImageStack` containing the data to search. )doc"; -static const auto DOC_StackSearch_get_trajectory_position = R"doc( - todo - )doc"; +static const auto DOC_StackSearch_get_psi_curves = R"doc( + Return the time series of psi values for a given trajectory in pixel space. -static const auto DOC_StackSearch_get_trajectory_positions = R"doc( - todo - )doc"; + Parameters + ---------- + trj : `kb.Trajectory` + The input trajectory. -static const auto DOC_StackSearch_get_psi_curves = R"doc( - todo + Returns + ------- + result : `list` of `float` + The psi values at each time step with NO_DATA replaced by 0.0. )doc"; static const auto DOC_StackSearch_get_phi_curves = R"doc( - todo - )doc"; + Return the time series of phi values for a given trajectory in pixel space. -static const auto DOC_StackSearch_prepare_psi_phi = R"doc( - todo - )doc"; + Parameters + ---------- + trj : `kb.Trajectory` + The input trajectory. -static const auto DOC_StackSearch_get_psi_images = R"doc( - todo + Returns + ------- + result : `list` of `float` + The phi values at each time step with NO_DATA replaced by 0.0. )doc"; -static const auto DOC_StackSearch_get_phi_images = R"doc( - todo +static const auto DOC_StackSearch_prepare_psi_phi = R"doc( + Compute the cached psi and phi data. )doc"; static const auto DOC_StackSearch_get_results = R"doc( diff --git a/src/kbmod/search/stack_search.cpp b/src/kbmod/search/stack_search.cpp index 4c8777c84..eb7829304 100644 --- a/src/kbmod/search/stack_search.cpp +++ b/src/kbmod/search/stack_search.cpp @@ -209,10 +209,6 @@ std::vector StackSearch::get_phi_curves(Trajectory& t) { return create_curves(t, phi_images); } -std::vector& StackSearch::get_psi_images() { return psi_images; } - -std::vector& StackSearch::get_phi_images() { return phi_images; } - void StackSearch::sort_results() { __gnu_parallel::sort(results.begin(), results.end(), [](Trajectory a, Trajectory b) { return b.lh < a.lh; }); @@ -273,8 +269,6 @@ static void stack_search_bindings(py::module& m) { .def("get_phi_curves", (std::vector(ks::*)(tj&)) & ks::get_phi_curves, pydocs::DOC_StackSearch_get_phi_curves) .def("prepare_psi_phi", &ks::prepare_psi_phi, pydocs::DOC_StackSearch_prepare_psi_phi) - .def("get_psi_images", &ks::get_psi_images, pydocs::DOC_StackSearch_get_psi_images) - .def("get_phi_images", &ks::get_phi_images, pydocs::DOC_StackSearch_get_phi_images) .def("get_results", &ks::get_results, pydocs::DOC_StackSearch_get_results) .def("set_results", &ks::set_results, pydocs::DOC_StackSearch_set_results); } diff --git a/src/kbmod/search/stack_search.h b/src/kbmod/search/stack_search.h index fbb9568a1..48f3386d9 100644 --- a/src/kbmod/search/stack_search.h +++ b/src/kbmod/search/stack_search.h @@ -55,8 +55,6 @@ class StackSearch { void filter_results_lh(float min_lh); // Getters for the Psi and Phi data. - std::vector& get_psi_images(); - std::vector& get_phi_images(); std::vector get_psi_curves(Trajectory& t); std::vector get_phi_curves(Trajectory& t); diff --git a/tests/test_search.py b/tests/test_search.py index 7d27423cc..33c06cc82 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -87,56 +87,6 @@ def setUp(self): self.params.m02_limit = 35.5 self.params.m20_limit = 35.5 - def test_psiphi(self): - p = PSF(0.00001) - - # Image1 has a single object. - height = 19 - width = 5 - image1 = LayeredImage(width, height, 2.0, 4.0, 1.0, p) - add_fake_object(image1, 3.5, 2.5, 400.0, p) - - # Image2 has a single object and a masked pixel. - image2 = LayeredImage(width, height, 2.0, 4.0, 2.0, p) - add_fake_object(image2, 4.5, 2.5, 400.0, p) - - mask = image2.get_mask() - mask.set_pixel(9, 4, 1) - image2.apply_mask(1) - - # Create a stack from the two objects. - stack = ImageStack([image1, image2]) - search = StackSearch(stack) - - # Generate psi and phi. - search.prepare_psi_phi() - psi = search.get_psi_images() - phi = search.get_phi_images() - - # Test phi and psi for image1. - sci = image1.get_science() - var = image1.get_variance() - for x in range(width): - for y in range(height): - self.assertAlmostEqual( - psi[0].get_pixel(y, x), sci.get_pixel(y, x) / var.get_pixel(y, x), delta=1e-6 - ) - self.assertAlmostEqual(phi[0].get_pixel(y, x), 1.0 / var.get_pixel(y, x), delta=1e-6) - - # Test phi and psi for image2. - sci = image2.get_science() - var = image2.get_variance() - for x in range(width): - for y in range(height): - if x == 4 and y == 9: - self.assertFalse(psi[1].pixel_has_data(y, x)) - self.assertFalse(phi[1].pixel_has_data(y, x)) - else: - self.assertAlmostEqual( - psi[1].get_pixel(y, x), sci.get_pixel(y, x) / var.get_pixel(y, x), delta=1e-6 - ) - self.assertAlmostEqual(phi[1].get_pixel(y, x), 1.0 / var.get_pixel(y, x), delta=1e-6) - @unittest.skipIf(not HAS_GPU, "Skipping test (no GPU detected)") def test_results(self): self.search.search( From 43e82a83d5e811b3f7978ba6b24c5863bd6af0ad Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:20:57 -0500 Subject: [PATCH 12/27] Revert the name change --- src/kbmod/search/bindings.cpp | 4 +- src/kbmod/search/kernels.cu | 31 ++++----- src/kbmod/search/layered_image.cpp | 8 +-- .../{search_data.cpp => psi_phi_array.cpp} | 0 .../{search_data_ds.h => psi_phi_array_ds.h} | 24 +++---- ...rch_data_utils.h => psi_phi_array_utils.h} | 20 +++--- src/kbmod/search/pydocs/search_data_docs.h | 66 +++++++++---------- src/kbmod/search/stack_search.cpp | 7 +- src/kbmod/search/stack_search.h | 4 +- ...t_search_data.py => test_psi_phi_array.py} | 10 +-- 10 files changed, 88 insertions(+), 86 deletions(-) rename src/kbmod/search/{search_data.cpp => psi_phi_array.cpp} (100%) rename src/kbmod/search/{search_data_ds.h => psi_phi_array_ds.h} (91%) rename src/kbmod/search/{search_data_utils.h => psi_phi_array_utils.h} (52%) rename tests/{test_search_data.py => test_psi_phi_array.py} (98%) diff --git a/src/kbmod/search/bindings.cpp b/src/kbmod/search/bindings.cpp index c39343c2a..225bf477b 100644 --- a/src/kbmod/search/bindings.cpp +++ b/src/kbmod/search/bindings.cpp @@ -16,7 +16,7 @@ namespace py = pybind11; #include "stack_search.cpp" #include "stamp_creator.cpp" #include "kernel_testing_helpers.cpp" -#include "search_data.cpp" +#include "psi_phi_array.cpp" PYBIND11_MODULE(search, m) { m.attr("KB_NO_DATA") = pybind11::float_(search::NO_DATA); @@ -39,7 +39,7 @@ PYBIND11_MODULE(search, m) { search::trajectory_bindings(m); search::image_moments_bindings(m); search::stamp_parameters_bindings(m); - search::search_data_binding(m); + search::psi_phi_array_binding(m); // Functions from raw_image.cpp m.def("create_median_image", &search::create_median_image); m.def("create_summed_image", &search::create_summed_image); diff --git a/src/kbmod/search/kernels.cu b/src/kbmod/search/kernels.cu index 21bf19a9c..c400c4380 100644 --- a/src/kbmod/search/kernels.cu +++ b/src/kbmod/search/kernels.cu @@ -19,11 +19,11 @@ #include "common.h" #include "cuda_errors.h" -#include "search_data_ds.h" +#include "psi_phi_array_ds.h" namespace search { -extern "C" void device_allocate_search_data_arrays(SearchData *data) { +extern "C" void device_allocate_psi_phi_array_arrays(PsiPhiArray *data) { if (!data->cpu_array_allocated() || !data->cpu_time_array_allocated()) { throw std::runtime_error("CPU data is not allocated."); } @@ -47,7 +47,7 @@ extern "C" void device_allocate_search_data_arrays(SearchData *data) { data->set_gpu_time_array_ptr(device_times_ptr); } -extern "C" void device_free_search_data_arrays(SearchData *data) { +extern "C" void device_free_psi_phi_array_arrays(PsiPhiArray *data) { if (data->gpu_array_allocated()) { checkCudaErrors(cudaFree(data->get_gpu_array_ptr())); data->set_gpu_array_ptr(nullptr); @@ -58,8 +58,8 @@ extern "C" void device_free_search_data_arrays(SearchData *data) { } } -__host__ __device__ PsiPhi read_encoded_psi_phi(SearchDataMeta ¶ms, void *psi_phi_vect, int time, - int row, int col) { +__host__ __device__ PsiPhi read_encoded_psi_phi(PsiPhiArrayMeta ¶ms, void *psi_phi_vect, int time, + int row, int col) { // Bounds checking. if ((row < 0) || (col < 0) || (row >= params.height) || (col >= params.width)) { return {NO_DATA, NO_DATA}; @@ -141,11 +141,12 @@ extern "C" __device__ __host__ void SigmaGFilteredIndicesCU(float *values, int n /* * Evaluate the likelihood score (as computed with from the psi and phi values) for a single - * given candidate trajectory. Modifies the trajectory in place to update the number of + * given candidate trajectory. Modifies the trajectory in place to update the number of * observations, likelihood, and flux. */ -extern "C" __device__ __host__ void evaluateTrajectory(SearchDataMeta psi_phi_meta, void *psi_phi_vect, float *image_times, - SearchParameters params, Trajectory *candidate) { +extern "C" __device__ __host__ void evaluateTrajectory(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_vect, + float *image_times, SearchParameters params, + Trajectory *candidate) { // Data structures used for filtering. We fill in only what we need. float psi_array[MAX_NUM_IMAGES]; float phi_array[MAX_NUM_IMAGES]; @@ -222,7 +223,7 @@ extern "C" __device__ __host__ void evaluateTrajectory(SearchDataMeta psi_phi_me * * Creates a local copy of psi_phi_meta and params in local memory space. */ -__global__ void searchFilterImages(SearchDataMeta psi_phi_meta, void *psi_phi_vect, float *image_times, +__global__ void searchFilterImages(PsiPhiArrayMeta psi_phi_meta, void *psi_phi_vect, float *image_times, SearchParameters params, int num_trajectories, Trajectory *trajectories, Trajectory *results) { // Get the x and y coordinates within the search space. @@ -291,23 +292,23 @@ __global__ void searchFilterImages(SearchDataMeta psi_phi_meta, void *psi_phi_ve } } -extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters params, int num_trajectories, +extern "C" void deviceSearchFilter(PsiPhiArray &psi_phi_array, SearchParameters params, int num_trajectories, Trajectory *trj_to_search, int num_results, Trajectory *best_results) { // Allocate Device memory Trajectory *device_tests; Trajectory *device_search_results; // Check the hard coded maximum number of images against the num_images. - int num_images = search_data.get_num_times(); + int num_images = psi_phi_array.get_num_times(); if (num_images > MAX_NUM_IMAGES) { throw std::runtime_error("Number of images exceeds GPU maximum."); } // Check that the device vectors have already been allocated. - if (search_data.gpu_array_allocated() == false) { + if (psi_phi_array.gpu_array_allocated() == false) { throw std::runtime_error("PsiPhi data has not been created."); } - if (search_data.gpu_time_array_allocated() == false) { + if (psi_phi_array.gpu_time_array_allocated() == false) { throw std::runtime_error("GPU time data has not been created."); } @@ -336,8 +337,8 @@ extern "C" void deviceSearchFilter(SearchData &search_data, SearchParameters par dim3 threads(THREAD_DIM_X, THREAD_DIM_Y); // Launch Search - searchFilterImages<<>>(search_data.get_meta_data(), search_data.get_gpu_array_ptr(), - static_cast(search_data.get_gpu_time_array_ptr()), + searchFilterImages<<>>(psi_phi_array.get_meta_data(), psi_phi_array.get_gpu_array_ptr(), + static_cast(psi_phi_array.get_gpu_time_array_ptr()), params, num_trajectories, device_tests, device_search_results); cudaDeviceSynchronize(); diff --git a/src/kbmod/search/layered_image.cpp b/src/kbmod/search/layered_image.cpp index 903ec82ad..ba7efbb06 100644 --- a/src/kbmod/search/layered_image.cpp +++ b/src/kbmod/search/layered_image.cpp @@ -40,12 +40,12 @@ LayeredImage::LayeredImage(const RawImage& sci, const RawImage& var, const RawIm variance = var; } -LayeredImage::LayeredImage(unsigned w, unsigned h, float noise_stdev, float pixel_variance, - double time, const PSF& psf) +LayeredImage::LayeredImage(unsigned w, unsigned h, float noise_stdev, float pixel_variance, double time, + const PSF& psf) : LayeredImage(w, h, noise_stdev, pixel_variance, time, psf, -1) {} -LayeredImage::LayeredImage(unsigned w, unsigned h, float noise_stdev, float pixel_variance, - double time, const PSF& psf, int seed) +LayeredImage::LayeredImage(unsigned w, unsigned h, float noise_stdev, float pixel_variance, double time, + const PSF& psf, int seed) : psf(psf), width(w), height(h) { std::random_device r; std::default_random_engine generator(r()); diff --git a/src/kbmod/search/search_data.cpp b/src/kbmod/search/psi_phi_array.cpp similarity index 100% rename from src/kbmod/search/search_data.cpp rename to src/kbmod/search/psi_phi_array.cpp diff --git a/src/kbmod/search/search_data_ds.h b/src/kbmod/search/psi_phi_array_ds.h similarity index 91% rename from src/kbmod/search/search_data_ds.h rename to src/kbmod/search/psi_phi_array_ds.h index 7a64842ce..477b17041 100644 --- a/src/kbmod/search/search_data_ds.h +++ b/src/kbmod/search/psi_phi_array_ds.h @@ -1,5 +1,5 @@ /* - * search_data_ds.h + * psi_phi_array_ds.h * * The data structure for the raw data needed for the search algorith, * including the psi/phi values and the zeroed times. The the data @@ -17,8 +17,8 @@ * Created on: Dec 5, 2023 */ -#ifndef SEARCH_DATA_DS_ -#define SEARCH_DATA_DS_ +#ifndef PSI_PHI_ARRAY_DS_ +#define PSI_PHI_ARRAY_DS_ #include #include @@ -44,8 +44,8 @@ inline float decode_uint_scalar(float value, float min_val, float scale) { return (value == 0.0) ? NO_DATA : (value - 1.0) * scale + min_val; } -// The struct of meta data for the SearchData. -struct SearchDataMeta { +// The struct of meta data for the PsiPhiArray. +struct PsiPhiArrayMeta { int num_times = 0; int width = 0; int height = 0; @@ -66,17 +66,17 @@ struct SearchDataMeta { float phi_scale = 1.0; }; -/* SearchData is a class to hold the psi and phi arrays for the CPU and GPU as well as +/* PsiPhiArray is a class to hold the psi and phi arrays for the CPU and GPU as well as the meta data and functions to do encoding and decoding on CPU. */ -class SearchData { +class PsiPhiArray { public: - explicit SearchData(); - virtual ~SearchData(); + explicit PsiPhiArray(); + virtual ~PsiPhiArray(); void clear(); - inline SearchDataMeta& get_meta_data() { return meta_data; } + inline PsiPhiArrayMeta& get_meta_data() { return meta_data; } // --- Getter functions (for Python interface) ---------------- inline int get_num_bytes() { return meta_data.num_bytes; } @@ -121,7 +121,7 @@ class SearchData { inline void set_gpu_time_array_ptr(float* new_ptr) { gpu_time_array = new_ptr; } private: - SearchDataMeta meta_data; + PsiPhiArrayMeta meta_data; // Pointers to the arrays void* cpu_array_ptr = nullptr; @@ -132,4 +132,4 @@ class SearchData { } /* namespace search */ -#endif /* SEARCH_DATA_DS_ */ +#endif /* PSI_PHI_ARRAY_DS_ */ diff --git a/src/kbmod/search/search_data_utils.h b/src/kbmod/search/psi_phi_array_utils.h similarity index 52% rename from src/kbmod/search/search_data_utils.h rename to src/kbmod/search/psi_phi_array_utils.h index 683895ce4..3573df268 100644 --- a/src/kbmod/search/search_data_utils.h +++ b/src/kbmod/search/psi_phi_array_utils.h @@ -1,5 +1,5 @@ /* - * search_data_utils.h + * psi_phi_array_utils.h * * The utility functions for the psi/phi array. Broken out from the header * data structure so that it can use packages that won't be imported into the @@ -8,8 +8,8 @@ * Created on: Dec 8, 2023 */ -#ifndef SEARCH_DATA_UTILS_ -#define SEARCH_DATA_UTILS_ +#ifndef PSI_PHI_ARRAY_UTILS_ +#define PSI_PHI_ARRAY_UTILS_ #include #include @@ -19,7 +19,7 @@ #include "common.h" #include "image_stack.h" #include "layered_image.h" -#include "search_data_ds.h" +#include "psi_phi_array_ds.h" #include "raw_image.h" namespace search { @@ -27,13 +27,13 @@ namespace search { // Compute the min, max, and scale parameter from the a vector of image data. std::array compute_scale_params_from_image_vect(const std::vector& imgs, int num_bytes); -void fill_search_data(SearchData& result_data, int num_bytes, const std::vector& psi_imgs, - const std::vector& phi_imgs, const std::vector zeroed_times, - bool debug = false); +void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vector& psi_imgs, + const std::vector& phi_imgs, const std::vector zeroed_times, + bool debug = false); -void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, - bool debug = false); +void fill_psi_phi_array_from_image_stack(PsiPhiArray& result_data, ImageStack& stack, int num_bytes, + bool debug = false); } /* namespace search */ -#endif /* SEARCH_DATA_UTILS_ */ +#endif /* PSI_PHI_ARRAY_UTILS_ */ diff --git a/src/kbmod/search/pydocs/search_data_docs.h b/src/kbmod/search/pydocs/search_data_docs.h index 4f668e3ea..05abe3053 100644 --- a/src/kbmod/search/pydocs/search_data_docs.h +++ b/src/kbmod/search/pydocs/search_data_docs.h @@ -1,5 +1,5 @@ -#ifndef SEARCH_DATA_DOCS -#define SEARCH_DATA_DOCS +#ifndef PSI_PHI_ARRAY_DOCS +#define PSI_PHI_ARRAY_DOCS namespace pydocs { @@ -14,88 +14,88 @@ static const auto DOC_PsiPhi = R"doc( The phi value at a pixel. )doc"; -static const auto DOC_SearchData = R"doc( +static const auto DOC_PsiPhiArray = R"doc( An encoded array of Psi and Phi values along with their meta data. )doc"; -static const auto DOC_SearchData_get_num_bytes = R"doc( +static const auto DOC_PsiPhiArray_get_num_bytes = R"doc( The target number of bytes to use for encoding the data (1 for uint8, 2 for uint16, or 4 for float32). Might differ from actual number of bytes (block_size). )doc"; -static const auto DOC_SearchData_get_num_times = R"doc( +static const auto DOC_PsiPhiArray_get_num_times = R"doc( The number of times. )doc"; -static const auto DOC_SearchData_get_width = R"doc( +static const auto DOC_PsiPhiArray_get_width = R"doc( The image width. )doc"; -static const auto DOC_SearchData_get_height = R"doc( +static const auto DOC_PsiPhiArray_get_height = R"doc( The image height. )doc"; -static const auto DOC_SearchData_get_pixels_per_image = R"doc( +static const auto DOC_PsiPhiArray_get_pixels_per_image = R"doc( The number of pixels per each image. )doc"; -static const auto DOC_SearchData_get_num_entries = R"doc( +static const auto DOC_PsiPhiArray_get_num_entries = R"doc( The number of array entries. )doc"; -static const auto DOC_SearchData_get_total_array_size = R"doc( +static const auto DOC_PsiPhiArray_get_total_array_size = R"doc( The size of the array in bytes. )doc"; -static const auto DOC_SearchData_get_block_size = R"doc( +static const auto DOC_PsiPhiArray_get_block_size = R"doc( The size of a single entry in bytes. )doc"; -static const auto DOC_SearchData_get_psi_min_val = R"doc( +static const auto DOC_PsiPhiArray_get_psi_min_val = R"doc( The minimum value of psi used in the scaling computations. )doc"; -static const auto DOC_SearchData_get_psi_max_val = R"doc( +static const auto DOC_PsiPhiArray_get_psi_max_val = R"doc( The maximum value of psi used in the scaling computations. )doc"; -static const auto DOC_SearchData_get_psi_scale = R"doc( +static const auto DOC_PsiPhiArray_get_psi_scale = R"doc( The scaling parameter for psi. )doc"; -static const auto DOC_SearchData_get_phi_min_val = R"doc( +static const auto DOC_PsiPhiArray_get_phi_min_val = R"doc( The minimum value of phi used in the scaling computations. )doc"; -static const auto DOC_SearchData_get_phi_max_val = R"doc( +static const auto DOC_PsiPhiArray_get_phi_max_val = R"doc( The maximum value of phi used in the scaling computations. )doc"; -static const auto DOC_SearchData_get_phi_scale = R"doc( +static const auto DOC_PsiPhiArray_get_phi_scale = R"doc( The scaling parameter for phi. )doc"; -static const auto DOC_SearchData_get_cpu_array_allocated = R"doc( +static const auto DOC_PsiPhiArray_get_cpu_array_allocated = R"doc( A Boolean indicating whether the cpu data (psi/phi) array exists. )doc"; -static const auto DOC_SearchData_get_gpu_array_allocated = R"doc( +static const auto DOC_PsiPhiArray_get_gpu_array_allocated = R"doc( A Boolean indicating whether the gpu data (psi/phi) array exists. )doc"; -static const auto DOC_SearchData_get_cpu_time_array_allocated = R"doc( +static const auto DOC_PsiPhiArray_get_cpu_time_array_allocated = R"doc( A Boolean indicating whether the cpu time array exists. )doc"; -static const auto DOC_SearchData_get_gpu_time_array_allocated = R"doc( +static const auto DOC_PsiPhiArray_get_gpu_time_array_allocated = R"doc( A Boolean indicating whether the gpu time array exists. )doc"; -static const auto DOC_SearchData_clear = R"doc( +static const auto DOC_PsiPhiArray_clear = R"doc( Clear all data and free the arrays. )doc"; -static const auto DOC_SearchData_read_psi_phi = R"doc( +static const auto DOC_PsiPhiArray_read_psi_phi = R"doc( Read a PsiPhi value from the CPU array. Parameters @@ -113,7 +113,7 @@ static const auto DOC_SearchData_read_psi_phi = R"doc( The pixel values. )doc"; -static const auto DOC_SearchData_read_time = R"doc( +static const auto DOC_PsiPhiArray_read_time = R"doc( Read a zeroed time value from the CPU array. Parameters @@ -127,9 +127,9 @@ static const auto DOC_SearchData_read_time = R"doc( The time. )doc"; -static const auto DOC_SearchData_set_meta_data = R"doc( +static const auto DOC_PsiPhiArray_set_meta_data = R"doc( Set the meta data for the array. Automatically called by - fill_search_data(). + fill_psi_phi_array(). Parameters ---------- @@ -143,12 +143,12 @@ static const auto DOC_SearchData_set_meta_data = R"doc( The width of each image in pixels. )doc"; -static const auto DOC_SearchData_fill_search_data = R"doc( - Fill the SearchData from Psi and Phi images. +static const auto DOC_PsiPhiArray_fill_psi_phi_array = R"doc( + Fill the PsiPhiArray from Psi and Phi images. Parameters ---------- - result_data : `SearchData` + result_data : `PsiPhiArray` The location to store the data. num_bytes : `int` The type of encoding to use (1, 2, or 4). @@ -160,12 +160,12 @@ static const auto DOC_SearchData_fill_search_data = R"doc( A list of floating point times starting at zero. )doc"; -static const auto DOC_SearchData_fill_search_data_from_image_stack = R"doc( - Fill the SearchData an ImageStack. +static const auto DOC_PsiPhiArray_fill_psi_phi_array_from_image_stack = R"doc( + Fill the PsiPhiArray an ImageStack. Parameters ---------- - result_data : `SearchData` + result_data : `PsiPhiArray` The location to store the data. num_bytes : `int` The type of encoding to use (1, 2, or 4). @@ -175,4 +175,4 @@ static const auto DOC_SearchData_fill_search_data_from_image_stack = R"doc( } // namespace pydocs -#endif /* SEARCH_DATA_DOCS */ +#endif /* PSI_PHI_ARRAY_DOCS */ diff --git a/src/kbmod/search/stack_search.cpp b/src/kbmod/search/stack_search.cpp index 401177d62..c945b4507 100644 --- a/src/kbmod/search/stack_search.cpp +++ b/src/kbmod/search/stack_search.cpp @@ -2,7 +2,7 @@ namespace search { #ifdef HAVE_CUDA -extern "C" void deviceSearchFilter(SearchData& search_data, SearchParameters params, int num_trajectories, +extern "C" void deviceSearchFilter(PsiPhiArray& psi_phi_array, SearchParameters params, int num_trajectories, Trajectory* trj_to_search, int num_results, Trajectory* best_results); #endif @@ -75,8 +75,9 @@ void StackSearch::search(int ang_steps, int vel_steps, float min_ang, float max_ DebugTimer psi_phi_timer = DebugTimer("Creating psi/phi buffers", debug_info); prepare_psi_phi(); - SearchData psi_phi_data; - fill_search_data(psi_phi_data, params.encode_num_bytes, psi_images, phi_images, image_times, debug_info); + PsiPhiArray psi_phi_data; + fill_psi_phi_array(psi_phi_data, params.encode_num_bytes, psi_images, phi_images, image_times, + debug_info); psi_phi_timer.stop(); // Allocate a vector for the results. diff --git a/src/kbmod/search/stack_search.h b/src/kbmod/search/stack_search.h index ca7ebdd9f..fbb9568a1 100644 --- a/src/kbmod/search/stack_search.h +++ b/src/kbmod/search/stack_search.h @@ -16,8 +16,8 @@ #include "geom.h" #include "image_stack.h" #include "psf.h" -#include "search_data_ds.h" -#include "search_data_utils.h" +#include "psi_phi_array_ds.h" +#include "psi_phi_array_utils.h" #include "pydocs/stack_search_docs.h" #include "stamp_creator.h" diff --git a/tests/test_search_data.py b/tests/test_psi_phi_array.py similarity index 98% rename from tests/test_search_data.py rename to tests/test_psi_phi_array.py index 80b847365..0e805dd7c 100644 --- a/tests/test_search_data.py +++ b/tests/test_psi_phi_array.py @@ -9,7 +9,7 @@ ImageStack, LayeredImage, PsiPhi, - SearchData, + PsiPhiArray, RawImage, compute_scale_params_from_image_vect, decode_uint_scalar, @@ -39,7 +39,7 @@ def setUp(self): self.zeroed_times = [0.0, 1.0] def test_set_meta_data(self): - arr = SearchData() + arr = PsiPhiArray() self.assertEqual(arr.num_times, 0) self.assertEqual(arr.num_bytes, 4) self.assertEqual(arr.width, 0) @@ -130,7 +130,7 @@ def test_compute_scale_params_from_image_vect(self): def test_fill_search_data(self): for num_bytes in [2, 4]: - arr = SearchData() + arr = PsiPhiArray() fill_search_data( arr, num_bytes, [self.psi_1, self.psi_2], [self.phi_1, self.phi_2], self.zeroed_times, False ) @@ -191,8 +191,8 @@ def test_fill_search_data_from_image_stack(self): ) im_stack = ImageStack(images) - # Create the SearchData from the ImageStack. - arr = SearchData() + # Create the PsiPhiArray from the ImageStack. + arr = PsiPhiArray() fill_search_data_from_image_stack(arr, im_stack, 4, False) # Check the meta data. From a0605b6db638729a78904fb841b1cf52a1f5ca28 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:23:41 -0500 Subject: [PATCH 13/27] Update psi_phi_array.cpp --- src/kbmod/search/psi_phi_array.cpp | 112 ++++++++++++++--------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/src/kbmod/search/psi_phi_array.cpp b/src/kbmod/search/psi_phi_array.cpp index ae2e1c425..329d7493e 100644 --- a/src/kbmod/search/psi_phi_array.cpp +++ b/src/kbmod/search/psi_phi_array.cpp @@ -1,25 +1,25 @@ -#include "search_data_ds.h" -#include "search_data_utils.h" -#include "pydocs/search_data_docs.h" +#include "psi_phi_array_ds.h" +#include "psi_phi_array_utils.h" +#include "pydocs/psi_phi_array_docs.h" namespace search { // Declaration of CUDA functions that will be linked in. #ifdef HAVE_CUDA -extern "C" void device_allocate_search_data_arrays(SearchData* data); +extern "C" void device_allocate_psi_phi_array_arrays(PsiPhiArray* data); -extern "C" void device_free_search_data_arrays(SearchData* data); +extern "C" void device_free_psi_phi_array_arrays(PsiPhiArray* data); #endif // ------------------------------------------------------- // --- Implementation of core data structure functions --- // ------------------------------------------------------- -SearchData::SearchData() {} +PsiPhiArray::PsiPhiArray() {} -SearchData::~SearchData() { clear(); } +PsiPhiArray::~PsiPhiArray() { clear(); } -void SearchData::clear() { +void PsiPhiArray::clear() { // Free all used memory on CPU and GPU. if (cpu_array_ptr != nullptr) { free(cpu_array_ptr); @@ -31,7 +31,7 @@ void SearchData::clear() { } #ifdef HAVE_CUDA if ((gpu_array_ptr != nullptr) || (gpu_time_array != nullptr)) { - device_free_search_data_arrays(this); + device_free_psi_phi_array_arrays(this); gpu_array_ptr = nullptr; gpu_time_array = nullptr; } @@ -54,7 +54,7 @@ void SearchData::clear() { meta_data.phi_scale = 1.0; } -void SearchData::set_meta_data(int new_num_bytes, int new_num_times, int new_height, int new_width) { +void PsiPhiArray::set_meta_data(int new_num_bytes, int new_num_times, int new_height, int new_width) { // Validity checking of parameters. if (new_num_bytes != -1 && new_num_bytes != 1 && new_num_bytes != 2 && new_num_bytes != 4) { throw std::runtime_error("Invalid setting of num_bytes. Must be (-1 [use default], 1, 2, or 4)."); @@ -86,7 +86,7 @@ void SearchData::set_meta_data(int new_num_bytes, int new_num_times, int new_hei meta_data.total_array_size = meta_data.block_size * meta_data.num_entries; } -void SearchData::set_psi_scaling(float min_val, float max_val, float scale_val) { +void PsiPhiArray::set_psi_scaling(float min_val, float max_val, float scale_val) { if (min_val > max_val) throw std::runtime_error("Min value needs to be < max value"); if (scale_val <= 0) throw std::runtime_error("Scale value must be greater than zero."); meta_data.psi_min_val = min_val; @@ -94,7 +94,7 @@ void SearchData::set_psi_scaling(float min_val, float max_val, float scale_val) meta_data.psi_scale = scale_val; } -void SearchData::set_phi_scaling(float min_val, float max_val, float scale_val) { +void PsiPhiArray::set_phi_scaling(float min_val, float max_val, float scale_val) { if (min_val > max_val) throw std::runtime_error("Min value needs to be < max value"); if (scale_val <= 0) throw std::runtime_error("Scale value must be greater than zero."); meta_data.phi_min_val = min_val; @@ -102,7 +102,7 @@ void SearchData::set_phi_scaling(float min_val, float max_val, float scale_val) meta_data.phi_scale = scale_val; } -PsiPhi SearchData::read_psi_phi(int time, int row, int col) { +PsiPhi PsiPhiArray::read_psi_phi(int time, int row, int col) { PsiPhi result = {NO_DATA, NO_DATA}; // Array allocation and bounds checking. @@ -136,7 +136,7 @@ PsiPhi SearchData::read_psi_phi(int time, int row, int col) { return result; } -float SearchData::read_time(int time_index) { +float PsiPhiArray::read_time(int time_index) { if (cpu_time_array == nullptr) throw std::runtime_error("Read from unallocated times array."); if ((time_index < 0) || (time_index >= meta_data.num_times)) { throw std::runtime_error("Out of bounds read for time step."); @@ -175,8 +175,8 @@ std::array compute_scale_params_from_image_vect(const std::vector -void set_encode_cpu_search_data(SearchData& data, const std::vector& psi_imgs, - const std::vector& phi_imgs, bool debug) { +void set_encode_cpu_psi_phi_array(PsiPhiArray& data, const std::vector& psi_imgs, + const std::vector& phi_imgs, bool debug) { if (data.get_cpu_array_ptr() != nullptr) { throw std::runtime_error("CPU PsiPhi already allocated."); } @@ -219,8 +219,8 @@ void set_encode_cpu_search_data(SearchData& data, const std::vector& p data.set_cpu_array_ptr((void*)encoded); } -void set_float_cpu_search_data(SearchData& data, const std::vector& psi_imgs, - const std::vector& phi_imgs, bool debug) { +void set_float_cpu_psi_phi_array(PsiPhiArray& data, const std::vector& psi_imgs, + const std::vector& phi_imgs, bool debug) { if (data.get_cpu_array_ptr() != nullptr) { throw std::runtime_error("CPU PsiPhi already allocated."); } @@ -245,9 +245,9 @@ void set_float_cpu_search_data(SearchData& data, const std::vector& ps data.set_cpu_array_ptr((void*)encoded); } -void fill_search_data(SearchData& result_data, int num_bytes, const std::vector& psi_imgs, - const std::vector& phi_imgs, const std::vector zeroed_times, - bool debug) { +void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vector& psi_imgs, + const std::vector& phi_imgs, const std::vector zeroed_times, + bool debug) { if (result_data.get_cpu_array_ptr() != nullptr) { return; } @@ -282,16 +282,16 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< // Do the local encoding. if (result_data.get_num_bytes() == 1) { - set_encode_cpu_search_data(result_data, psi_imgs, phi_imgs, debug); + set_encode_cpu_psi_phi_array(result_data, psi_imgs, phi_imgs, debug); } else { - set_encode_cpu_search_data(result_data, psi_imgs, phi_imgs, debug); + set_encode_cpu_psi_phi_array(result_data, psi_imgs, phi_imgs, debug); } } else { if (debug) { printf("Encoding psi and phi as floats.\n"); } // Just interleave psi and phi images. - set_float_cpu_search_data(result_data, psi_imgs, phi_imgs, debug); + set_float_cpu_psi_phi_array(result_data, psi_imgs, phi_imgs, debug); } // Copy the time array. @@ -313,7 +313,7 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< printf("Allocating GPU memory for times array using %lu bytes.\n", times_bytes); } - device_allocate_search_data_arrays(&result_data); + device_allocate_psi_phi_array_arrays(&result_data); if (result_data.get_gpu_array_ptr() == nullptr) { throw std::runtime_error("Unable to allocate GPU PsiPhi array."); } @@ -323,8 +323,8 @@ void fill_search_data(SearchData& result_data, int num_bytes, const std::vector< #endif } -void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stack, int num_bytes, - bool debug) { +void fill_psi_phi_array_from_image_stack(PsiPhiArray& result_data, ImageStack& stack, int num_bytes, + bool debug) { // Compute Phi and Psi from convolved images while leaving masked pixels alone // Reinsert 0s for NO_DATA? std::vector psi_images; @@ -346,7 +346,7 @@ void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stac // Convert these into an array form. Needs the full psi and phi computed first so the // encoding can compute the bounds of each array. std::vector zeroed_times = stack.build_zeroed_times(); - fill_search_data(result_data, num_bytes, psi_images, phi_images, zeroed_times, debug); + fill_psi_phi_array(result_data, num_bytes, psi_images, phi_images, zeroed_times, debug); } // ------------------------------------------- @@ -354,55 +354,55 @@ void fill_search_data_from_image_stack(SearchData& result_data, ImageStack& stac // ------------------------------------------- #ifdef Py_PYTHON_H -static void search_data_binding(py::module& m) { - using ppa = search::SearchData; +static void psi_phi_array_binding(py::module& m) { + using ppa = search::PsiPhiArray; py::class_(m, "PsiPhi", pydocs::DOC_PsiPhi) .def(py::init<>()) .def_readwrite("psi", &search::PsiPhi::psi) .def_readwrite("phi", &search::PsiPhi::phi); - py::class_(m, "SearchData", pydocs::DOC_SearchData) + py::class_(m, "PsiPhiArray", pydocs::DOC_PsiPhiArray) .def(py::init<>()) - .def_property_readonly("num_bytes", &ppa::get_num_bytes, pydocs::DOC_SearchData_get_num_bytes) - .def_property_readonly("num_times", &ppa::get_num_times, pydocs::DOC_SearchData_get_num_times) - .def_property_readonly("width", &ppa::get_width, pydocs::DOC_SearchData_get_width) - .def_property_readonly("height", &ppa::get_height, pydocs::DOC_SearchData_get_height) + .def_property_readonly("num_bytes", &ppa::get_num_bytes, pydocs::DOC_PsiPhiArray_get_num_bytes) + .def_property_readonly("num_times", &ppa::get_num_times, pydocs::DOC_PsiPhiArray_get_num_times) + .def_property_readonly("width", &ppa::get_width, pydocs::DOC_PsiPhiArray_get_width) + .def_property_readonly("height", &ppa::get_height, pydocs::DOC_PsiPhiArray_get_height) .def_property_readonly("pixels_per_image", &ppa::get_pixels_per_image, - pydocs::DOC_SearchData_get_pixels_per_image) + pydocs::DOC_PsiPhiArray_get_pixels_per_image) .def_property_readonly("num_entries", &ppa::get_num_entries, - pydocs::DOC_SearchData_get_num_entries) + pydocs::DOC_PsiPhiArray_get_num_entries) .def_property_readonly("total_array_size", &ppa::get_total_array_size, - pydocs::DOC_SearchData_get_total_array_size) - .def_property_readonly("block_size", &ppa::get_block_size, pydocs::DOC_SearchData_get_block_size) + pydocs::DOC_PsiPhiArray_get_total_array_size) + .def_property_readonly("block_size", &ppa::get_block_size, pydocs::DOC_PsiPhiArray_get_block_size) .def_property_readonly("psi_min_val", &ppa::get_psi_min_val, - pydocs::DOC_SearchData_get_psi_min_val) + pydocs::DOC_PsiPhiArray_get_psi_min_val) .def_property_readonly("psi_max_val", &ppa::get_psi_max_val, - pydocs::DOC_SearchData_get_psi_max_val) - .def_property_readonly("psi_scale", &ppa::get_psi_scale, pydocs::DOC_SearchData_get_psi_scale) + pydocs::DOC_PsiPhiArray_get_psi_max_val) + .def_property_readonly("psi_scale", &ppa::get_psi_scale, pydocs::DOC_PsiPhiArray_get_psi_scale) .def_property_readonly("phi_min_val", &ppa::get_phi_min_val, - pydocs::DOC_SearchData_get_phi_min_val) + pydocs::DOC_PsiPhiArray_get_phi_min_val) .def_property_readonly("phi_max_val", &ppa::get_phi_max_val, - pydocs::DOC_SearchData_get_phi_max_val) - .def_property_readonly("phi_scale", &ppa::get_phi_scale, pydocs::DOC_SearchData_get_phi_scale) + pydocs::DOC_PsiPhiArray_get_phi_max_val) + .def_property_readonly("phi_scale", &ppa::get_phi_scale, pydocs::DOC_PsiPhiArray_get_phi_scale) .def_property_readonly("cpu_array_allocated", &ppa::cpu_array_allocated, - pydocs::DOC_SearchData_get_cpu_array_allocated) + pydocs::DOC_PsiPhiArray_get_cpu_array_allocated) .def_property_readonly("gpu_array_allocated", &ppa::gpu_array_allocated, - pydocs::DOC_SearchData_get_gpu_array_allocated) + pydocs::DOC_PsiPhiArray_get_gpu_array_allocated) .def_property_readonly("cpu_time_array_allocated", &ppa::cpu_time_array_allocated, - pydocs::DOC_SearchData_get_cpu_time_array_allocated) + pydocs::DOC_PsiPhiArray_get_cpu_time_array_allocated) .def_property_readonly("gpu_time_array_allocated", &ppa::gpu_time_array_allocated, - pydocs::DOC_SearchData_get_gpu_time_array_allocated) - .def("set_meta_data", &ppa::set_meta_data, pydocs::DOC_SearchData_set_meta_data) - .def("clear", &ppa::clear, pydocs::DOC_SearchData_clear) - .def("read_psi_phi", &ppa::read_psi_phi, pydocs::DOC_SearchData_read_psi_phi) - .def("read_time", &ppa::read_time, pydocs::DOC_SearchData_read_time); + pydocs::DOC_PsiPhiArray_get_gpu_time_array_allocated) + .def("set_meta_data", &ppa::set_meta_data, pydocs::DOC_PsiPhiArray_set_meta_data) + .def("clear", &ppa::clear, pydocs::DOC_PsiPhiArray_clear) + .def("read_psi_phi", &ppa::read_psi_phi, pydocs::DOC_PsiPhiArray_read_psi_phi) + .def("read_time", &ppa::read_time, pydocs::DOC_PsiPhiArray_read_time); m.def("compute_scale_params_from_image_vect", &search::compute_scale_params_from_image_vect); m.def("decode_uint_scalar", &search::decode_uint_scalar); m.def("encode_uint_scalar", &search::encode_uint_scalar); - m.def("fill_search_data", &search::fill_search_data, pydocs::DOC_SearchData_fill_search_data); - m.def("fill_search_data_from_image_stack", &search::fill_search_data_from_image_stack, - pydocs::DOC_SearchData_fill_search_data_from_image_stack); + m.def("fill_psi_phi_array", &search::fill_psi_phi_array, pydocs::DOC_PsiPhiArray_fill_psi_phi_array); + m.def("fill_psi_phi_array_from_image_stack", &search::fill_psi_phi_array_from_image_stack, + pydocs::DOC_PsiPhiArray_fill_psi_phi_array_from_image_stack); } #endif From 089778242689439f88c59155c98082f91837c192 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:24:43 -0500 Subject: [PATCH 14/27] Rename file --- .../search/pydocs/{search_data_docs.h => psi_phi_array_docs.h} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/kbmod/search/pydocs/{search_data_docs.h => psi_phi_array_docs.h} (100%) diff --git a/src/kbmod/search/pydocs/search_data_docs.h b/src/kbmod/search/pydocs/psi_phi_array_docs.h similarity index 100% rename from src/kbmod/search/pydocs/search_data_docs.h rename to src/kbmod/search/pydocs/psi_phi_array_docs.h From f0062d9adc89b85e20c6204cf16d0527ebc7e402 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:27:14 -0500 Subject: [PATCH 15/27] Fix names from bad replace --- src/kbmod/search/kernels.cu | 4 ++-- src/kbmod/search/psi_phi_array.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/kbmod/search/kernels.cu b/src/kbmod/search/kernels.cu index c400c4380..50c497754 100644 --- a/src/kbmod/search/kernels.cu +++ b/src/kbmod/search/kernels.cu @@ -23,7 +23,7 @@ namespace search { -extern "C" void device_allocate_psi_phi_array_arrays(PsiPhiArray *data) { +extern "C" void device_allocate_psi_phi_arrays(PsiPhiArray *data) { if (!data->cpu_array_allocated() || !data->cpu_time_array_allocated()) { throw std::runtime_error("CPU data is not allocated."); } @@ -47,7 +47,7 @@ extern "C" void device_allocate_psi_phi_array_arrays(PsiPhiArray *data) { data->set_gpu_time_array_ptr(device_times_ptr); } -extern "C" void device_free_psi_phi_array_arrays(PsiPhiArray *data) { +extern "C" void device_free_psi_phi_arrays(PsiPhiArray *data) { if (data->gpu_array_allocated()) { checkCudaErrors(cudaFree(data->get_gpu_array_ptr())); data->set_gpu_array_ptr(nullptr); diff --git a/src/kbmod/search/psi_phi_array.cpp b/src/kbmod/search/psi_phi_array.cpp index 329d7493e..a7bad84e3 100644 --- a/src/kbmod/search/psi_phi_array.cpp +++ b/src/kbmod/search/psi_phi_array.cpp @@ -6,9 +6,9 @@ namespace search { // Declaration of CUDA functions that will be linked in. #ifdef HAVE_CUDA -extern "C" void device_allocate_psi_phi_array_arrays(PsiPhiArray* data); +extern "C" void device_allocate_psi_phi_arrays(PsiPhiArray* data); -extern "C" void device_free_psi_phi_array_arrays(PsiPhiArray* data); +extern "C" void device_free_psi_phi_arrays(PsiPhiArray* data); #endif // ------------------------------------------------------- From 5d1d76cd4dce4cffec362f040b6e367987d33cb0 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:28:37 -0500 Subject: [PATCH 16/27] Update test_psi_phi_array.py --- tests/test_psi_phi_array.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_psi_phi_array.py b/tests/test_psi_phi_array.py index 0e805dd7c..c261e4edb 100644 --- a/tests/test_psi_phi_array.py +++ b/tests/test_psi_phi_array.py @@ -14,12 +14,12 @@ compute_scale_params_from_image_vect, decode_uint_scalar, encode_uint_scalar, - fill_search_data, - fill_search_data_from_image_stack, + fill_psi_phi_array, + fill_psi_phi_array_from_image_stack, ) -class test_search_data(unittest.TestCase): +class test_psi_phi_array(unittest.TestCase): def setUp(self): self.num_times = 2 self.width = 4 @@ -128,10 +128,10 @@ def test_compute_scale_params_from_image_vect(self): self.assertAlmostEqual(result_uint16[1], max_val, delta=1e-5) self.assertAlmostEqual(result_uint16[2], max_val / 65535.0, delta=1e-5) - def test_fill_search_data(self): + def test_fill_psi_phi_array(self): for num_bytes in [2, 4]: arr = PsiPhiArray() - fill_search_data( + fill_psi_phi_array( arr, num_bytes, [self.psi_1, self.psi_2], [self.phi_1, self.phi_2], self.zeroed_times, False ) @@ -173,7 +173,7 @@ def test_fill_search_data(self): self.assertFalse(arr.gpu_array_allocated) self.assertFalse(arr.gpu_time_array_allocated) - def test_fill_search_data_from_image_stack(self): + def test_fill_psi_phi_array_from_image_stack(self): # Build a fake image stack. num_times = 5 width = 21 @@ -193,7 +193,7 @@ def test_fill_search_data_from_image_stack(self): # Create the PsiPhiArray from the ImageStack. arr = PsiPhiArray() - fill_search_data_from_image_stack(arr, im_stack, 4, False) + fill_psi_phi_array_from_image_stack(arr, im_stack, 4, False) # Check the meta data. self.assertEqual(arr.num_times, num_times) From 17105527ffb9462af17c2b4c76b9e6d48f884b2b Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:31:04 -0500 Subject: [PATCH 17/27] Update psi_phi_array.cpp --- src/kbmod/search/psi_phi_array.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kbmod/search/psi_phi_array.cpp b/src/kbmod/search/psi_phi_array.cpp index a7bad84e3..e0640e9da 100644 --- a/src/kbmod/search/psi_phi_array.cpp +++ b/src/kbmod/search/psi_phi_array.cpp @@ -31,7 +31,7 @@ void PsiPhiArray::clear() { } #ifdef HAVE_CUDA if ((gpu_array_ptr != nullptr) || (gpu_time_array != nullptr)) { - device_free_psi_phi_array_arrays(this); + device_free_psi_phi_arrays(this); gpu_array_ptr = nullptr; gpu_time_array = nullptr; } @@ -313,7 +313,7 @@ void fill_psi_phi_array(PsiPhiArray& result_data, int num_bytes, const std::vect printf("Allocating GPU memory for times array using %lu bytes.\n", times_bytes); } - device_allocate_psi_phi_array_arrays(&result_data); + device_allocate_psi_phi_arrays(&result_data); if (result_data.get_gpu_array_ptr() == nullptr) { throw std::runtime_error("Unable to allocate GPU PsiPhi array."); } From 0119b7c347715e769bc9051dc5f7a96cb484fc85 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:56:30 -0500 Subject: [PATCH 18/27] Fix precision error --- src/kbmod/search/image_stack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kbmod/search/image_stack.cpp b/src/kbmod/search/image_stack.cpp index cf4244a19..0ba33319c 100644 --- a/src/kbmod/search/image_stack.cpp +++ b/src/kbmod/search/image_stack.cpp @@ -47,7 +47,7 @@ float ImageStack::get_zeroed_time(int index) const { std::vector ImageStack::build_zeroed_times() const { std::vector zeroed_times = std::vector(); if (images.size() > 0) { - float t0 = images[0].get_obstime(); + double t0 = images[0].get_obstime(); for (auto& i : images) { zeroed_times.push_back(i.get_obstime() - t0); } From 328e021bd13845e9990b73d91d4fc99c0ecf4f27 Mon Sep 17 00:00:00 2001 From: DinoBektesevic Date: Sun, 4 Feb 2024 00:48:38 -0800 Subject: [PATCH 19/27] Python<3.9 does not support OR operator for dictionaries, skip test. --- tests/test_std_config.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_std_config.py b/tests/test_std_config.py index 5df4e946a..0110e657c 100644 --- a/tests/test_std_config.py +++ b/tests/test_std_config.py @@ -1,3 +1,5 @@ +import sys + import unittest from kbmod import StandardizerConfig @@ -27,7 +29,6 @@ def test_config(self): conf["a"] = 10 self.assertEqual(conf["a"], 10) - self.assertEqual(conf2 | conf, expected) self.assertEqual(list(iter(conf)), ["a", "b", "c"]) # Test .update method @@ -45,3 +46,14 @@ def test_config(self): with self.assertRaises(TypeError): conf2.update([1, 2, 3]) + + @unittest.skipIf(sys.version_info < (3, 9), "py<3.9 does not support or for dicts.") + def test_or(self): + expected = {"a": 1, "b": 2, "c": 3} + conf = StandardizerConfig(expected) + conf2 = StandardizerConfig(a=1, b=2, c=3) + self.assertEqual(conf2 | conf, expected) + + +if __name__ == "__main__": + unittest.main() From b349e5dd34d2af4d79213e6792a1c7fa5c9fd99a Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:02:41 -0500 Subject: [PATCH 20/27] Clean up StackSearch --- src/kbmod/search/pydocs/stack_search_docs.h | 4 --- src/kbmod/search/stack_search.cpp | 27 ++++++--------------- src/kbmod/search/stack_search.h | 9 ++----- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/kbmod/search/pydocs/stack_search_docs.h b/src/kbmod/search/pydocs/stack_search_docs.h index f52e2aece..8e2c5bd00 100644 --- a/src/kbmod/search/pydocs/stack_search_docs.h +++ b/src/kbmod/search/pydocs/stack_search_docs.h @@ -59,10 +59,6 @@ static const auto DOC_StackSearch_set_debug = R"doc( Set to ``True`` to turn on debug output and ``False`` to turn it off. )doc"; -static const auto DOC_StackSearch_filter_min_obs = R"doc( - todo - )doc"; - static const auto DOC_StackSearch_get_num_images = R"doc( "Returns the number of images to process. ")doc"; diff --git a/src/kbmod/search/stack_search.cpp b/src/kbmod/search/stack_search.cpp index cf06968a6..b75030ae2 100644 --- a/src/kbmod/search/stack_search.cpp +++ b/src/kbmod/search/stack_search.cpp @@ -68,7 +68,8 @@ void StackSearch::set_start_bounds_y(int y_min, int y_max) { void StackSearch::search(int ang_steps, int vel_steps, float min_ang, float max_ang, float min_vel, float mavx, int min_observations) { DebugTimer core_timer = DebugTimer("Running core search", debug_info); - create_search_list(ang_steps, vel_steps, min_ang, max_ang, min_vel, mavx); + std::vector search_list = + create_grid_search_list(ang_steps, vel_steps, min_ang, max_ang, min_vel, mavx); // Create a data stucture for the per-image data. std::vector image_times = stack.build_zeroed_times(); @@ -132,8 +133,9 @@ void StackSearch::prepare_psi_phi() { } } -void StackSearch::create_search_list(int angle_steps, int velocity_steps, float min_ang, float max_ang, - float min_vel, float mavx) { +std::vector StackSearch::create_grid_search_list(int angle_steps, int velocity_steps, + float min_ang, float max_ang, float min_vel, + float mavx) { DebugTimer timer = DebugTimer("Creating search candidate list", debug_info); std::vector angles(angle_steps); @@ -149,7 +151,7 @@ void StackSearch::create_search_list(int angle_steps, int velocity_steps, float } int trajCount = angle_steps * velocity_steps; - search_list = std::vector(trajCount); + std::vector search_list = std::vector(trajCount); for (int a = 0; a < angle_steps; ++a) { for (int v = 0; v < velocity_steps; ++v) { search_list[a * velocity_steps + v].vx = cos(angles[a]) * velocities[v]; @@ -157,6 +159,8 @@ void StackSearch::create_search_list(int angle_steps, int velocity_steps, float } } timer.stop(); + + return search_list; } std::vector StackSearch::create_curves(Trajectory t, const std::vector& imgs) { @@ -214,20 +218,6 @@ void StackSearch::sort_results() { [](Trajectory a, Trajectory b) { return b.lh < a.lh; }); } -void StackSearch::filter_results(int min_observations) { - results.erase(std::remove_if(results.begin(), results.end(), - std::bind([](Trajectory t, int cutoff) { return t.obs_count < cutoff; }, - std::placeholders::_1, min_observations)), - results.end()); -} - -void StackSearch::filter_results_lh(float min_lh) { - results.erase(std::remove_if(results.begin(), results.end(), - std::bind([](Trajectory t, float cutoff) { return t.lh < cutoff; }, - std::placeholders::_1, min_lh)), - results.end()); -} - std::vector StackSearch::get_results(int start, int count) { if (start + count >= results.size()) { count = results.size() - start; @@ -256,7 +246,6 @@ static void stack_search_bindings(py::module& m) { .def("set_start_bounds_x", &ks::set_start_bounds_x, pydocs::DOC_StackSearch_set_start_bounds_x) .def("set_start_bounds_y", &ks::set_start_bounds_y, pydocs::DOC_StackSearch_set_start_bounds_y) .def("set_debug", &ks::set_debug, pydocs::DOC_StackSearch_set_debug) - .def("filter_min_obs", &ks::filter_results, pydocs::DOC_StackSearch_filter_min_obs) .def("get_num_images", &ks::num_images, pydocs::DOC_StackSearch_get_num_images) .def("get_image_width", &ks::get_image_width, pydocs::DOC_StackSearch_get_image_width) .def("get_image_height", &ks::get_image_height, pydocs::DOC_StackSearch_get_image_height) diff --git a/src/kbmod/search/stack_search.h b/src/kbmod/search/stack_search.h index 48f3386d9..72d971dd4 100644 --- a/src/kbmod/search/stack_search.h +++ b/src/kbmod/search/stack_search.h @@ -50,10 +50,6 @@ class StackSearch { // Gets the vector of result trajectories. std::vector get_results(int start, int end); - // Filters the results based on various parameters. - void filter_results(int min_observations); - void filter_results_lh(float min_lh); - // Getters for the Psi and Phi data. std::vector get_psi_curves(Trajectory& t); std::vector get_phi_curves(Trajectory& t); @@ -71,13 +67,12 @@ class StackSearch { std::vector create_curves(Trajectory t, const std::vector& imgs); // Creates list of trajectories to search. - void create_search_list(int angle_steps, int velocity_steps, float min_ang, float max_ang, float min_vel, - float max_vel); + std::vector create_grid_search_list(int angle_steps, int velocity_steps, float min_ang, + float max_ang, float min_vel, float mavx); bool psi_phi_generated; bool debug_info; ImageStack stack; - std::vector search_list; std::vector psi_images; std::vector phi_images; std::vector results; From 4ed4643318e572713492f823309c6679019985c8 Mon Sep 17 00:00:00 2001 From: Colin Orion Chandler Date: Tue, 6 Feb 2024 20:29:21 -0800 Subject: [PATCH 21/27] New Notebooks and updated Region Search Notebook (#455) * Plot correction, clarifying text disable gridlines for the sky plotting * Create Region Searching Workbook.ipynb This contains the essence of Region Search for now. * Creating a structure for scratch notebooks Brainstorming, demos, and testing to share. * Update RegionSearchTesting.ipynb black --target-version py38 --line-length 110 is what works, but only if black was installed with [jupyter] * Update Region Searching Workbook.ipynb Consolidation into a single Pandas dataframe, Notebook cleanup, master function in preparation for a demo, TODO items added/updated, and a Next Steps added to the end. --- .../Region Searching Workbook.ipynb | 2938 +++++++++++++++++ .../coc/RegionSearchTesting.ipynb | 973 ++++++ notebooks/region_search/sky_patches.ipynb | 7 +- 3 files changed, 3915 insertions(+), 3 deletions(-) create mode 100644 notebooks/region_search/Region Searching Workbook.ipynb create mode 100644 notebooks/region_search/coc/RegionSearchTesting.ipynb diff --git a/notebooks/region_search/Region Searching Workbook.ipynb b/notebooks/region_search/Region Searching Workbook.ipynb new file mode 100644 index 000000000..5d7e6da65 --- /dev/null +++ b/notebooks/region_search/Region Searching Workbook.ipynb @@ -0,0 +1,2938 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d94d8d2b", + "metadata": {}, + "source": [ + "# Butler Interface for User\n", + "\n", + "The point of this notebook is to do step-by-step exploration of the DEEP dataset that was first run through KBMOD for the first set of papers. That work was led by Hayden Smotherman, hence the reference to that name.\n", + "\n", + "The \"Steven\" name that appears is Steven Stetzler, who inherited the responsibilities of properly re/processing the entire DEEP dataset through data acquired through 2023.\n", + "\n", + "### Basic Idea\n", + "Here we are connecting to an existing Butler/repository with the intent of carrying out queries to find data suitable for KBMOD to use.\n", + "\n", + "#### FAQ\n", + "\n", + "Q: Who wrote this, and when?\\\n", + "A: Colin Orion Chandler (coc123@uw.edu), late January 2024.\n", + "\n", + "Q: What are the basic requirements to run this?\\\n", + "A: (a) Epyc access, (b) with the appropriate (LSST w_2022_06) weekly build active, (c) permissions to access the repo_path mentioned below, and (d) an acceptable Kernel in the Jupyter-Hub of Epyc (user or global).\n", + "\n", + "Q: Why don't we just query the DB (e.g., PostGres, SQLite) to get what we need?\\\n", + "A: Because (a) the underlying schema can (and has/does) change, and (b) the underlying DB does not contain views, meaning that we would have to recreate the very complex relational mapping ourselves. Moreover, aside from (b) being very challenging, the relationship mapping recreation would fail due to the reasons mentioned in (a)." + ] + }, + { + "cell_type": "markdown", + "id": "9ea4ebd7", + "metadata": {}, + "source": [ + "### Initial Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 339, + "id": "d0189084", + "metadata": {}, + "outputs": [], + "source": [ + "# Import packages needed to run the notebook\n", + "import lsst\n", + "import lsst.daf.butler as dafButler\n", + "import os\n", + "import time\n", + "from matplotlib import pyplot as plt\n", + "import progressbar\n", + "from concurrent.futures import ProcessPoolExecutor, as_completed\n", + "from astropy.time import Time # for converting Butler visitInfo.date (TAI) to UTC strings\n", + "from astropy import units as u\n", + "import pandas as pd\n", + "import pickle\n", + "from dateutil import parser" + ] + }, + { + "cell_type": "code", + "execution_count": 476, + "id": "ceeec168", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "96 CPUs were reported as available by the multiprocessing module.\n" + ] + } + ], + "source": [ + "# We will use some kind of multiprocessing in a few places. Let's see what the sytsem thinks we have available.\n", + "# NOTE: we could set limits on executors later using this value, if desired. 2/6/2024 COC\n", + "\n", + "import multiprocessing\n", + "\n", + "available_cpus = multiprocessing.cpu_count()\n", + "print(f\"{available_cpus} CPUs were reported as available by the multiprocessing module.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "693492d4", + "metadata": {}, + "outputs": [], + "source": [ + "# this code will run in different environments, so we need somewhere (not the working directory) to save output\n", + "basedir = f'{os.environ[\"HOME\"]}/kbmod_tmp'\n", + "os.makedirs(basedir, exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b13eb927", + "metadata": {}, + "outputs": [], + "source": [ + "# set up the Butler\n", + "# NOTE: the repo path contains configs that point us to the underlying registry (DB)\n", + "# NOTE: do NOT assume there is write protection! (i.e., don't make changes)\n", + "repo_path = f\"/epyc/users/smotherh/DEEP/PointingGroups/butler-repo\"\n", + "butler = dafButler.Butler(repo_path)" + ] + }, + { + "cell_type": "markdown", + "id": "85cc47d8", + "metadata": {}, + "source": [ + "#### Collections\n", + "1. Explore the available collections\n", + "2. Construct a list of the collections containing the data we care about.\n", + "\n", + "Different collections hold different datasets.\n", + "\n", + "For the Hayden DEEP repo, the collections we are concerned with are organized by pointing groups (discrete regions on the sky).\n", + "\n", + "We want to figure out what those are, how they are named, and what else is available.\n", + "\n", + "We will also dump a full list of collection names to disk. \\\n", + "NOTE: as of 2/1/2024, there are 1,292 named collections in the repo.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 465, + "id": "e6a546f0", + "metadata": {}, + "outputs": [], + "source": [ + "def get_collection_names(butler, basedir, verbose=False, export=True):\n", + " \"\"\"\n", + " Making this a function 2/6/2024 COC.\n", + " \"\"\"\n", + " all_collection_names = []\n", + "\n", + " for c in sorted(butler.registry.queryCollections(\"*\")):\n", + " all_collection_names.append(c)\n", + "\n", + " if export == True:\n", + " outfile = f\"{basedir}/all_collection_names.lst\"\n", + " with open(outfile, \"w\") as f:\n", + " for c in all_collection_names:\n", + " print(c, file=f)\n", + "\n", + " if verbose:\n", + " message = f\"Found {len(all_collection_names)} collections in the Butler.\"\n", + " if export == True:\n", + " message += f' Wrote to \"{outfile}\".'\n", + " print(message)\n", + " return all_collection_names" + ] + }, + { + "cell_type": "code", + "execution_count": 466, + "id": "3006e4fd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 1292 collections in the Butler. Wrote to \"/astro/users/coc123/kbmod_tmp/all_collection_names.lst\".\n" + ] + } + ], + "source": [ + "all_collection_names = get_collection_names(butler=butler, basedir=basedir, verbose=True, export=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 468, + "id": "b409c810", + "metadata": {}, + "outputs": [], + "source": [ + "# We looked through the collections already.\n", + "# We will string manipulate to get to what we need.\n", + "# Previously, we used a list file on disk. (This could be a better option for some users.)\n", + "\n", + "\n", + "def get_desired_collections(all_collections_list, desired_collection_list=None):\n", + " \"\"\"\n", + " Produce a list of collections that will be used for querying the Butler.\n", + "\n", + " If desired_collection_list is None, then a hard-wired \"default\" approach\n", + " (for Haden/DEEP) is carried out, requiring:\n", + " 1. \"Pointing\" must be in the collection name.\n", + " 2. \"/imdiff_r/\" must be in the collection name.\n", + " 3. \"/2021\" may not be in the collection name.\n", + "\n", + " Otherwise, desired_collection_list can be either\n", + " 1. a Python list of desired collection names, or\n", + " 2. a filename (ending in .lst) that specifies the desired collections.\n", + " Either way, the collection names are verified against the (required) collections_list.\n", + "\n", + " Made this into a function 2/6/2024 COC.\n", + "\n", + " NOTE/TODO: untested are the supplied list and list file approaches.\n", + " \"\"\"\n", + "\n", + " desired_collections = []\n", + "\n", + " if desired_collection_list == None:\n", + " for collection_name in all_collection_names:\n", + " if (\n", + " \"Pointing\" in collection_name\n", + " and \"/imdiff_r\" in collection_name\n", + " and \"/2021\" not in collection_name\n", + " ):\n", + " desired_collections.append(collection_name)\n", + " else:\n", + " if type(desired_collection_list) == type(\"\"):\n", + " with open(desired_collection_list, \"r\") as f:\n", + " for line in f:\n", + " desired_collections.append(line.strip())\n", + " else:\n", + " for entry in desired_collection_list:\n", + " desired_collections.append(entry)\n", + " #\n", + " # Validate entries\n", + " for entry in desired_collections:\n", + " if entry not in all_collections_list:\n", + " raise KeyError(f'\"{entry}\" is not in the master list of collections supplied.')\n", + " #\n", + " return desired_collections" + ] + }, + { + "cell_type": "code", + "execution_count": 473, + "id": "dc9a4efc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['PointingGroup006/imdiff_r',\n", + " 'PointingGroup008/imdiff_r',\n", + " 'PointingGroup009/imdiff_r',\n", + " 'PointingGroup016/imdiff_r',\n", + " 'PointingGroup018/imdiff_r',\n", + " 'PointingGroup019/imdiff_r',\n", + " 'PointingGroup021/imdiff_r',\n", + " 'PointingGroup023/imdiff_r']" + ] + }, + "execution_count": 473, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "desired_collections = get_desired_collections(all_collections_list=all_collection_names)\n", + "desired_collections" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "4537e06a", + "metadata": {}, + "outputs": [], + "source": [ + "# ASIDE: there is a set of collections with dates in their names\n", + "# (e.g., PointingGroup006/imdiff_r/20211110T184421Z).\n", + "# Those (only?) have the following datasetTypes, (so we use the name without the date for all the datasetTypes):\n", + "# Across all collections, we see the following numbers by datasetType:\n", + "# 47383 deepDiff_diaSrc\n", + "# 8 deepDiff_diaSrc_schema\n", + "# 47383 deepDiff_differenceExp\n", + "# 29445 deepDiff_warpedExp\n", + "# 8 imageDifference_config\n", + "# 17942 imageDifference_log\n", + "# 47383 imageDifference_metadata\n", + "# 8 packages\n", + "\n", + "# which is too little? we we will snag the parent Collections (stripping dates)" + ] + }, + { + "cell_type": "markdown", + "id": "2fcdae7b", + "metadata": {}, + "source": [ + "#### datasetTypes\n", + "\n", + "Here we explore another dimension of the Butler: datasetType.\n", + "\n", + "tldr; we just care about one currently: deepDiff_differenceExp" + ] + }, + { + "cell_type": "code", + "execution_count": 169, + "id": "47c8c37c", + "metadata": {}, + "outputs": [], + "source": [ + "# TIMING NOTE: about 2 minutes here\n", + "#\n", + "# Maybe a KBMOD user must know which datasetType(s) they need?\n", + "# Here we show how to explore them to figure that out.\n", + "#\n", + "# NOTE: we tested two approaches,\n", + "# (a) iterating over desired_collections, and\n", + "# (b) supply desired_collections.\n", + "# The output was the same, but the iterating method took 268s, and the supplied method took 97s.\n", + "# There *was* a point where we thought we were seeing different results depending on the approach,\n", + "# but this has been sorted out. (COC suspects a testing break in a loop somwhere.)\n", + "# COC NTS: [TODO] function, [TODO] caching\n", + "\n", + "\n", + "def getDatasetTypeStats(butler, overwrite=False):\n", + " \"\"\"\n", + " Get information on all datasetTypes found in a Butler.\n", + " TODO implement caching if desired. If not, get rid of overwrite option.\n", + " 2/1/2024 COC\n", + " \"\"\"\n", + " datasetTypes = {}\n", + "\n", + " import glob\n", + "\n", + " cache_file = f\"{basedir}/dataset_types.csv\"\n", + " cache_exists = False\n", + " if len(glob.glob(cache_file)) > 0:\n", + " cache_exists = True\n", + "\n", + " if overwrite == False and cache_exists == True:\n", + " print(f\"Recycling {cache_file} as overwrite was False...\")\n", + " with open(cache_file, \"r\") as f:\n", + " for line in f:\n", + " print(line)\n", + " line = line.strip().split(\",\")\n", + " datasetTypes[line[0]] = int(line[1])\n", + " print(f\"Read {len(datasetTypes)} datasetTypes from disk.\")\n", + " return datasetTypes\n", + "\n", + " q = sorted(butler.registry.queryDatasetTypes())\n", + "\n", + " with progressbar.ProgressBar(max_value=len(q)) as bar:\n", + " for j, dt in enumerate(q):\n", + " n = 0\n", + " for i, ref in enumerate(\n", + " butler.registry.queryDatasets(datasetType=dt, collections=desired_collections)\n", + " ):\n", + " n += 1\n", + " if n > 0:\n", + " if dt.name not in datasetTypes:\n", + " datasetTypes[dt.name] = 0\n", + " datasetTypes[dt.name] += n\n", + " bar.update(j)\n", + "\n", + " if cache_exists == False or overwrite == True:\n", + " print(f\"Saving {len(datasetTypes)} datasetTypes to {cache_file} now...\")\n", + " with open(cache_file, \"w\") as f:\n", + " for key in datasetTypes:\n", + " print(f\"{key},{datasetTypes[key]}\", file=f)\n", + " else:\n", + " print(f\"Saw {len(datasetTypes)} datasetTypes.\")\n", + " return datasetTypes" + ] + }, + { + "cell_type": "code", + "execution_count": 170, + "id": "b741015f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100% (129 of 129) |######################| Elapsed Time: 0:02:28 Time: 0:02:28\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving 46 datasetTypes to /astro/users/coc123/kbmod_tmp/dataset_types.csv now...\n", + "CPU times: user 1min 57s, sys: 2.96 s, total: 2min\n", + "Wall time: 2min 28s\n" + ] + } + ], + "source": [ + "%%time\n", + "# TIMING NOTE: this takes roughly 2.5 minutes without cache (2/6/2024 COC)\n", + "datasetTypes = getDatasetTypeStats(butler=butler, overwrite=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6406f04c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Across all collections, we see the following numbers by datasetType: \n", + "8 assembleCoadd_config\n", + "268 assembleCoadd_log\n", + "700 assembleCoadd_metadata\n", + "122856 cal_ref_cat\n", + "47403 calexp\n", + "47403 calexpBackground\n", + "8 calibrate_config\n", + "17961 calibrate_log\n", + "47403 calibrate_metadata\n", + "8 characterizeImage_config\n", + "18290 characterizeImage_log\n", + "47423 characterizeImage_metadata\n", + "693 deepCoadd\n", + "167085 deepCoadd_directWarp\n", + "693 deepCoadd_inputMap\n", + "167085 deepCoadd_psfMatchedWarp\n", + "47383 deepDiff_diaSrc\n", + "8 deepDiff_diaSrc_schema\n", + "47383 deepDiff_differenceExp\n", + "29445 deepDiff_warpedExp\n", + "524283 gaia_DR1_v1\n", + "47423 icExp\n", + "47423 icExpBackground\n", + "47423 icSrc\n", + "8 icSrc_schema\n", + "8 imageDifference_config\n", + "17942 imageDifference_log\n", + "47383 imageDifference_metadata\n", + "8 isr_config\n", + "18290 isr_log\n", + "48422 isr_metadata\n", + "8 makeWarp_config\n", + "64924 makeWarp_log\n", + "167085 makeWarp_metadata\n", + "48422 overscanRaw\n", + "8 overscan_config\n", + "18290 overscan_log\n", + "48422 overscan_metadata\n", + "32 packages\n", + "48422 postISRCCD\n", + "130924 ps1_pv3_3pi_20170110\n", + "48422 raw\n", + "1 skyMap\n", + "47403 src\n", + "47403 srcMatch\n", + "8 src_schema\n" + ] + } + ], + "source": [ + "# The number of records for each datasetType.\n", + "# This may be especially useful for users who do not yet know which datasetType(s) they need.\n", + "\n", + "print(f\"Across all collections, we see the following numbers by datasetType: \")\n", + "for dt in datasetTypes:\n", + " print(f\"{datasetTypes[dt]!s:10} {dt.name}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "53a03c75", + "metadata": {}, + "outputs": [], + "source": [ + "# This is the datasetType that we care about.\n", + "# NOTE: we should allow this to be a list in case a user needs multiple.\n", + "desired_datasetTypes = [\"deepDiff_differenceExp\"]" + ] + }, + { + "cell_type": "markdown", + "id": "3c351fc7", + "metadata": {}, + "source": [ + "### Visit Detector Region (VDR) Querying\n", + "\n", + "Here we will query the Butler and extract all of the \"visit_detector_region\" information.\\\n", + "At this stage we are gathering:\n", + "1. dataIds for all future queries\n", + "2. lsst.sphgeom.region objects" + ] + }, + { + "cell_type": "code", + "execution_count": 608, + "id": "c47e5588", + "metadata": {}, + "outputs": [], + "source": [ + "def get_vdr_data(butler, desired_collections, desired_datasetTypes):\n", + " \"\"\"\n", + "\n", + " Made as function 2/6/2024 COC.\n", + " \"\"\"\n", + " # VDR === Visit Detector Region\n", + " # VDRs hold what we need in terms of region hashes and unique dataIds.\n", + " # NOTE: this typically takes < 5s to run 2/1/2024 COC\n", + " # NOTE: tried iterating over desired_collections vs supplying desired_collections; same output 2/1/2024 COC\n", + "\n", + " vdr_dict = {\"data_id\": [], \"region\": [], \"detector\": []}\n", + " # vdr_ids = []\n", + " # vdr_regions = []\n", + " # vdr_detectors = []\n", + "\n", + " for dt in desired_datasetTypes:\n", + " datasetRefs = butler.registry.queryDimensionRecords(\n", + " \"visit_detector_region\", datasets=dt, collections=desired_collections\n", + " )\n", + " for ref in datasetRefs:\n", + " vdr_dict[\"data_id\"].append(ref.dataId)\n", + " vdr_dict[\"region\"].append(\n", + " ref.region\n", + " ) # keeping as objects for now; should .encode() for caching/export\n", + " vdr_dict[\"detector\"].append(ref.detector) # 2/2/2024 COC\n", + " # BUT if we decided to export this or cache this, we should write the encode() version to disk\n", + " #\n", + " example_vdr_ref = ref # this leaves a VDR Python object we can play with\n", + " # other data available:\n", + " # id = ref.id# id -- e.g., 1592350 (for DEEP dataset, I think UUIDs for newer Butlers)\n", + " # visit = ref.dataId.full['visit'] # e.g., 946725\n", + " # vdr_filters.append(ref.dataId.full['band']) # e.g., VR\n", + " # vdr_detectors.append(ref.dataId.full['detector']) # e.g., 1\n", + " df = pd.DataFrame.from_dict(vdr_dict)\n", + " return df, example_vdr_ref" + ] + }, + { + "cell_type": "code", + "execution_count": 611, + "id": "eb222e02", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.82 s, sys: 83.2 ms, total: 1.91 s\n", + "Wall time: 2.4 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# TIMING NOTE: requires about 2 seconds 2/6/2024 COC\n", + "df, example_vdr_ref = get_vdr_data(\n", + " butler=butler, desired_collections=desired_collections, desired_datasetTypes=desired_datasetTypes\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 612, + "id": "6e9462e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
data_idregiondetector
0(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9847372525065534...1
1(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9847381014554984...1
2(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9847383417970056...1
3(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9847382159041213...1
4(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9847381374341414...1
............
47378(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.987608537646486,...62
47379(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9876085083003562...62
47380(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9876085761885252...62
47381(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9876085761885252...62
47382(instrument, detector, visit)ConvexPolygon([UnitVector3d(0.9876086828694174...62
\n", + "

47383 rows × 3 columns

\n", + "
" + ], + "text/plain": [ + " data_id \n", + "0 (instrument, detector, visit) \\\n", + "1 (instrument, detector, visit) \n", + "2 (instrument, detector, visit) \n", + "3 (instrument, detector, visit) \n", + "4 (instrument, detector, visit) \n", + "... ... \n", + "47378 (instrument, detector, visit) \n", + "47379 (instrument, detector, visit) \n", + "47380 (instrument, detector, visit) \n", + "47381 (instrument, detector, visit) \n", + "47382 (instrument, detector, visit) \n", + "\n", + " region detector \n", + "0 ConvexPolygon([UnitVector3d(0.9847372525065534... 1 \n", + "1 ConvexPolygon([UnitVector3d(0.9847381014554984... 1 \n", + "2 ConvexPolygon([UnitVector3d(0.9847383417970056... 1 \n", + "3 ConvexPolygon([UnitVector3d(0.9847382159041213... 1 \n", + "4 ConvexPolygon([UnitVector3d(0.9847381374341414... 1 \n", + "... ... ... \n", + "47378 ConvexPolygon([UnitVector3d(0.987608537646486,... 62 \n", + "47379 ConvexPolygon([UnitVector3d(0.9876085083003562... 62 \n", + "47380 ConvexPolygon([UnitVector3d(0.9876085761885252... 62 \n", + "47381 ConvexPolygon([UnitVector3d(0.9876085761885252... 62 \n", + "47382 ConvexPolygon([UnitVector3d(0.9876086828694174... 62 \n", + "\n", + "[47383 rows x 3 columns]" + ] + }, + "execution_count": 612, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 610, + "id": "e04e2f35", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We found 47383 regions spanning the 8 desired collections.\n" + ] + } + ], + "source": [ + "print(f\"We found {len(df['data_id'])} regions spanning the {len(desired_collections)} desired collections.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 559, + "id": "08d5b443", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
visit_detector_region:\n",
+       "  instrument: 'DECam'\n",
+       "  detector: 62\n",
+       "  visit: 946176\n",
+       "  region: ConvexPolygon([UnitVector3d(0.9876086828694174, -0.13336028508776862, -0.08272922024438323), UnitVector3d(0.9873378171284917, -0.13332652431396907, -0.08595389916869185), UnitVector3d(0.9881047366097594, -0.12752395595185462, -0.08594573955553172), UnitVector3d(0.9883760335240734, -0.12755303452468866, -0.0827226676235914)])
"
+      ],
+      "text/plain": [
+       "visit_detector_region.RecordClass(instrument='DECam', detector=62, visit=946176, region=ConvexPolygon([UnitVector3d(0.9876086828694174, -0.13336028508776862, -0.08272922024438323), UnitVector3d(0.9873378171284917, -0.13332652431396907, -0.08595389916869185), UnitVector3d(0.9881047366097594, -0.12752395595185462, -0.08594573955553172), UnitVector3d(0.9883760335240734, -0.12755303452468866, -0.0827226676235914)]))"
+      ]
+     },
+     "execution_count": 559,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# For demonstration purposes we still have the last \"ref\" from the last cell's iteration\n",
+    "example_vdr_ref"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 560,
+   "id": "e28031c2",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{instrument: 'DECam', detector: 62, visit: 946176}"
+      ]
+     },
+     "execution_count": 560,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# This is the unique dataId we would need to retrieve this specific image later\n",
+    "example_vdr_ref.dataId"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 561,
+   "id": "761f3610",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "example_vdr_ref.instrument = DECam,     example_vdr_ref.detector = 62,     example_vdr_ref.visit = 946176\n"
+     ]
+    }
+   ],
+   "source": [
+    "# The unique dataId is made up of the following attributes:\n",
+    "print(\n",
+    "    f\"example_vdr_ref.instrument = {example_vdr_ref.instrument}, \\\n",
+    "    example_vdr_ref.detector = {example_vdr_ref.detector}, \\\n",
+    "    example_vdr_ref.visit = {example_vdr_ref.visit}\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 562,
+   "id": "5d6b2106",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "b'p\\xddnE\\x86}\\x9a\\xef?\\x0f\\xc3\\x84\\'\\xf3\\x11\\xc1\\xbf\\x80\\x8a_\\xff\\xbd-\\xb5\\xbf\\x04xUzE\\x98\\xef?\\x94\\x15\\xcf\\xf2\\xd7\\x10\\xc1\\xbf\\x9d\\xa9\\xe4!\\x13\\x01\\xb6\\xbf\\x1d_\\x18\\xd3\\x8d\\x9e\\xef?\\x80\\x87\"z\\xb4R\\xc0\\xbff\\x1d\\x9f<\\x8a\\x00\\xb6\\xbf\\xe4Z\\x84\\xc6\\xc6\\xa0\\xef?\\x1f\\x01\\xe5g\\xa8S\\xc0\\xbf\\xba\\xc9\\x14\\x10P-\\xb5\\xbf'"
+      ]
+     },
+     "execution_count": 562,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# This is the region hash we would use for \"region matching\" later\n",
+    "example_vdr_ref.region.encode()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 563,
+   "id": "aa653a8b",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "False"
+      ]
+     },
+     "execution_count": 563,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# We check to see if we can crawl to other data from the dataId (False means no).\n",
+    "example_vdr_ref.dataId.hasRecords()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3f49186c",
+   "metadata": {},
+   "source": [
+    "# Dataframe Assembly\n",
+    "\n",
+    "This used to contain more, but after I moved everything to a dictionary, then a DF, this became less important.\n",
+    "TODO determine where this should go. Or remove."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 587,
+   "id": "acda18f4",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "CPU times: user 323 ms, sys: 68 ms, total: 391 ms\n",
+      "Wall time: 582 ms\n"
+     ]
+    }
+   ],
+   "source": [
+    "%%time\n",
+    "\n",
+    "# Save the dataframe to a pickle file for easier resuming of the notebook\n",
+    "# Size was < 20 Mb 2/5/2024 COC\n",
+    "# COC Note: this was not that useful, maybe TODO remove\n",
+    "df_filename = f\"{basedir}/region_search_df.pickle\"\n",
+    "df.to_pickle(df_filename)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bb076b4b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# load the pickle\n",
+    "# df = pd.read_pickle(file_name)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5bdaaf00",
+   "metadata": {},
+   "source": [
+    "### Instrument handling\n",
+    "Some Butler queries require the instrument to be specified.\\\n",
+    "For now, we are just supplying the one we care about (KLUDGE).\\\n",
+    "It's a list so we can mix-and-match shift-and-stack across instruments(!).\\\n",
+    "NOTE: will leave this as a list for future-proofing purpose"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 564,
+   "id": "0228bb4d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def getInstruments(butler, vdr_ids, first_instrument_only=True):\n",
+    "    \"\"\"Iterate through our records to determine which instrument(s) are involved.\n",
+    "    Return a list of the identified instruments.\n",
+    "    If first_instrument_only is True, stop as soon as we found an instrument.\n",
+    "    \"\"\"\n",
+    "    # KLUDGE: snag the instrument name of the first record we find in a visitInfo query.\n",
+    "    instrument_names = []\n",
+    "    for i, dataId in enumerate(vdr_ids):\n",
+    "        visitInfo = butler.get(\"calexp.visitInfo\", dataId=dataId, collections=desired_collections)\n",
+    "        instrument_name = visitInfo.instrumentLabel\n",
+    "        if instrument_name not in instrument_names:\n",
+    "            print(f'Found {instrument_name}. Adding to \"desired_instruments\" now.')\n",
+    "            instrument_names.append(instrument_name)\n",
+    "        if first_instrument_only == True and len(instrument_names) > 0:\n",
+    "            print(\n",
+    "                f\"WARNING: we are not iterating over all rows to find instruments, just taking the first one.\"\n",
+    "            )\n",
+    "            break\n",
+    "    return instrument_names"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 613,
+   "id": "6c3bb244",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Found DECam. Adding to \"desired_instruments\" now.\n",
+      "WARNING: we are not iterating over all rows to find instruments, just taking the first one.\n",
+      "CPU times: user 140 ms, sys: 19 ms, total: 159 ms\n",
+      "Wall time: 199 ms\n"
+     ]
+    }
+   ],
+   "source": [
+    "%%time\n",
+    "desired_instruments = getInstruments(butler=butler, vdr_ids=df[\"data_id\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "47a434b2",
+   "metadata": {},
+   "source": [
+    "### Butler Retrieval Example\n",
+    "\n",
+    "A quick stop to see how we can grab the full Butler record via a dataId."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 614,
+   "id": "dfc37b54",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "CPU times: user 875 ms, sys: 108 ms, total: 983 ms\n",
+      "Wall time: 1.02 s\n"
+     ]
+    }
+   ],
+   "source": [
+    "%%time\n",
+    "# Back to the dataId, we can actually fetch the image from the Butler\n",
+    "# TIMING NOTE: it takes 1 to 2 seconds to do this, so we won't be able to do this at scale\n",
+    "\n",
+    "example_butler_get = butler.get(\n",
+    "    desired_datasetTypes[0], collections=desired_collections, dataId=example_vdr_ref.dataId\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 529,
+   "id": "789f1d42",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "62"
+      ]
+     },
+     "execution_count": 529,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "example_butler_get.detector.getId()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 530,
+   "id": "351e8d03",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'VR'"
+      ]
+     },
+     "execution_count": 530,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "example_butler_get.filterLabel.bandLabel"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 531,
+   "id": "1c1e5431",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       ""
+      ]
+     },
+     "execution_count": 531,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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",
+      "text/plain": [
+       "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# we can even view the image right here\n", + "plt.imshow(example_butler_get.image.array, cmap=\"gray\")" + ] + }, + { + "cell_type": "code", + "execution_count": 532, + "id": "ba1b7ab6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FITS standard SkyWcs:\n", + "Sky Origin: (352.4630539808, -4.8516830845)\n", + "Pixel Origin: (1126.14, 1991.36)\n", + "Pixel Scale: 0.262593 arcsec/pixel" + ] + }, + "execution_count": 532, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# There IS a WCS here, which we did not see in the earlier VDR\n", + "example_butler_get.wcs" + ] + }, + { + "cell_type": "code", + "execution_count": 503, + "id": "4270995e", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Here are all the keywords available from this image:\n", + "NEXTEND\n", + "PROCTYPE\n", + "PRODTYPE\n", + "PIXSCAL1\n", + "PIXSCAL2\n", + "FILENAME\n", + "TELESCOP\n", + "OBSERVAT\n", + "INSTRUME\n", + "EXPREQ\n", + "OBSID\n", + "TIME-OBS\n", + "OPENSHUT\n", + "EXPNUM\n", + "OBJECT\n", + "OBSTYPE\n", + "CAMSHUT\n", + "PROGRAM\n", + "OBSERVER\n", + "PROPOSER\n", + "DTPI\n", + "PROPID\n", + "EXCLUDED\n", + "SEQID\n", + "SEQNUM\n", + "SEQTOT\n", + "AOS\n", + "BCAM\n", + "GUIDER\n", + "SKYSTAT\n", + "FILTER\n", + "INSTANCE\n", + "ERRORS\n", + "TELEQUIN\n", + "TELSTAT\n", + "RA\n", + "DEC\n", + "TELRA\n", + "TELDEC\n", + "HA\n", + "ZD\n", + "AZ\n", + "DOMEAZ\n", + "ZPDELRA\n", + "ZPDELDEC\n", + "TELFOCUS\n", + "VSUB\n", + "GSKYPHOT\n", + "LSKYPHOT\n", + "WINDSPD\n", + "WINDDIR\n", + "PRESSURE\n", + "DIMMSEE\n", + "DIMM2SEE\n", + "MASS2\n", + "ASTIG1\n", + "ASTIG2\n", + "OUTTEMP\n", + "AIRMASS\n", + "GSKYVAR\n", + "GSKYHOT\n", + "LSKYVAR\n", + "LSKYHOT\n", + "LSKYPOW\n", + "MSURTEMP\n", + "MAIRTEMP\n", + "UPTRTEMP\n", + "LWTRTEMP\n", + "PMOSTEMP\n", + "UTN-TEMP\n", + "UTS-TEMP\n", + "UTW-TEMP\n", + "UTE-TEMP\n", + "PMN-TEMP\n", + "PMS-TEMP\n", + "PMW-TEMP\n", + "PME-TEMP\n", + "DOMELOW\n", + "DOMEHIGH\n", + "DOMEFLOR\n", + "G-MEANX\n", + "G-MEANY\n", + "DONUTFS4\n", + "DONUTFS3\n", + "DONUTFS2\n", + "DONUTFS1\n", + "G-FLXVAR\n", + "G-MEANXY\n", + "DONUTFN1\n", + "DONUTFN2\n", + "DONUTFN3\n", + "DONUTFN4\n", + "TIME_RECORDED\n", + "G-FEEDBK\n", + "G-CCDNUM\n", + "DOXT\n", + "G-MAXX\n", + "FADZ\n", + "FADY\n", + "FADX\n", + "G-MODE\n", + "FAYT\n", + "DODZ\n", + "DODY\n", + "DODX\n", + "MULTIEXP\n", + "SKYUPDAT\n", + "G-SEEING\n", + "G-TRANSP\n", + "G-MEANY2\n", + "DOYT\n", + "G-LATENC\n", + "LUTVER\n", + "FAXT\n", + "G-MAXY\n", + "G-MEANX2\n", + "SISPIVER\n", + "CONSTVER\n", + "HDRVER\n", + "DTPROPID\n", + "DTCALDAT\n", + "DTSITE\n", + "DTTELESC\n", + "DTACQNAM\n", + "DTINSTRU\n", + "ODATEOBS\n", + "DTNSANAM\n", + "HISTORY\n", + "COMMENT\n", + "ZTENSION\n", + "ZPCOUNT\n", + "ZGCOUNT\n", + "BUNIT\n", + "DETSIZE\n", + "DETSEC\n", + "CCDSEC\n", + "DETSECA\n", + "CCDSECA\n", + "AMPSECA\n", + "DETSECB\n", + "CCDSECB\n", + "AMPSECB\n", + "DETECTOR\n", + "CCDNUM\n", + "DETPOS\n", + "EXTNAME\n", + "GAINA\n", + "RDNOISEA\n", + "SATURATA\n", + "GAINB\n", + "RDNOISEB\n", + "SATURATB\n", + "FPA\n", + "INHERIT\n", + "CCDBIN1\n", + "CCDBIN2\n", + "DHEINF\n", + "DHEFIRM\n", + "SLOT00\n", + "SLOT01\n", + "SLOT02\n", + "SLOT03\n", + "SLOT04\n", + "SLOT05\n", + "LTV2\n", + "LTV1\n", + "VALIDA\n", + "VALIDB\n", + "NDONUTS\n", + "CHECKVER\n", + "ASTRO METADATA FIX MODIFIED\n", + "ASTRO METADATA FIX DATE\n", + "ISR_OSCAN_LEVELA\n", + "ISR_OSCAN_SIGMAA\n", + "OVERSCAN\n", + "ISR_OSCAN_LEVELB\n", + "ISR_OSCAN_SIGMAB\n", + "SKYLEVEL\n", + "SKYSIGMA\n", + "FLATNESS_PP\n", + "FLATNESS_RMS\n", + "FLATNESS_NGRIDS\n", + "FLATNESS_MESHX\n", + "FLATNESS_MESHY\n", + "BGMEAN\n", + "BGVAR\n", + "SFM_ASTROM_OFFSET_MEAN\n", + "SFM_ASTROM_OFFSET_STD\n", + "MAGZERO\n", + "MAGZERO_RMS\n", + "MAGZERO_NOBJ\n", + "COLORTERM1\n", + "COLORTERM2\n", + "COLORTERM3\n" + ] + } + ], + "source": [ + "# We can access all kinds of metadata this way.\n", + "# NOTE: keywords are source data-dependent.\n", + "# NOTE: this linkage works with butler.get() but not necessarily elsewhere.\n", + "meta = example_butler_get.getInfo().getMetadata()\n", + "\n", + "print(f\"Here are all the keywords available from this image:\")\n", + "for k in meta.keys():\n", + " print(k)" + ] + }, + { + "cell_type": "markdown", + "id": "d4f8d811", + "metadata": {}, + "source": [ + "### URI / URL / Path Handling\n", + "Here we grab URIs for the dataIds we need." + ] + }, + { + "cell_type": "code", + "execution_count": 272, + "id": "7bbc916e", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: getURIs() does not work as you'd think (i.e., it only handles a single dataId)\n", + "# butler.getURIs(desired_datasetTypes[0], vdr_ids, collections=desired_collections)" + ] + }, + { + "cell_type": "code", + "execution_count": 567, + "id": "e0f8f8f9", + "metadata": {}, + "outputs": [], + "source": [ + "# The single-thead approach (below) requires some 2 hours to execute. So instead we will multiprocess.\n", + "# paths = [butler.getURI(desired_datasetTypes[0], dataId=dataId, collections=desired_collections) for dataId in vdr_ids]\n", + "\n", + "\n", + "def chunked_dataIds(dataIds, chunk_size=200):\n", + " \"\"\"Yield successive chunk_size chunks from dataIds.\"\"\"\n", + " for i in range(0, len(dataIds), chunk_size):\n", + " yield dataIds[i : i + chunk_size]\n", + "\n", + "\n", + "def get_uris(dataIds_chunk, repo_path, desired_datasetTypes, desired_collections):\n", + " \"\"\"Fetch URIs for a list of dataIds.\"\"\"\n", + " chunk_uris = []\n", + " butler = dafButler.Butler(repo_path)\n", + " for dataId in dataIds_chunk:\n", + " try:\n", + " uri = butler.getURI(desired_datasetTypes[0], dataId=dataId, collections=desired_collections)\n", + " uri = uri.geturl() # Convert to URL string\n", + " chunk_uris.append(uri)\n", + " except Exception as e:\n", + " print(f\"Failed to retrieve path for dataId {dataId}: {e}\")\n", + " return chunk_uris\n", + "\n", + "\n", + "def getURIs(butler, dataIds, repo_path, desired_datasetTypes, desired_collections, overwrite=False):\n", + " \"\"\"\n", + " Get URIs from a Butler for a set of dataIDs.\n", + " Cache results to disk for future runs.\n", + " TODO: consider exporting as CSV so we can validate URIs against dataIds. 2/6/2024 COC\n", + " Updated 2/5/2024 COC\n", + " \"\"\"\n", + " paths = []\n", + "\n", + " cache_file = f\"{basedir}/uri_cache.lst\"\n", + " cached_exists = False\n", + " if len(glob.glob(cache_file)) > 0:\n", + " cached_exists = True\n", + "\n", + " if cached_exists == True and overwrite == False:\n", + " with open(cache_file, \"r\") as f:\n", + " for line in f:\n", + " paths.append(line.strip())\n", + " print(f\"Recycled {len(paths)} paths from {cache_file} as overwrite was {overwrite}.\")\n", + " return paths\n", + "\n", + " # Prepare dataId chunks\n", + " dataId_chunks = list(chunked_dataIds(dataIds))\n", + "\n", + " # Execute get_uris in parallel and preserve order\n", + " with ProcessPoolExecutor() as executor:\n", + " # Initialize progress bar\n", + " with progressbar.ProgressBar(max_value=len(dataId_chunks)) as bar:\n", + " # Use map to execute get_uris on each chunk and maintain order\n", + " result_chunks = list(\n", + " executor.map(\n", + " get_uris,\n", + " dataId_chunks,\n", + " [repo_path] * len(dataId_chunks),\n", + " [desired_datasetTypes] * len(dataId_chunks),\n", + " [desired_collections] * len(dataId_chunks),\n", + " )\n", + " )\n", + "\n", + " for i, chunk_uris in enumerate(result_chunks):\n", + " paths.extend(chunk_uris) # Add the retrieved URIs to the main list\n", + " bar.update(i)\n", + "\n", + " with open(cache_file, \"w\") as f:\n", + " for path in paths:\n", + " print(path, file=f)\n", + " print(f\"Wrote {len(paths)} paths to disk for caching purposes.\")\n", + "\n", + " return paths" + ] + }, + { + "cell_type": "code", + "execution_count": 628, + "id": "6dd2ea6a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recycled 47383 paths from /astro/users/coc123/kbmod_tmp/uri_cache.lst as overwrite was False.\n", + "CPU times: user 45 ms, sys: 32.1 ms, total: 77.1 ms\n", + "Wall time: 76.7 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# TIMING NOTE: This required 90s uncached 2/5/2024 COC\n", + "\n", + "df[\"uri\"] = getURIs(\n", + " butler=butler,\n", + " dataIds=df[\"data_id\"],\n", + " repo_path=repo_path,\n", + " desired_datasetTypes=desired_datasetTypes,\n", + " desired_collections=desired_collections,\n", + " overwrite=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 617, + "id": "eae8d366", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'file:///epyc/users/smotherh/DEEP/PointingGroups/butler-repo/PointingGroup021/imdiff_r/20210723T174135Z/deepDiff_differenceExp/20190927/VR/VR_DECam_c0007_6300.0_2600.0/898286/deepDiff_differenceExp_DECam_VR_VR_DECam_c0007_6300_0_2600_0_898286_S29_PointingGroup021_imdiff_r_20210723T174135Z.fits'" + ] + }, + "execution_count": 617, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# example URI\n", + "df[\"uri\"].iloc()[0]" + ] + }, + { + "cell_type": "markdown", + "id": "fb728cee", + "metadata": {}, + "source": [ + "### Timestamp Handling\n", + "Here we will access the timestamp (datetime) information from the Butler for our records." + ] + }, + { + "cell_type": "code", + "execution_count": 619, + "id": "df7ff31f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 DateTime(\"2019-09-27T00:20:59.932016000\", TAI) 120.0 (351.3806941054, -5.2403083277)\n", + "CPU times: user 126 ms, sys: 12 ms, total: 138 ms\n", + "Wall time: 175 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "# Now we want to get metadata like datetime, exposure time, etc.\n", + "for i, dataId in enumerate(df[\"data_id\"]):\n", + " visitInfo = butler.get(\"calexp.visitInfo\", dataId=dataId, collections=desired_collections)\n", + " print(i, visitInfo.date, visitInfo.exposureTime, visitInfo.boresightRaDec)\n", + " break\n", + "# We have the visitInfo object for the exploration below." + ] + }, + { + "cell_type": "code", + "execution_count": 572, + "id": "3ccadfaa", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "lsst.daf.base.dateTime.dateTime.DateTime" + ] + }, + "execution_count": 572, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# That visitInfo.date format may not look familiar. Let's find out why:\n", + "type(visitInfo.date)" + ] + }, + { + "cell_type": "code", + "execution_count": 573, + "id": "d73d8119", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2019-09-27T00:20:22.932'" + ] + }, + "execution_count": 573, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Let's convert to a plain string, UTC (handles 37 s offset).\n", + "t = Time(testing, format=\"isot\", scale=\"tai\")\n", + "str(t.utc)" + ] + }, + { + "cell_type": "code", + "execution_count": 574, + "id": "89822691", + "metadata": {}, + "outputs": [], + "source": [ + "def getTimestamps(dataIds, overwrite=False):\n", + " \"\"\"Get timestamps for a bunch of dataIds.\n", + " Convert the LSST/Butler TAI to UTC in the process.\n", + " Do this all in a chunked, multiprocessing way.\n", + " Takes about 3 minutes as of 2/1/2024 (Hayden DEEP).\n", + " BUT if we have the values cached, just read those instead, unless overwrite is True.\n", + " 2/1/2024 COC\n", + " \"\"\"\n", + " # thank you ChatGPT 4 for helping parallelize\n", + "\n", + " timestamps = []\n", + "\n", + " import glob\n", + "\n", + " cache_file = f\"{basedir}/vdr_timestamps.lst\"\n", + "\n", + " cache_file_exists = False\n", + " if len(glob.glob(cache_file)) > 0:\n", + " cache_file_exists = True\n", + "\n", + " if overwrite == False and cache_file_exists == True:\n", + " print(f\"Overwrite is False, so we will read the timestamps from file now...\")\n", + " with open(cache_file, \"r\") as f:\n", + " for line in f:\n", + " timestamps.append(line.strip())\n", + " print(f\"Recycled {len(timestamps)} from {cache_file}.\")\n", + " return timestamps\n", + "\n", + " if overwrite or not cache_file_exists:\n", + " timestamps = [] # Re-initialize timestamps here to ensure it's fresh\n", + "\n", + " with ProcessPoolExecutor() as executor:\n", + " dataId_chunks = list(chunked_dataIds(dataIds))\n", + " # Initialize progress bar\n", + " with progressbar.ProgressBar(max_value=len(dataId_chunks)) as bar:\n", + " # Use map for preserving order and simplifying the code\n", + " results = executor.map(get_timestamps, dataId_chunks)\n", + "\n", + " # Process results and maintain the order\n", + " for i, chunk_result in enumerate(results):\n", + " timestamps.extend(chunk_result) # Correctly extend with the result of each future\n", + " bar.update(i)\n", + "\n", + " # Write to cache if necessary\n", + " if overwrite or not cache_file_exists:\n", + " with open(cache_file, \"w\") as f:\n", + " for ts in timestamps:\n", + " print(ts, file=f)\n", + " print(f\"Wrote {len(timestamps)} lines to {cache_file} for future use.\")\n", + "\n", + " print(f\"Obtained {len(timestamps)} timestamps.\")\n", + " return timestamps" + ] + }, + { + "cell_type": "code", + "execution_count": 620, + "id": "5ea2f086", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2019-09-27T00:20:22.932'" + ] + }, + "execution_count": 620, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Double-check that we can convert Butler timestamp (TAI) to UTC string\n", + "visitInfo = butler.get(\"calexp.visitInfo\", dataId=df[\"data_id\"].iloc()[0], collections=desired_collections)\n", + "t = Time(str(visitInfo.date).split('\"')[1], format=\"isot\", scale=\"tai\")\n", + "tutc = str(t.utc)\n", + "tutc" + ] + }, + { + "cell_type": "code", + "execution_count": 621, + "id": "7e0d33c0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwrite is False, so we will read the timestamps from file now...\n", + "Recycled 47383 from /astro/users/coc123/kbmod_tmp/vdr_timestamps.lst.\n", + "CPU times: user 24.1 ms, sys: 9.07 ms, total: 33.2 ms\n", + "Wall time: 30.1 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# TIMING NOTE: this took < 5 minutes 2/5/2024 COC\n", + "df[\"ut\"] = getTimestamps(dataIds=df[\"data_id\"], overwrite=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 622, + "id": "adddf1e4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 2019-09-27T00:20:22.932\n", + "1 2019-09-27T00:22:51.015\n", + "2 2019-09-27T00:25:19.136\n", + "3 2019-09-27T00:27:47.118\n", + "4 2019-09-27T00:30:15.537\n", + " ... \n", + "47378 2020-10-17T04:00:51.409\n", + "47379 2020-10-17T04:03:19.873\n", + "47380 2020-10-17T04:05:48.949\n", + "47381 2020-10-17T04:08:17.445\n", + "47382 2020-10-17T04:10:46.218\n", + "Name: ut, Length: 47383, dtype: object" + ] + }, + "execution_count": 622, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"ut\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 364, + "id": "a789a331", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 : 2019-09-27T00:20:22.932 for {instrument: 'DECam', detector: 1, visit: 898286}. Dict had: 2019-09-27\n", + "1000 : 2019-09-27T02:24:03.066 for {instrument: 'DECam', detector: 12, visit: 898336}. Dict had: 2019-08-29\n", + "2000 : 2019-09-27T00:32:44.405 for {instrument: 'DECam', detector: 23, visit: 898291}. Dict had: 2019-08-29\n", + "3000 : 2019-09-27T02:36:24.497 for {instrument: 'DECam', detector: 33, visit: 898341}. Dict had: 2019-08-29\n", + "4000 : 2019-09-27T00:45:05.306 for {instrument: 'DECam', detector: 44, visit: 898296}. Dict had: 2019-08-29\n", + "5000 : 2019-09-27T02:51:16.295 for {instrument: 'DECam', detector: 54, visit: 898347}. Dict had: 2019-08-29\n", + "6000 : 2019-08-29T07:25:55.714 for {instrument: 'DECam', detector: 4, visit: 891512}. Dict had: 2019-08-30\n", + "7000 : 2019-08-29T06:36:25.673 for {instrument: 'DECam', detector: 14, visit: 891492}. Dict had: 2019-08-30\n", + "8000 : 2019-08-29T05:46:48.259 for {instrument: 'DECam', detector: 24, visit: 891472}. Dict had: 2019-08-29\n", + "9000 : 2019-08-29T09:11:52.823 for {instrument: 'DECam', detector: 33, visit: 891554}. Dict had: 2020-10-19\n", + "10000: 2019-08-29T08:24:43.475 for {instrument: 'DECam', detector: 43, visit: 891535}. Dict had: 2020-10-19\n", + "11000: 2019-08-29T07:39:44.133 for {instrument: 'DECam', detector: 53, visit: 891517}. Dict had: 2020-10-19\n", + "12000: 2020-10-19T03:57:38.040 for {instrument: 'DECam', detector: 1, visit: 946776}. Dict had: 2020-10-19\n", + "13000: 2020-10-19T03:55:07.913 for {instrument: 'DECam', detector: 13, visit: 946775}. Dict had: 2020-10-19\n", + "14000: 2020-10-19T03:52:39.901 for {instrument: 'DECam', detector: 24, visit: 946774}. Dict had: 2020-10-19\n", + "15000: 2020-10-19T03:50:11.828 for {instrument: 'DECam', detector: 35, visit: 946773}. Dict had: 2019-09-27\n", + "16000: 2020-10-19T03:47:43.087 for {instrument: 'DECam', detector: 46, visit: 946772}. Dict had: 2019-09-27\n", + "17000: 2020-10-19T03:45:14.564 for {instrument: 'DECam', detector: 57, visit: 946771}. Dict had: 2019-09-27\n", + "18000: 2019-08-30T07:36:47.123 for {instrument: 'DECam', detector: 7, visit: 891898}. Dict had: 2019-09-27\n", + "19000: 2019-08-30T07:11:50.193 for {instrument: 'DECam', detector: 17, visit: 891888}. Dict had: 2019-09-27\n", + "20000: 2019-08-30T06:47:05.343 for {instrument: 'DECam', detector: 27, visit: 891878}. Dict had: 2019-08-30\n", + "21000: 2019-08-30T06:22:19.105 for {instrument: 'DECam', detector: 37, visit: 891868}. Dict had: 2019-08-30\n", + "22000: 2019-08-30T05:57:29.152 for {instrument: 'DECam', detector: 47, visit: 891858}. Dict had: 2019-08-30\n", + "23000: 2019-08-30T05:32:32.635 for {instrument: 'DECam', detector: 57, visit: 891848}. Dict had: 2019-08-30\n", + "24000: 2019-09-28T01:46:11.541 for {instrument: 'DECam', detector: 6, visit: 898736}. Dict had: 2019-09-28\n", + "25000: 2019-09-28T03:00:31.172 for {instrument: 'DECam', detector: 16, visit: 898766}. Dict had: 2019-09-28\n", + "26000: 2019-09-28T00:17:03.748 for {instrument: 'DECam', detector: 27, visit: 898700}. Dict had: 2019-09-28\n", + "27000: 2019-09-28T01:31:18.723 for {instrument: 'DECam', detector: 37, visit: 898730}. Dict had: 2019-09-28\n", + "28000: 2019-09-28T02:45:39.745 for {instrument: 'DECam', detector: 47, visit: 898760}. Dict had: 2019-09-28\n", + "29000: 2019-09-28T04:00:09.666 for {instrument: 'DECam', detector: 57, visit: 898790}. Dict had: 2019-08-28\n", + "30000: 2019-08-28T06:43:48.523 for {instrument: 'DECam', detector: 7, visit: 891114}. Dict had: 2019-08-28\n", + "31000: 2019-08-28T05:29:20.494 for {instrument: 'DECam', detector: 17, visit: 891084}. Dict had: 2019-08-28\n", + "32000: 2019-08-28T08:31:09.143 for {instrument: 'DECam', detector: 26, visit: 891157}. Dict had: 2019-08-28\n", + "33000: 2019-08-28T07:21:26.793 for {instrument: 'DECam', detector: 36, visit: 891129}. Dict had: 2019-08-28\n", + "34000: 2019-08-28T06:06:34.210 for {instrument: 'DECam', detector: 46, visit: 891099}. Dict had: 2019-08-28\n", + "35000: 2019-08-28T09:08:44.560 for {instrument: 'DECam', detector: 55, visit: 891172}. Dict had: 2019-09-28\n", + "36000: 2019-09-29T00:19:06.349 for {instrument: 'DECam', detector: 5, visit: 899020}. Dict had: 2019-09-29\n", + "37000: 2019-09-29T01:58:29.441 for {instrument: 'DECam', detector: 15, visit: 899060}. Dict had: 2019-09-29\n", + "38000: 2019-09-29T03:37:44.590 for {instrument: 'DECam', detector: 25, visit: 899100}. Dict had: 2019-09-29\n", + "39000: 2019-09-29T01:18:42.912 for {instrument: 'DECam', detector: 36, visit: 899044}. Dict had: 2019-09-29\n", + "40000: 2019-09-29T02:58:02.739 for {instrument: 'DECam', detector: 46, visit: 899084}. Dict had: 2019-09-29\n", + "41000: 2019-09-29T00:38:58.257 for {instrument: 'DECam', detector: 57, visit: 899028}. Dict had: 2019-09-29\n", + "42000: 2020-10-17T03:45:59.925 for {instrument: 'DECam', detector: 6, visit: 946166}. Dict had: 2020-10-17\n", + "43000: 2020-10-17T01:26:44.472 for {instrument: 'DECam', detector: 17, visit: 946110}. Dict had: 2020-10-17\n", + "44000: 2020-10-17T03:06:21.338 for {instrument: 'DECam', detector: 27, visit: 946150}. Dict had: 2020-10-17\n", + "45000: 2020-10-17T00:46:58.038 for {instrument: 'DECam', detector: 38, visit: 946094}. Dict had: 2020-10-17\n", + "46000: 2020-10-17T02:26:33.210 for {instrument: 'DECam', detector: 48, visit: 946134}. Dict had: 2020-10-17\n", + "47000: 2020-10-17T04:05:48.949 for {instrument: 'DECam', detector: 58, visit: 946174}. Dict had: 2020-10-17\n" + ] + } + ], + "source": [ + "# This is for coming back to later to make sure the stamps line up\n", + "# IGNORE FOR NOW 2/6/2024 COC\n", + "# for i in range(0,len(vdr_ids),1000):\n", + "# dataId = vdr_ids[i]\n", + "# visitInfo = butler.get(\"calexp.visitInfo\", dataId=dataId, collections=desired_collections)\n", + "# t = Time(str(visitInfo.date).split('\"')[1], format=\"isot\", scale=\"tai\")\n", + "# tutc = str(t.utc)\n", + "# print(f\"{i!s:5}: {tutc} for {dataId}. Dict had: {id_to_date[dataId]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "6f355bcf", + "metadata": {}, + "source": [ + "### Working with the Regions\n", + "\n", + "We still don't know what will be ideal to do for Region Search.\\\n", + "So, to start with, we will work on extract the (RA, Dec) of the (1) center coordinate of a chip, and (2) four corners associated with a chip." + ] + }, + { + "cell_type": "markdown", + "id": "f060a724", + "metadata": {}, + "source": [ + "#### We will start with corners (i.e., quadilateral vertices from the convexPolygon sphgeom.regions)" + ] + }, + { + "cell_type": "code", + "execution_count": 578, + "id": "e45d7199", + "metadata": {}, + "outputs": [], + "source": [ + "def getRegionCorners(region):\n", + " \"\"\"\n", + " Using the 2D boundingBox() from an input region (convexPolygon), we\n", + " extract the (RA, Dec) coordinates of each vertex.\n", + " As there are four vertices, the input object is a quadrilateral.\n", + " 2/2/2024 COC\n", + " \"\"\"\n", + " corners = []\n", + " bbox = region.getBoundingBox()\n", + " corners.append((bbox.getLon().getA().asDegrees(), bbox.getLat().getA().asDegrees()))\n", + " corners.append((bbox.getLon().getA().asDegrees(), bbox.getLat().getB().asDegrees()))\n", + " corners.append((bbox.getLon().getB().asDegrees(), bbox.getLat().getA().asDegrees()))\n", + " corners.append((bbox.getLon().getB().asDegrees(), bbox.getLat().getB().asDegrees()))\n", + " return corners" + ] + }, + { + "cell_type": "code", + "execution_count": 579, + "id": "9b278b50", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(352.30950511932116, -4.930880058593892),\n", + " (352.30950511932116, -4.7450820235469715),\n", + " (352.64644359666477, -4.930880058593892),\n", + " (352.64644359666477, -4.7450820235469715)]" + ] + }, + "execution_count": 579, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Example\n", + "corners = getRegionCorners(example_vdr_ref.region)\n", + "corners" + ] + }, + { + "cell_type": "code", + "execution_count": 580, + "id": "b37bfe47", + "metadata": {}, + "outputs": [], + "source": [ + "def getMinMaxRaDec(ra_dec_touples, verbose=False):\n", + " \"\"\"\n", + " Highly unoptimized way to find and return\n", + " (minRA, maxRA) and (minDec, MaxDec).\n", + " 2/2/2024 COC\n", + " \"\"\"\n", + " min_ra = min([i[0] for i in ra_dec_touples])\n", + " min_dec = min([i[1] for i in ra_dec_touples])\n", + " max_ra = max([i[0] for i in ra_dec_touples])\n", + " max_dec = max([i[1] for i in ra_dec_touples])\n", + " if verbose:\n", + " print(f\"RA range: {min_ra} to {max_ra}\")\n", + " print(f\"Dec range: {min_dec} to {max_dec}\")\n", + " return (min_ra, max_ra), (min_dec, max_dec)" + ] + }, + { + "cell_type": "code", + "execution_count": 581, + "id": "ef329736", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((352.30950511932116, 352.64644359666477),\n", + " (-4.930880058593892, -4.7450820235469715))" + ] + }, + "execution_count": 581, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Example\n", + "getMinMaxRaDec(ra_dec_touples=corners)" + ] + }, + { + "cell_type": "markdown", + "id": "d95593bb", + "metadata": {}, + "source": [ + "#### Centers\n", + "\n", + "It may be easer to just work with center (RA, Dec) coordinates.\\\n", + "We can later store this in a DB (e.g., Postgres) for cone searches." + ] + }, + { + "cell_type": "code", + "execution_count": 582, + "id": "4c89be9e", + "metadata": {}, + "outputs": [], + "source": [ + "def getCenterRaDec(region):\n", + " \"\"\"\n", + " We pull the 2D boundingBox (not the boundingBox3d) from a region.\n", + " Then we extract the center's (RA, Dec) coordinates.\n", + " 2/2/2024 COC\n", + " \"\"\"\n", + " bbox_center = region.getBoundingBox().getCenter()\n", + " ra = bbox_center.getLon().asDegrees()\n", + " dec = bbox_center.getLat().asDegrees()\n", + " return (ra, dec)" + ] + }, + { + "cell_type": "code", + "execution_count": 182, + "id": "8e1e764c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(352.477974357993, -4.837981041070432)" + ] + }, + "execution_count": 182, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getCenterRaDec(tmpref.region)" + ] + }, + { + "cell_type": "code", + "execution_count": 624, + "id": "8e54db29", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 363 ms, sys: 19 ms, total: 382 ms\n", + "Wall time: 379 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "df[\"center_coord\"] = [getCenterRaDec(i) for i in df[\"region\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": 625, + "id": "64908868", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "47383" + ] + }, + "execution_count": 625, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(df[\"center_coord\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 626, + "id": "625de417", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(351.0694028401149, -4.336598368890197)" + ] + }, + "execution_count": 626, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"center_coord\"].iloc()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 627, + "id": "67b57215", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.2 s, sys: 26 ms, total: 1.23 s\n", + "Wall time: 1.22 s\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 627, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%time\n", + "# Let us visualize where on the sky our chip centers reside\n", + "fig = plt.figure(figsize=(4, 4))\n", + "plt.xlabel(\"RA\")\n", + "plt.ylabel(\"Dec\")\n", + "plt.scatter(\n", + " [i[0] for i in df[\"center_coord\"].iloc()], [i[1] for i in df[\"center_coord\"].iloc()], s=1, alpha=0.5\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "41d4fd38", + "metadata": {}, + "source": [ + "The DECam mosaic shape is clearly visible, thrice.\\\n", + "There is an offset from each of the three pointings, too.\n", + "\n", + "In the most simple approach, each of these \"dots\" represents a set of images that can be fed to KBMOD." + ] + }, + { + "cell_type": "code", + "execution_count": 554, + "id": "c2dc4e1a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{instrument: 'DECam', detector: 1, visit: 898286}" + ] + }, + "execution_count": 554, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# look at the first dataId, which does not show up in a pretty way in the DF\n", + "df[\"data_id\"].iloc[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 435, + "id": "580ac4ed", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 435, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Let us look at just detector 62 pointings.\n", + "# This will give us a decent idea of how many pointings there are (here, 8).\n", + "\n", + "df62 = df[df[\"detector\"] == 62]\n", + "plt.clf()\n", + "plt.scatter(*zip(*df62[\"center_coord\"]), s=1, alpha=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 237, + "id": "6ee544b8", + "metadata": {}, + "outputs": [], + "source": [ + "# chip62_coord0 = df62['center_coord'][0]" + ] + }, + { + "cell_type": "markdown", + "id": "df7d441d", + "metadata": {}, + "source": [ + "### Region Matching\n", + "Here region matching means comparing two lsst.sphgeom.region objects.\\\n", + "Purpose: to see if they have any overlap whatsoever. \\\n", + "NOTE: a partial or full overlap is called an \"intersection\" in sphgeom lingo.\n", + "\n", + "NOTE: Work in progress, but pausing this avenue for now. 2/2/2024 COC" + ] + }, + { + "cell_type": "code", + "execution_count": 589, + "id": "2c770982", + "metadata": {}, + "outputs": [], + "source": [ + "# manual region-region matching between all chips\n", + "# NOTE: estimate time is 1 hour, so skipping this for now\n", + "\n", + "doit = False\n", + "if doit == True:\n", + " lastTime = time.time()\n", + "\n", + " matches = {}\n", + "\n", + " with progressbar.ProgressBar(max_value=len(vdr_regions)) as bar:\n", + " for i, l in enumerate(vdr_regions):\n", + " my_id = vdr_ids[i]\n", + " matches[my_id] = []\n", + " for j, r in enumerate(vdr_regions):\n", + " if i == j:\n", + " continue\n", + " if l.intersects(r):\n", + " matches[my_id].append(vdr_ids[j])\n", + " bar.update(i)\n", + " elapsed = round(time.time() - lastTime, 1)\n", + " print(f\"It took {elapsed} seconds.\")" + ] + }, + { + "cell_type": "markdown", + "id": "ba8db514", + "metadata": {}, + "source": [ + "To be continued..." + ] + }, + { + "cell_type": "markdown", + "id": "f00d9b2f", + "metadata": {}, + "source": [ + "### HTM Exploration" + ] + }, + { + "cell_type": "code", + "execution_count": 202, + "id": "2fcdd8b3", + "metadata": {}, + "outputs": [], + "source": [ + "# Colin playing 1/15/2024 COC\n", + "def getHTMstuff(ra, dec, level=7, verbose=False):\n", + " \"\"\"\n", + " 1/16/2024 COC inception.\n", + " A function that\n", + " 1. Fetches an HTM ID for a pixelization of a user-supplied level.\n", + " 2. Determines the angular size (radius, in arcseconds) of the pixelization level.\n", + " LSST stores the unique HTM ID for level 7 pixelization in the Butler, hence the default level=7.\n", + " \"\"\"\n", + " pixelization = lsst.sphgeom.HtmPixelization(level)\n", + "\n", + " try: # kludges; use the Butler way, but if being passed normal numbers, use those\n", + " ra = ra.asDegrees()\n", + " except AttributeError as msg:\n", + " pass\n", + " try:\n", + " dec = dec.asDegrees()\n", + " except AttributeError as msg:\n", + " pass\n", + "\n", + " htm_id = pixelization.index(\n", + " lsst.sphgeom.UnitVector3d(\n", + " # sphgeom.LonLat.fromDegrees(ra.asDegrees(), dec.asDegrees())\n", + " lsst.sphgeom.LonLat.fromDegrees(ra, dec)\n", + " )\n", + " )\n", + " circle = pixelization.triangle(htm_id).getBoundingCircle()\n", + " scale = circle.getOpeningAngle().asDegrees() * 3600.0\n", + " level = pixelization.getLevel()\n", + " if verbose:\n", + " print(f\"HTM ID={htm_id} at level={level} is bounded by a circle of radius ~{scale:0.2f} arcsec.\")\n", + " return (htm_id, scale)" + ] + }, + { + "cell_type": "code", + "execution_count": 204, + "id": "c0916372", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HTM ID=189361 at level=7 is bounded by a circle of radius ~1895.11 arcsec.\n" + ] + }, + { + "data": { + "text/plain": [ + "(189361, 1895.111766130883)" + ] + }, + "execution_count": 204, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getHTMstuff(vdr_centers[0][0], vdr_centers[0][1], verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 588, + "id": "47c447c5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], + "source": [ + "dsRefs = butler.registry.queryDatasets(\n", + " datasetType=desired_datasetTypes[0], htm7=147116, collections=desired_collections\n", + ")\n", + "print(len(sorted(dsRefs)))" + ] + }, + { + "cell_type": "markdown", + "id": "d59bcdde", + "metadata": {}, + "source": [ + "To be continued..." + ] + }, + { + "cell_type": "markdown", + "id": "10339ac6", + "metadata": {}, + "source": [ + "### Brute force approach for (small) discrete datasets\n", + "\n", + "The idea here is that we can organize discrete piles of images, such as DEEP, DDF." + ] + }, + { + "cell_type": "code", + "execution_count": 441, + "id": "048e38e1", + "metadata": {}, + "outputs": [], + "source": [ + "def find_overlapping_coords(df, uncertainty_radius, overwrite=False):\n", + " \"\"\"\n", + "\n", + " Added caching 2/5/2024 COC\n", + " \"\"\"\n", + " import glob\n", + "\n", + " cache_file = f\"{basedir}/overlapping_sets.pickle\"\n", + "\n", + " cache_exists = False\n", + " if len(glob.glob(cache_file)) > 0:\n", + " cache_exists = True\n", + "\n", + " if overwrite == False and cache_exists == True:\n", + " with open(cache_file, \"rb\") as f:\n", + " print(f\"Recycling {cache_file} as overwrite={overwrite}.\")\n", + " overlapping_sets = pickle.load(f)\n", + " return overlapping_sets\n", + "\n", + " df_copy = df.copy()\n", + "\n", + " # Assuming uncertainty_radius is provided as a float in arcseconds\n", + " uncertainty_radius_as = uncertainty_radius * u.arcsec\n", + "\n", + " all_coords = SkyCoord(\n", + " ra=[x[0] for x in df_copy[\"center_coord\"]] * u.degree,\n", + " dec=[x[1] for x in df_copy[\"center_coord\"]] * u.degree,\n", + " )\n", + "\n", + " overlapping_sets = {}\n", + " set_counter = 1\n", + " processed_data_ids = []\n", + "\n", + " periodic_update_counter = 0\n", + " with progressbar.ProgressBar(max_value=len(all_coords)) as bar:\n", + " for index, coord in enumerate(all_coords):\n", + " data_id = df_copy.iloc[index][\"data_id\"]\n", + " if data_id not in processed_data_ids:\n", + " distances = (\n", + " coord.separation(all_coords).to(u.arcsec).value\n", + " ) # Convert distances to arcseconds as numeric values\n", + "\n", + " # Perform comparison as numeric values, bypassing direct unit comparison\n", + " within_radius = (distances <= uncertainty_radius_as.value) & (distances > 0)\n", + "\n", + " if any(within_radius):\n", + " overlapping_indices = [\n", + " i\n", + " for i, distance in enumerate(distances)\n", + " if (distance <= uncertainty_radius_as.value) and i != index\n", + " ]\n", + " overlapping_data_ids = df_copy.iloc[overlapping_indices][\"data_id\"].tolist()\n", + " overlapping_data_ids.append(data_id)\n", + "\n", + " processed_data_ids.extend(overlapping_data_ids)\n", + "\n", + " overlapping_sets[f\"set_{set_counter}\"] = overlapping_data_ids\n", + " set_counter += 1\n", + " #\n", + " # mitigate too much output 2/5/2024 COC\n", + " periodic_update_counter += 1\n", + " if periodic_update_counter >= 250:\n", + " periodic_update_counter = 0\n", + " bar.update(index)\n", + "\n", + " with open(cache_file, \"wb\") as f:\n", + " pickle.dump(overlapping_sets, f, protocol=pickle.HIGHEST_PROTOCOL)\n", + " print(f\"Saved overlapping_sets to {cache_file} for caching purposes.\")\n", + "\n", + " return overlapping_sets" + ] + }, + { + "cell_type": "code", + "execution_count": 442, + "id": "25710a99", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recycling /astro/users/coc123/kbmod_tmp/overlapping_sets.pickle as overwrite=False.\n", + "CPU times: user 173 ms, sys: 22.1 ms, total: 195 ms\n", + "Wall time: 192 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# TIMING NOTE: this takes about 1.25 hours [TODO update that number]\n", + "# TODO test caching\n", + "\n", + "overlapping_sets = find_overlapping_coords(df=df, uncertainty_radius=30, overwrite=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 590, + "id": "5f2d5a03", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "There are 488 discrete chip-level pointings.\n" + ] + } + ], + "source": [ + "print(f\"There are {len(overlapping_sets.keys())} discrete chip-level pointings.\") # should be 488" + ] + }, + { + "cell_type": "code", + "execution_count": 591, + "id": "9ed24e28", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 503 ms, sys: 24.9 ms, total: 527 ms\n", + "Wall time: 526 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 591, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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0qWqqKiQc047d+485/2mTZumNWvWJP7dr1+/dJYJAABCJG3Ny9dff6077rhDDz/8sG699dbE7RdffPE57xuNRpWfn5+u0gAAQIil7bTRe++9p6amJvXq1UuXX365CgoKNH36dO3ateuc962rq9PQoUNVUlKiqqoqHT58+KzrxuNxxWKxNgsAAMheaWtePv30U0lSdXW17r33Xr366qsaMmSIpkyZomPHjp31ftOnT9fatWtVW1urRx55RPX19aqoqFA8Hu9w/WXLlik3NzexFBYWpmV7AABAMCR9nZfq6mrdf//9na5TX1+vjz76SDfeeKOefPJJ3XbbbZK+OUoycuRIPfDAA5ozZ06X8g4ePKiioiKtW7euw2HfeDzeprGJxWIqLCzkOi8AAIRIMtd5SXrmZf78+brhhhs6Xae4uFgnTpyQJF1yySWJ26PRqL773e9q//79Xc4rKChQUVGRPv744w6/Ho1GFY1Gu/x4AAAg3JJuXvLy8pSXl3fO9caPH69oNKqGhgb99V//tSTp9OnT2rt3r4qKirqcd/ToUTU2NqqgoCDZUgEAQBZK28zLoEGDNHfuXC1ZskQbN25UQ0OD5s2bJ0n6yU9+klivtLRUNTU1kqQvv/xSd955p7Zs2aK9e/eqrq5OM2bMUF5enq699tp0lQoAAEIkrdd5efjhh9WnTx/dfPPNamlpUVlZmWprazVkyJDEOg0NDWpubpYk9e7dWzt37tR//Md/6Pjx4yooKNCVV16pF154QQMHDkxnqQAAICT4YEYAAGAurQO7Qdfai3G9FwAAwqP1ebsrx1SyrnlpfZcT13sBACB8Tpw4odzc3E7XybrTRn/+85914MABDRw4UJFIJHHdl8bGRk4jBQz7JrjYN8HG/gku9k33Oed04sQJDR8+XL16df5+oqw78tKrVy+NHDmy3e2DBg3iBymg2DfBxb4JNvZPcLFvuudcR1xape2t0gAAAOlA8wIAAEIl65uXaDSqJUuW8BECAcS+CS72TbCxf4KLfZMZWTewCwAAslvWH3kBAADZheYFAACECs0LAAAIFZoXAAAQKqFsXp544glddtlliYsATZo0SW+88Ubi65WVlYpEIm2WiRMntnmMeDyuX/ziF8rLy9N5552na665Rp999lmmNyXrpGLfPPXUUyovL9egQYMUiUR0/PjxDG9Fdurpvjl27Jh+8Ytf6OKLL9aAAQM0atQo/f3f/33iU+HRfan4vZkzZ47GjBmjnJwcXXjhhZo5c6b++Mc/ZnpTsk4q9k0r55ymT5+uSCSi3/72txnaguwUyuZl5MiRWr58ubZt26Zt27apoqJCM2fO1K5duxLrTJs2TQcPHkwsr7/+epvHWLBggWpqarRu3Tr993//t7788kv96Ec/0p/+9KdMb05WScW+OXnypKZNm6a777470+VntZ7umwMHDujAgQNauXKldu7cqaefflpvvvmmbr31VovNySqp+L0ZP3681qxZow8//FAbNmyQc05Tp07lb1oPpWLftFq1apUikUimSs9uLksMGTLE/du//ZtzzrnZs2e7mTNnnnXd48ePu759+7p169YlbmtqanK9evVyb775ZrpL9U4y++ZMmzZtcpLcF198kb7iPNfdfdPqxRdfdP369XOnT59OQ3V+6+m++cMf/uAkuU8++SQN1fmtO/tmx44dbuTIke7gwYNOkqupqUlvkVkulEdezvSnP/1J69at01dffaVJkyYlbq+rq9PQoUNVUlKiqqoqHT58OPG1//mf/9Hp06c1derUxG3Dhw/X2LFjtXnz5ozWn826s2+QGanaN83NzRo0aJD69Mm6j0kzk4p989VXX2nNmjUaPXq0CgsLM1G2F7q7b06ePKmf/exnWr16tfLz8zNddnay7p666/3333fnnXee6927t8vNzXWvvfZa4mvr1q1zr776qtu5c6d75ZVX3Pe+9z136aWXuv/93/91zjm3du1a169fv3aPefXVV7vbbrstY9uQrXqyb87EkZfUS9W+cc65I0eOuFGjRrl77rknU+VntVTsm8cff9ydd955TpIrLS3lqEuK9HTf3Hbbbe7WW29N/Fsceemx0DYv8Xjcffzxx66+vt4tWrTI5eXluV27dnW47oEDB1zfvn3d+vXrnXNnb17+5m/+xs2ZMyetdfugJ/vmTDQvqZeqfdPc3OzKysrctGnT3KlTp9JdthdSsW+OHz/uPvroI/f222+7GTNmuO9///uupaUlE+VntZ7sm5dfftlddNFF7sSJE4l1aF56LrTNy7ddddVVnR41ueiii9zy5cudc8797ne/c5LcsWPH2qxz2WWXufvuuy+tdfoomX1zJpqX9OvOvonFYm7SpEnuqquu4okxjbr7e9MqHo+7AQMGuOeffz4d5XktmX1zxx13uEgk4nr37p1YJLlevXq5KVOmZKji7BP6mZdWzjnF4/EOv3b06FE1NjaqoKBA0jdT+X379tVbb72VWOfgwYP64IMPNHny5IzU65Nk9g0yK9l9E4vFNHXqVPXr10+vvPKK+vfvn6lSvZOK35vOHgPdl8y+WbRokd5//33t2LEjsUjSY489pjVr1mSq5Oxj2Tl11+LFi90777zj9uzZ495//3139913u169ermNGze6EydOuH/8x390mzdvdnv27HGbNm1ykyZNciNGjHCxWCzxGHPnznUjR450//Vf/+Xee+89V1FR4b73ve+5r7/+2nDLwi8V++bgwYNu+/bt7l//9V+dJPfOO++47du3u6NHjxpuWfj1dN/EYjFXVlbm/uqv/sp98skn7uDBg4mF35ue6em+2b17t1u6dKnbtm2b27dvn9u8ebObOXOmO//8893nn39uvHXhloq/ad8mThv1WCibl7/7u79zRUVFrl+/fu7CCy90V111ldu4caNzzrmTJ0+6qVOnugsvvND17dvXjRo1ys2ePdvt37+/zWO0tLS4+fPnu/PPP9/l5OS4H/3oR+3WQfJSsW+WLFniJLVb1qxZY7BF2aOn+6b1NF5Hy549e4y2Kjv0dN80NTW56dOnu6FDh7q+ffu6kSNHulmzZrk//vGPVpuUNVLxN+3baF56LuKccxZHfAAAALoja2ZeAACAH2heAABAqNC8AACAUKF5AQAAoULzAgAAQoXmBQAAhArNCwAACBWaFwAAECo0LwAAIFRoXgAAQKjQvAAAgFCheQEAAKHyf+l78ybIIQDcAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%time\n", + "\n", + "# Looking at the processed data.\n", + "\n", + "# Create a lookup table for data_id to center_coord\n", + "id_to_coord = df.set_index(\"data_id\")[\"center_coord\"].to_dict()\n", + "\n", + "# Preparing for bulk plotting (if every point uses the same label, adjust as needed)\n", + "coords = [id_to_coord[overlapping_sets[p][0]] for p in overlapping_sets]\n", + "x_coords, y_coords = zip(*coords) # Assuming coords are tuples or lists\n", + "\n", + "# Plotting in bulk\n", + "plt.scatter(x_coords, y_coords, s=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 594, + "id": "55ffd374", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2019-09-27T00:20:22.932'" + ] + }, + "execution_count": 594, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"ut\"].iloc()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 595, + "id": "fcd39c41", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":19: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed two minor releases later. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap(obj)`` instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 6.24 s, sys: 526 ms, total: 6.77 s\n", + "Wall time: 6.24 s\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%time\n", + "\n", + "# TIMING NOTE: this requires about 7 seconds\n", + "\n", + "from matplotlib.cm import get_cmap\n", + "from matplotlib.colors import Normalize\n", + "\n", + "# Convert \"ut\" column to datetime\n", + "df[\"ut_datetime\"] = pd.to_datetime(df[\"ut\"])\n", + "\n", + "# Create lookup tables for data_id to center_coord and ut_datetime\n", + "id_to_coord = df.set_index(\"data_id\")[\"center_coord\"].to_dict()\n", + "id_to_date = df.set_index(\"data_id\")[\"ut_datetime\"].dt.date.to_dict()\n", + "\n", + "# Extract unique dates and create a color map\n", + "unique_dates = sorted(set(id_to_date.values()))\n", + "date_to_color = {date: i for i, date in enumerate(unique_dates)}\n", + "norm = Normalize(vmin=0, vmax=len(unique_dates) - 1)\n", + "cmap = get_cmap(\"tab20\", len(unique_dates)) # Choose a colormap that fits the data\n", + "\n", + "# Preparing data for plotting\n", + "coords = [id_to_coord[overlapping_sets[p][0]] for p in overlapping_sets]\n", + "dates = [id_to_date[overlapping_sets[p][0]] for p in overlapping_sets]\n", + "colors = [cmap(norm(date_to_color[date])) for date in dates]\n", + "\n", + "# Plotting\n", + "lcount = 0\n", + "for (x, y), color, date in zip(coords, colors, dates):\n", + " lcount += 1\n", + " plt.scatter(x, y, color=color, label=date.strftime(\"%Y-%m-%d\"), alpha=0.75, s=2)\n", + "\n", + "# To avoid duplicate labels in the legend, handle legend entries manually\n", + "handles, labels = plt.gca().get_legend_handles_labels()\n", + "by_label = dict(zip(labels, handles)) # Removing duplicates\n", + "plt.legend(by_label.values(), by_label.keys())\n", + "plt.savefig(f\"{basedir}/pointings.pdf\")\n", + "# plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 596, + "id": "418c97ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['data_id', 'region', 'detector', 'uri', 'center_coord', 'ut',\n", + " 'ut_datetime'],\n", + " dtype='object')" + ] + }, + "execution_count": 596, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.columns" + ] + }, + { + "cell_type": "code", + "execution_count": 597, + "id": "d06ed515", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[datetime.date(2019, 8, 28),\n", + " datetime.date(2019, 8, 29),\n", + " datetime.date(2019, 8, 30),\n", + " datetime.date(2019, 9, 27),\n", + " datetime.date(2019, 9, 28),\n", + " datetime.date(2019, 9, 29),\n", + " datetime.date(2020, 10, 17),\n", + " datetime.date(2020, 10, 19)]" + ] + }, + "execution_count": 597, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Here are the unique dates found in the discrete dataset\n", + "unique_dates = sorted(set(id_to_date.values()))\n", + "unique_dates" + ] + }, + { + "cell_type": "markdown", + "id": "3dc0bb58", + "metadata": {}, + "source": [ + "##### Double-checking a single date" + ] + }, + { + "cell_type": "code", + "execution_count": 598, + "id": "58158f47", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "data_id (instrument, detector, visit)\n", + "region ConvexPolygon([UnitVector3d(0.9847372525065534...\n", + "detector 1\n", + "uri file:///epyc/users/smotherh/DEEP/PointingGroup...\n", + "center_coord (351.0694028401149, -4.336598368890197)\n", + "ut 2019-09-27T00:20:22.932\n", + "ut_datetime 2019-09-27 00:20:22.932000\n", + "Name: 0, dtype: object" + ] + }, + "execution_count": 598, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.iloc()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 599, + "id": "2ff625e7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime.date(2019, 9, 27)" + ] + }, + "execution_count": 599, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id_to_date[df[\"data_id\"].iloc()[0]]" + ] + }, + { + "cell_type": "code", + "execution_count": 600, + "id": "c3b48b13", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(351.0694028401149, -4.336598368890197)" + ] + }, + "execution_count": 600, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id_to_coord[df[\"data_id\"].iloc()[0]]" + ] + }, + { + "cell_type": "code", + "execution_count": 601, + "id": "16506478", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6267" + ] + }, + "execution_count": 601, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We will make a dataframe with just the date we are checking\n", + "df20190828 = df[df[\"ut_datetime\"].dt.date == parser.parse(\"2019-08-28\").date()]\n", + "len(df20190828)" + ] + }, + { + "cell_type": "code", + "execution_count": 603, + "id": "166c9137", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 933 ms, sys: 33 ms, total: 966 ms\n", + "Wall time: 962 ms\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAGxCAYAAABvIsx7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA0+klEQVR4nO3de3xU1b3///cQkiHEZARGSFImJKJCDxeFRBFoBQQBy0UFuRSq5SHSeioqLVTA1iZYK2ilarHHelqLFGnBnoBQgVOQW6scf4YgCvQYIRAIhpAiOBMuTiKs7x/+mMOQ2wwymczi9Xw89uMxs/fae6/Pmmzmzd57ZhzGGCMAAACLNIt2BwAAAC41Ag4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDoBabdy4Uffdd586d+6spKQkfe1rX9Mdd9yhwsLCGm23b9+uQYMG6YorrtCVV16pUaNGad++fTXaPf/88xo1apSysrLkcDjUv3//Ovf/t7/9TX379lViYqJcLpdGjBih3bt3h1VDqP0qLy/X1KlTdfXVVysxMVEdOnTQ5MmTdfDgwZD2E85YGWP0u9/9TtnZ2UpJSVGbNm3Ur18/rV69OqzaANSPgAOgVi+99JJKSkr0yCOPaM2aNXrhhRdUUVGhm2++WRs3bgy0++ijj9S/f39VVVXp9ddf1x/+8Ad9/PHH+uY3v6l//etfQdv87W9/qwMHDujWW2/VVVddVee+V65cqdtvv11t27ZVfn6+fvvb32rPnj365je/qeLi4pD6H2q//H6/brnlFi1btkwzZszQ2rVr9dhjj2n16tXq06ePKisrL9lYSVJubq6+973v6aabblJ+fr5effVVOZ1ODR8+XMuXLw+pNgAhMABQiyNHjtSYV1lZadq1a2cGDhwYmDdmzBjjdruN1+sNzCspKTHx8fHm0UcfDVr/zJkzgcddunQx/fr1q3XfnTp1Mt27dzdnz54N2mZCQoKZMGFCSP0PtV/r1683kszvf//7oPX/9Kc/GUlm+fLlDe4r1LEyxpivfe1r5hvf+EbQvNOnTxuXy2VGjhwZUm0AGsYZHAC1atu2bY15V1xxhf7t3/5NpaWlkqQvvvhCb775pkaPHq2UlJRAuw4dOmjAgAFasWJF0PrNmjX8T86nn36qoqIi3X777XI4HEHb7Nq1q9544w2dOXOm3m2E06/4+HhJksvlCtrGlVdeKUlq0aJFg30OZazO39+F+2rRokVgAnBpEHAAhMzr9Wr79u3q0qWLJKm4uFinT59W9+7da7Tt3r279u7dq88//zysfVRVVUmSnE5njWVOp1OnTp1q8DJVOP3q27evsrOzlZeXp4KCAp04cULbt2/XY489pp49e2rQoEFh9f+cC8fqnEceeUT//d//rVdeeUXHjx/X4cOH9aMf/Uher1cPP/zwRe0LQE3No90BALHjwQcf1MmTJ/WTn/xE0pdnWySpdevWNdq2bt1axhgdP35caWlpIe+jXbt2at26td55552g+Z999pl27doVtN+6hNOv5s2ba9OmTZo4caJuuummQLv+/fsrPz8/cIYnXBeO1TnTpk1TYmKiHnzwQd1///2BPv31r39V3759L2pfAGriDA6AkDz++ONasmSJnnvuOWVnZwctO/9S0oXqW1abZs2a6cEHH9SGDRv085//XBUVFdq7d6++853v6NSpU4E2knT27Fl98cUXgenCS1eh9Ku6ulrjxo3Tjh079Lvf/U5///vftWjRIn3yySe67bbb5PV6JX356afz9/XFF1/Uue36xmrhwoV65JFHNHXqVL311ltas2aNBg8erDvuuEN/+9vfwhorAPWI8j1AAGJAXl6ekWR+8YtfBM3/6KOPjCTzm9/8psY6M2bMMA6Hw5w+fbrWbdZ3k3F1dbX54Q9/aBISEowkI8kMGzbM3H///UaSKS0tNcYY893vfjewXFJge+H066WXXjKSTEFBQVC74uJiI8nk5eUZY4zZtGlT0L4kmf3794c8VsYYc+zYMZOYmGgefPDBGsv69etnMjMzax0PAOHjEhWAes2ZM0d5eXnKy8vTY489FrSsY8eOSkxM1M6dO2ust3PnTl1zzTUXdeNs8+bN9atf/UpPPPGE9u/fL7fbrbS0NA0ZMkRZWVlq3769JCkvL09Tp04NrJecnBx2v3bs2KG4uDj17NkzqN3VV1+tNm3aBC6LZWdnq6CgIKhNenp60PP6xkqSioqKdPr0ad144401luXk5GjLli06ceKErrjiigbHCEADop2wADRdTzzxhJFkfvrTn9bZZuzYsaZt27bG5/MF5h04cMAkJCSYmTNn1rlefWdwalNYWGji4uLM888/H1L7UPs1Z84cI8m8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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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5cuXm507d5pRo0aZtLQ04/V6o67eQYMGmddff93s2rXL7NixwwwZMsRkZGSYkydP+ts01He01Txr1izTuXPngO1WVlYGtLHpNa6srAzYZkFBgZFkNm3a5G8Tjtf4cus1xphXX33VbNmyxezfv98UFRWZoUOHGrfbbb788ss6+43UMRzJmiN1HEeq3mg9hi+13kgdw+FAwKnDtddea379618bY/7xYt53330hrf/WW2+ZuLg4U11dXWebW2+91Tz22GMB87Kzs8306dONMcacO3fOpKammvnz5/uXf/HFF8bpdJpXXnklpPE0pDHqvVhlZaWRZLZs2eKfdyl9X6rGqHnWrFnmpptuqnO57a/x97//fdOxY0dz7tw5/7zGeo0vt94PP/zQSDJ79+6ts01TOoaNaZyaLxbJ47gx6rXpGL6U1zeSx/Dl4hLVRc6ePaulS5fq1KlT6tWrl3/+5s2b1a5dO914442aOHGiKisr692Ox+NRcnKymjev/fdMq6qqVFRUpIEDBwbMHzhwoLZu3SpJ2r9/vyoqKgLaxMfHq0+fPv42l6ux6q1rHUlq3bp1wPxQ+w5VY9e8Z88epaenKysrS6NHj9a+ffv8y2x+jauqqvTmm2/qkUcekcPhCFgWztf4StR76tQpvf7668rKypLb7a61TVM5hqXGq7k2kTiOG7teG47hS3l9I3UMXzGRTlhNxUcffWQSExNNTEyMcTqdZs2aNf5lS5cuNW+//bbZuXOnWb16tbnppptM586dzRdffFHrto4ePWoyMjLMj370ozr7+/TTT40k8+677wbM/+lPf2puvPFGY4wx7777rpFkPv3004A2EydONAMHDrzUUo0xjV/vxc6dO2eGDh1qvva1rwXMD7XvUESi5rVr15r/+q//Mh999JEpKCgwffr0MSkpKebo0aPGGLtf42XLlpmYmJgatYXrNb4S9f7iF78wiYmJRpLJzs6u93+6kT6GjWn8mi/W2MdxJOqN9mP4cl7fxj6GrzQCzv/P5/OZPXv2mMLCQjN9+nTjcrnM7t27a21bXl5uYmNjzfLly2ss83g8pmfPnmbw4MGmqqqqzv7O/+O4devWgPlz5swxnTp1Msb888ApLy8PaPPoo4+aQYMGhVpigMau92KPP/646dChgykrK6u3XX19hyrSNRtjzMmTJ01KSopZuHChMcbu13jgwIHm3nvvbbDdlXqNr0S9n3/+ufnkk0/Mli1bzNChQ02PHj3MmTNnat1GpI9hYxq/5os19nEc6XqNib5j+HLqbexj+Eoj4NShf//+5tvf/nady6+//vqAa67GGOP1ek2vXr1M//79G/wD8vl8JiYmxqxYsSJg/ve+9z1z5513GmOMKSkpMZLM9u3bA9oMGzbMPPzww6GU06Bw13uhSZMmmfbt25t9+/YF1b62vq+Exqz5QgMGDPDft2Hra1xaWmqaNWtm/vjHPwbVPhyv8aXUeyGfz2datmxpfve739W5vCkdw8aEv+YLNYXjuDHrvVA0HcMXCqXepnAMXy7uwamDMUY+n6/WZZ999pnKysqUlpbmn+f1ejVw4EDFxcVp9erVatGiRb3bj4uLU05OjgoKCgLmFxQUqHfv3pKkrKwspaamBrSpqqrSli1b/G2ulHDXe76PSZMmacWKFdq4caOysrIaXKe2vq+Uxqj5Yj6fT//3f//n365tr/F5r7/+utq1a6chQ4Y02DZcr3Go9Ya6jaZ2DDc03itR8/nlTeU4box6LxZNx3Co27hQUziGL1skUlVTM2PGDPOXv/zF7N+/33z00Udm5syZplmzZmb9+vXmxIkTZsqUKWbr1q1m//79ZtOmTaZXr17mK1/5iv8jgF6v1/Ts2dN07drV7N27N+Cjcxd+HO+uu+4yixYt8j8//xHT1157zfztb38zkydPNomJiaa0tNTfZv78+cbpdJoVK1aYnTt3mm984xuX/fHDSNX7b//2b8bpdJrNmzcHrHP69GljjAmq72irecqUKWbz5s1m37595r333jP33nuvSUpKsvY1NsaYs2fPmoyMDDNt2rQa4wrXa3y59ZaUlJi5c+eabdu2mQMHDpitW7ea++67z7Ru3docOXKkznojdQxHsuZIHceRqjdaj+FLrdeYyBzD4UDAMcY88sgjpkOHDiYuLs60bdvW9O/f36xfv94YY8zp06fNwIEDTdu2bU1sbKzJyMgw3/rWt8zBgwf962/atMlIqnXav3+/v12HDh3MrFmzAvr+xS9+4e+7R48eAR+1NOYfN/HNmjXLpKammvj4eHPnnXeanTt3RmW9da3z+uuvB913tNV8/vswYmNjTXp6uhk+fHiNa+g2vcbGGPOnP/3JSDLFxcU1xhWu1/hy6/3000/NPffcY9q1a2diY2NN+/btzZgxY8zHH38c0E9TOYYjWXOkjuNI1Rutx/Dl/E1H4hgOB4cxxoTzDBEAAEBj4x4cAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFjn/wHtQAVrqgshxQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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AADrNI30AHDtSp+52v+4ZN6QCI4EAOrHv1nRhYADRAH+YQ3epftqwaiuur+nO4Kjafz424KtuEQFwForPyiN9BAARAhncBAx/G8R4TbilrRIDwEW4d+s6OIwxphID6Kheb1eOZ1OeTweJSUlRXo4AAAgCKG8f3OJCgAAWIeAAwAArNMgAcfn86lbt25yOBzavn17re2qqqo0Y8YMdenSRQkJCUpLS9NDDz2k0tLAGwWzsrLkcDgCprFjx4a5CgAAEC0aJOA89thjSkur/2a/M2fOaNu2bXr88ce1bds2LV++XLt379bw4cOrtZ00aZKOHDnin1566aVwDB0AAEShsH+Kau3atVq3bp2WLVumtWvX1tnW6XRq/fr1AfMWLlyor33tazp48KDatWvnn9+iRQulpKSEZcwAACC6hfUMztGjRzVp0iQtXrxYLVq0uKJteDweORwOXX/99QHz8/Ly5HK51KlTJ02fPl0VFRW1bsPn88nr9QZMAADAXmE7g2OM0YQJE/Too4+qV69eKikpCXkbn332mWbOnKlx48YFfBxs/PjxysjIUEpKinbu3KlZs2bpgw8+qHb256Lc3FzNmTPnSksBAABRJuTvwcnJyak3LBQUFCg/P19Lly7VP/7xD8XExKikpEQZGRl6//331a1bt3r7qaqq0qhRo3Tw4EFt3ry5zs+7FxYWqlevXiosLFSPHj2qLff5fPL5fP7nXq9Xbreb78EBACCKhPI9OCEHnGPHjunYsWN1tklPT9fYsWP13//933I4HP7558+fV0xMjMaPH69XX3211vWrqqo0evRo7du3Txs3blTr1q3r7M8Yo/j4eC1evFhjxoyptwa+6A8AgOgTyvt3yJeoXC6XXC5Xve1eeOEFzZ071/+8tLRUgwYN0tKlS9W7d+9a17sYbvbs2aNNmzbVG24kadeuXaqqqlJqampwRQAAAKuF7R6cSz/xJEnXXXedJKlDhw5q27atf35mZqZyc3N133336dy5c3rggQe0bds2vfXWWzp//rzKysokSa1atVJcXJyKi4uVl5enb3zjG3K5XProo480bdo0de/eXbfffnu4ygEAAFEk4j+2WVRUJI/HI0k6fPiwVq1aJUnV7tPZtGmTsrKyFBcXpw0bNuj555/XqVOn5Ha7NWTIEM2ePVsxMTENPXwAANAI8WOb3IMDAEBU4Mc2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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAGxCAYAAABvIsx7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA0wUlEQVR4nO3de3RU5b3/8c8QciGRjMIAScqEUBRTAQVCRbCnQMFAG0TuUKiUirQexcpZUC62NsGFhtpSa1F77ClVS9OCFigcgSPIrbWUIwRRwGO4hoshRC7OhNskwvP7wx9ThtxmgMlkHt6vtfZaM89+9t7Pd08282HvPTMOY4wRAACARRpFegAAAADXGwEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQdAtdatW6eHHnpImZmZSkpK0pe+9CU98MADKiwsrNJ327Zt6tevn2666SbdfPPNGjp0qPbv3x/QZ/fu3Zo6daqysrJ08803q1mzZrr33nv1l7/8pdrtl5WVafz48XK5XEpMTFSPHj20du3aoMf/7rvv6uGHH1ZWVpbi4+PlcDhUXFxcY/958+YpMzNT8fHxatu2rWbNmqXKysqgtlVeXq5p06YpOztbLVq0kMPhUF5eXrV9HQ5HjVNmZmbQ9QGoHQEHQLV+85vfqLi4WE888YRWrlypF154QWVlZbrnnnu0bt06f7+PP/5YvXv3VkVFhd544w39/ve/1+7du/Vv//Zv+vTTT/39Vq9erRUrVmjYsGF68803VVBQoNtuu00jRozQ008/HbBtn8+nvn37au3atXrhhRe0bNkytWrVSgMGDNDGjRuDGv/atWv1zjvvKD09XT179qy17zPPPKMnnnhCQ4cO1dtvv61HH31Uzz77rB577LGgtnXixAn99re/lc/n0+DBg2vt+89//rPK9Ktf/UqSNGTIkKC2ByAIBgCqcezYsSpt5eXlplWrVqZv377+thEjRhiXy2U8Ho+/rbi42MTGxppp06b52z799FNz8eLFKuvMyckxiYmJ5vz58/62l156yUgymzZt8rdVVlaaO+64w9x9991Bjf/ChQv+xz//+c+NJHPgwIEq/Y4fP24SEhLM97///YD2Z555xjgcDrNr1646t3Xx4kV/bZ9++qmRZHJzc4MapzHGjB8/3jgcDrNnz56glwFQO87gAKhWy5Ytq7TddNNNuuOOO3T48GFJ0ueff6633npLw4YNU3Jysr9fmzZt1KdPHy1dutTf5nK55HA4qqzz7rvv1tmzZ3Xy5El/29KlS3X77berR48e/rbGjRvrO9/5jt577z198skndY6/UaPg/nn7n//5H50/f17f+973Atq/973vyRijv/71r3Wu49IlpqtRXl6uN998U7169dKtt956VesAUBUBB0DQPB6Ptm3bpg4dOkiS9u3bp3PnzunOO++s0vfOO+/U3r17df78+VrXuX79erVo0SIgUO3cubPGdUrSrl27rqWMADt37pQkderUKaA9NTVVLpfLPz9cFi5cqDNnzujhhx8O63aAGw0BB0DQHnvsMZ05c0Y//vGPJX1x74kkNWvWrErfZs2ayRijU6dO1bi+3/3ud9qwYYN+8pOfKCYmxt9+4sSJGtd5+XavhxMnTig+Pl5JSUnVbu96bqs68+fP180336xhw4aFdTvAjaZxpAcAIDo89dRTKigo0Lx585SVlRUwr7bLMzXNW7VqlR577DENHz5cjz/+eNDLXT7v4sWLunjxYkD75UEpWMFsyxijCxcuBMxr3Pja/gndtWuX/vd//1ePPfaYEhISrmldAAJxBgdAnWbNmqXZs2frmWee0aRJk/ztzZs3l1T9GZWTJ0/K4XDo5ptvrjLv7bff1tChQ3XfffepoKCgSsBo3rx5jeuU/nUm56GHHlJsbKx/6tu3b8i1NW/eXOfPn9fZs2er3d6lbW3cuDFgW7GxsbV+7DwY8+fPlyQuTwFhwBkcALWaNWuW8vLylJeXpyeffDJgXrt27dSkSRPt2LGjynI7duzQrbfeWuXMxNtvv63BgwerV69eWrx4seLi4qos26lTpxrXKUkdO3aUJOXl5QUErqZNm4Zc36V7b3bs2KHu3bv720tLS3X8+HH/trKysrRly5aAZdPS0kLe3iUVFRVasGCBsrKy1Llz56teD4AaRPhTXAAasKefftpIMj/5yU9q7DNy5EjTsmVL4/V6/W0HDx40cXFxZvr06QF93377bZOQkGD69etnzp07V+M6X375ZSPJbN682d9WWVlpOnToYLp37x5yHbV9TPzEiRMmISHBPPLIIwHt+fn5QX9M/HLBfkz8zTffNJLMyy+/HNL6AQSHMzgAqjV37lz99Kc/1YABA5STk6PN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Onr0qHbu3KnnnnsuxGqA6JAx419nMIvn5ERwJA0f+yo07C/YKmyfokpPTw94ftNNN0mS2rVrp9atW/vbMzMzlZ+fryFDhuj06dPKy8vTsGHDlJqaquLiYj355JNyuVwaMmSIJMnpdGrChAmaMmWKmjdvrmbNmmnq1Knq1KmT/1NVAADgxhb2j4nXpaioSB6PR5IUExOjHTt26A9/+IM+++wzpaamqk+fPlq0aJGaNm3qX+b5559X48aNNXLkSJ07d059+/bVa6+9ppiYmEiVAQAAGhCHMcZEehD1zev1yul0yuPxKDk5OdLDAQAAQQjl/ZvfogIAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgncaRHkAkXPoBda/XG+GRAACAYF163770Pl6bGzLglJeXS5LcbneERwIAAEJVXl4up9NZax+HCSYGWebixYsqKSlR06ZN5XA4wrYdr9crt9utw4cPKzk5OWzbaShutHqlG6/mG61e6carmXrtF801G2NUXl6utLQ0NWpU+102N+QZnEaNGql169b1tr3k5OSo+yO6FjdavdKNV/ONVq9049VMvfaL1prrOnNzCTcZAwAA6xBwAACAdQg4YRQfH6/c3FzFx8dHeij14karV7rxar7R6pVuvJqp1343Ss035E3GAADAbpzBAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQKOpN/85je68847/d/q2KNHD61atco/f/z48XI4HAHTPffc459/8uRJPf7447r99tuVmJio9PR0/fCHP5TH46lz2y+//LLatm2rhIQEZWVl6e9//3vAfGOM8vLylJaWpiZNmqh3797atWtXVNabn5+vr371q2ratKlatmypwYMHq6ioKKBPXduOtprz8vKqrDclJSWgj02vcUZGRpX1OhwOPfbYY0FvOxL1StIPfvADtWvXTk2aNFGLFi30wAMP6OOPP65z25E4hiNZc6SO40jVG63H8NXWG6ljOCwMzPLly82KFStMUVGRKSoqMk8++aSJjY01O3fuNMYY893vftcMGDDAHD161D+dOHHCv/yOHTvM0KFDzfLly83evXvN2rVrzW233WaGDRtW63YXLlxoYmNjzX/913+Zjz76yDzxxBMm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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAGxCAYAAABvIsx7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA1T0lEQVR4nO3de3RU9b3//9cYciGRjMAAScqEUBRTAQVCRbAVKNcWROReqJaKtFah0gXlYmsTXEiollpE7dHWqqWxoAWEI3AEubXK4ScEUcBjuIaLIUQuzoTbJMLn94dfpgy5zSCTyXx4Ptbaa8189mfv/XnvyWZe7L1nxmGMMQIAALDIdZEeAAAAwNVGwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAVCptWvX6oEHHlBmZqaSkpL0jW98Q/fcc4/y8/Mr9N26dat69eql66+/XjfccIMGDx6sffv2BfTZtWuXJk+erKysLN1www1q1KiR7rzzTv3zn/+sdPslJSUaM2aMXC6XEhMT1aVLF61Zsybo8b/33nt68MEHlZWVpfj4eDkcDhUWFlbZf968ecrMzFR8fLxatmypGTNmqLy8PKhtlZaWasqUKerTp4+aNGkih8OhnJycSvsaY/Tss8/6t5Wamqqf//znOnnyZNC1AagZAQdApf70pz+psLBQjz76qFasWKG5c+eqpKREd9xxh9auXevv9+mnn6p79+4qKyvTG2+8ob/+9a/atWuXvvvd7+rzzz/391u1apWWL1+uIUOG6M0331ReXp5uuukmDRs2TE888UTAtn0+n3r27Kk1a9Zo7ty5Wrp0qZo1a6Z+/fppw4YNQY1/zZo1evfdd5Wenq6uXbtW2/fJJ5/Uo48+qsGDB+udd97Rww8/rFmzZumRRx4JalvHjx/XSy+9JJ/Pp0GDBlXbd/LkyfrlL3+pe+65R2+//bamTZum119/Xb179w46UAEIggGAShw9erRCW2lpqWnWrJnp2bOnv23YsGHG5XIZj8fjbyssLDSxsbFmypQp/rbPP//cXLhwocI6+/fvbxITE825c+f8bc8//7yRZDZu3OhvKy8vN7fccou5/fbbgxr/+fPn/Y+ffvppI8ns37+/Qr9jx46ZhIQE89Of/jSg/cknnzQOh8Ps3Lmzxm1duHDBX9vnn39uJJns7OwK/Q4fPmxiYmLMhAkTAtpff/11I8m89NJLQVQGIBicwQFQqaZNm1Zou/7663XLLbfo0KFDkqQvv/xSb7/9toYMGaLk5GR/vxYtWqhHjx5asmSJv83lcsnhcFRY5+23364zZ87oxIkT/rYlS5bo5ptvVpcuXfxt9erV049+9CN98MEH+uyzz2oc/3XXBffP2//8z//o3Llz+slPfhLQ/pOf/ETGGL311ls1rsPhcFRa2+U2bdqk8+fP6wc/+EFA+4ABAyRJixYtCmrMAGpGwAEQNI/Ho61bt6pNmzaSpL179+rs2bO69dZbK/S99dZbtWfPHp07d67ada5bt05NmjQJCFQ7duyocp2StHPnzq9TRoAdO3ZIktq1axfQnpqaKpfL5Z9/NZSVlUmS4uPjA9pjY2PlcDj08ccfX7VtAdc6Ag6AoD3yyCM6ffq0fv3rX0v66t4TSWrUqFGFvo0aNZIxptqbZ//yl79o/fr1+s1vfqOYmBh/+/Hjx6tc56XbvRqOHz+u+Ph4JSUlVbq9q7mtW265RZL0/vvvB7Rv3LhRxpirui3gWlcv0gMAEB0ef/xx5eXlad68ecrKygqYV93lmarmrVy5Uo888oiGDh2qCRMmBL3cpfMuXLigCxcuBLRfGpSCFcy2jDE6f/58wLx69UL7J/S2227TXXfdpaefflo333yzevfurU8++UQPPfSQYmJigr6sBqBmHE0AajRjxgzNnDlTTz75pMaPH+9vb9y4saTKz6icOHFCDodDN9xwQ4V577zzjgYPHqzevXsrLy+vQsBo3LhxleuU/nMm54EHHlBsbKx/6tmzZ8i1NW7cWOfOndOZM2cq3d7FbW3YsCFgW7GxsdV+7Lwqb775pu68804NHz5cDRs2VI8ePTR48GC1b99e3/jGN0JeH4DKcQYHQLVmzJihnJwc5eTk6LHHHguY16pVK9WvX1/bt2+vsNz27dt14403KiEhIaD9nXfe0aBBg9StWzctWrRIcXFxFZZt165dleuUpLZt20qScnJyAgJXgwYNQq7v4r0327dvV+fOnf3txcXFOnbsmH9bWVlZ2rx5c8CyaWlpIW+vadOmWrFihUpKSlRcXKwWLVqofv36euGFFzR06NCQ1wegCpH9EBeAuuyJJ54wksxvfvObKvsMHz7cNG3a1Hi9Xn/bgQMHTFxcnJk6dWpA33feecckJCSYXr16mbNnz1a5zhdeeMFIMps2bfK3lZeXmzZt2pjOnTuHXEd1HxM/fvy4SUhIMA899FBAe25ubtAfE79UdR8Tr8rcuXPNddddZ/Lz80PaFoCqcQYHQKXmzJmj3/72t+rXr5/69++vTZs2Bcy/4447JH11hufb3/62BgwYoGnTpuncuXP67W9/K5fLpUmTJvn7v/feexo0aJBSUlL02GOPadu2bQHru+WWW/wfNX/ggQf0/PPPa9iwYZo9e7aaNm2qF154QQUFBXr33XeDGv/nn3/u/1LAi2d+Vq5cqSZNmqhJkybq1q2bpK8ud/3mN7/R448/rkaNGqlPnz7avHmzcnJy9OCDD/pvDK7JypUrdfr0aZWWlkqSPvnkE/+3NP/gBz9QYmKiJOnPf/6zpK/Ofn3xxRdauXKlXn75Zc2aNUsdO3YMalsAghDphAWgburWrZuRVOV0qS1btpiePXuaxMREk5ycbAYNGmT27NkT0Cc7O7va9a1bty6gf3Fxsbn//vtNo0aNTEJCgrnjjjvM6tWrgx7/unXrqtxWt27dKvSfO3euad26tYmLizPp6ekmOzvblJWVBb29Fi1aVLm9S88cvfjii+Zb3/qWSUxMNNdff7357ne/a956662gtwMgOA5jjKmtMAUAAFAb+BQVAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1rskv+rtw4YKKiorUoEGDan9kDwAA1B3GGJWWliotLa3GH6e9JgNOUVGR3G53pIcBAACuwKFDh9S8efNq+1yTAefiD/IdOnTI/9XwAACgbvN6vXK73UH9sO41GXAuXpZKTk4m4AAAEGWCub2Em4wBAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsM41+WObAAApY9py/2OHpP2z+0duMMBVRsABosClb0SFvAkhDEykBwBcZVyiAgAA1glbwCksLNTYsWPVsmVL1a9fX61atVJ2drbKysqqXc7hcFQ6Pf300/4+3bt3rzB/5MiR4SoFAKzU55aminFI9WOv08SeN0Z6OMBVFbZLVJ9++qkuXLigF198UTfeeKN27NihcePG6fTp0/r9739f5XJHjhwJeL5y5UqNHTtWQ4YMCWgfN26cnnjiCf/z+vXrX90CgDqEy1IIh5fu/3akhwCETdgCTr9+/dSvXz//829+85sqKCjQn/70p2oDTkpKSsDzpUuXqkePHvrmN78Z0J6YmFihLwAAgFTL9+B4PB41atQo6P5Hjx7V8uXLNXbs2Arz8vLy5HK51KZNG02ePFmlpaVVrsfn88nr9QZMAADAXrX2Kaq9e/dq3rx5mjNnTtDLvPbaa2rQoIEGDx4c0D569Gi1bNlSKSkp2rFjh6ZPn66PPvpIq1evrnQ9ubm5mjFjxtcaPwAAiB4OY0xInw7MycmpMSxs3rxZnTp18j8vKipSt27d1K1bN/3lL38JeluZmZnq3bu35s2bV22//Px8derUSfn5+erYsWOF+T6fTz6fz//c6/XK7XbL4/EoOTk56PEAAIDI8Xq9cjqdQb1/h3wGZ/z48TV+YikjI8P/uKioSD169FCXLl300ksvBb2df//73yooKNDChQtr7NuxY0fFxsZq9+7dlQac+Ph4xcfHB71tAAAQ3UIOOC6XSy6XK6i+n332mXr06KGsrCy98soruu664G/5efnll5WVlaXbbrutxr47d+5UeXm5UlNTg14/AACwV9huMi4qKlL37t3ldrv1+9//Xp9//rmKi4tVXFwc0C8zM1NLliwJaPN6vXrzzTf14IMPVljv3r179cQTT2jLli0qLCzUihUrNGzYMHXo0EF33nlnuMoBAABRJGw3Ga9atUp79uzRnj171Lx584B5l972U1BQII/HEzB/wYIFMsbohz/8YYX1xsXFac2aNZo7d65OnTolt9ut/v37Kzs7WzExMeEpBgAARJWQbzK2QSg3KQEAgLohlPdvfosKAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWKdepAcA1DUZ05b7HxfO7h/BkdR97Kvgsa+Cx77C1cAZHAAAYB0CDgAAsI7DGGMiPYja5vV65XQ65fF4lJycHOnhAACAIITy/s0ZHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1whpwBg4cqPT0dCUkJCg1NVX33XefioqKql3GGKOcnBylpaWpfv366t69u3bu3BnQx+fzacKECXK5XEpKStLAgQN1+PDhcJYCAACiSFgDTo8ePfTGG2+ooKBAixYt0t69ezV06NBql3nqqaf0hz/8Qc8995w2b96slJQU9e7dW6Wlpf4+EydO1JIlS7RgwQK99957OnXqlAYMGKDz58+HsxwAABAlHMYYU1sbW7ZsmQYNGiSfz6fY2NgK840xSktL08SJEzV16lRJX52tadasmX73u9/pZz/7mTwej5o0aaL58+drxIgRkqSioiK53W6tWLFCffv2rXEcXq9XTqdTHo9HycnJV7dIAAAQFqG8f9faPTgnTpxQXl6eunbtWmm4kaT9+/eruLhYffr08bfFx8erW7du2rhxoyQpPz9f5eXlAX3S0tLUtm1bf5/L+Xw+eb3egAkAANgr7AFn6tSpSkpKUuPGjXXw4EEtXbq0yr7FxcWSpGbNmgW0N2vWzD+vuLhYcXFxatiwYZV9Lpebmyun0+mf3G731ykJAADUcSEHnJycHDkcjmqnLVu2+Pv/6le/0ocffqhVq1YpJiZG999/v2q6KuZwOAKeG2MqtF2uuj7Tp0+Xx+PxT4cOHQqyWgAAEI3qhbrA+PHjNXLkyGr7ZGRk+B+7XC65XC61bt1a3/rWt+R2u7Vp0yZ16dKlwnIpKSmSvjpLk5qa6m8vKSnxn9VJSUlRWVmZTp48GXAWp6SkRF27dq10PPHx8YqPjw+6RgAAEN1CDjgXA8uVuHjmxufzVTq/ZcuWSklJ0erVq9WhQwdJUllZmTZs2KDf/e53kqSsrCzFxsZq9erVGj58uCTpyJEj2rFjh5566qkrGhcAALBLyAEnWB988IE++OADfec731HDhg21b98+/fa3v1WrVq0Czt5kZmYqNzdX9957rxwOhyZOnKhZs2bppptu0k033aRZs2YpMTFRo0aNkiQ5nU6NHTtWkyZNUuPGjdWoUSNNnjxZ7dq1U69evcJVDgAAiCJhCzj169fX4sWLlZ2drdOnTys1NVX9+vXTggULAi4XFRQUyOPx+J9PmTJFZ8+e1cMPP6yTJ0+qc+fOWrVqlRo0aODv88wzz6hevXoaPny4zp49q549e+rVV19VTExMuMoBAABRpFa/B6eu4HtwAACIPqG8f4ftDA6Aqydj2nL/48LZ/SM4krqPfRW8S/eVxP6CXQg4iBjeiABEC8Jg9OHXxAEAgHU4gwNEAf63GDz2VfDYV7AZNxlzkzEAAFGhTv7YJgAAQG0h4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOvUi/QAUDsypi33Py6c3T+CI6nbLt1PEvuqJvxdBY99FTz2VfDYV1XjDA4AALAOAQcAAFjHYYwxkR5EbfN6vXI6nfJ4PEpOTo70cAAAQBBCef8O6xmcgQMHKj09XQkJCUpNTdV9992noqKiKvuXl5dr6tSpateunZKSkpSWlqb777+/wjLdu3eXw+EImEaOHBnOUgAAQBQJa8Dp0aOH3njjDRUUFGjRokXau3evhg4dWmX/M2fOaOvWrXr88ce1detWLV68WLt27dLAgQMr9B03bpyOHDnin1588cVwlgIAAKJIrV6iWrZsmQYNGiSfz6fY2Nigltm8ebNuv/12HThwQOnp6ZK+OoPTvn17/fGPf7yicXCJCgCA6FNnLlFd6sSJE8rLy1PXrl2DDjeS5PF45HA4dMMNNwS05+XlyeVyqU2bNpo8ebJKS0urXIfP55PX6w2YAACAvcIecKZOnaqkpCQ1btxYBw8e1NKlS4Ne9ty5c5o2bZpGjRoVkNRGjx6tf/zjH1q/fr0ef/xxLVq0SIMHD65yPbm5uXI6nf7J7XZ/rZoAAEDdFvIlqpycHM2YMaPaPps3b1anTp0kSceOHdOJEyd04MABzZgxQ06nU2+//bYcDke16ygvL9ewYcN08OBBrV+/vtpTUfn5+erUqZPy8/PVsWPHCvN9Pp98Pp//udfrldvt5hIVAABRJJRLVCEHnGPHjunYsWPV9snIyFBCQkKF9sOHD8vtdmvjxo3q0qVLlcuXl5dr+PDh2rdvn9auXavGjRtXuz1jjOLj4zV//nyNGDGixhq4BwcAgOgTyvt3yD/V4HK55HK5rmhgF7PUpWdTLncx3OzevVvr1q2rMdxI0s6dO1VeXq7U1NQrGhcAALBL2O7B+eCDD/Tcc89p27ZtOnDggNatW6dRo0apVatWAWdvMjMztWTJEknSl19+qaFDh2rLli3Ky8vT+fPnVVxcrOLiYpWVlUmS9u7dqyeeeEJbtmxRYWGhVqxYoWHDhqlDhw668847w1UOAACIImH7sc369etr8eLFys7O1unTp5Wamqp+/fppwYIFio+P9/crKCiQx+OR9NUlrGXLlkmS2rdvH7C+devWqXv37oqLi9OaNWs0d+5cnTp1Sm63W/3791d2drZiYmLCVQ4AAIgi/FQD9+AAABAV6uT34AAAANQWAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdepFegAAcC3ImLbc/7hwdv8IjgS4NhBwrhH84xqcS/eTxL6qCX9XCAf+roLHvqoal6gAAIB1OIMDALWA/10DtcthjDGRHkRt83q9cjqd8ng8Sk5OjvRwAABAEEJ5/+YSFQAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGCdsAacgQMHKj09XQkJCUpNTdV9992noqKiapcZM2aMHA5HwHTHHXcE9PH5fJowYYJcLpeSkpI0cOBAHT58OJylAACAKBLWgNOjRw+98cYbKigo0KJFi7R3714NHTq0xuX69eunI0eO+KcVK1YEzJ84caKWLFmiBQsW6L333tOpU6c0YMAAnT9/PlylAACAKOIwxpja2tiyZcs0aNAg+Xw+xcbGVtpnzJgx+uKLL/TWW29VOt/j8ahJkyaaP3++RowYIUkqKiqS2+3WihUr1Ldv3xrH4fV65XQ65fF4lJycfMX1AACA2hPK+3et3YNz4sQJ5eXlqWvXrlWGm4vWr1+vpk2bqnXr1ho3bpxKSkr88/Lz81VeXq4+ffr429LS0tS2bVtt3Lix0vX5fD55vd6ACQAA2CvsAWfq1KlKSkpS48aNdfDgQS1durTa/t///veVl5entWvXas6cOdq8ebO+973vyefzSZKKi4sVFxenhg0bBizXrFkzFRcXV7rO3NxcOZ1O/+R2u69OcQAAoE4KOeDk5ORUuAn48mnLli3+/r/61a/04YcfatWqVYqJidH999+v6q6KjRgxQv3791fbtm119913a+XKldq1a5eWL19e7biMMXI4HJXOmz59ujwej386dOhQqGUDAIAoUi/UBcaPH6+RI0dW2ycjI8P/2OVyyeVyqXXr1vrWt74lt9utTZs2qUuXLkFtLzU1VS1atNDu3bslSSkpKSorK9PJkycDzuKUlJSoa9eula4jPj5e8fHxQW0PAABEv5ADzsXAciUunrm5eLkpGMePH9ehQ4eUmpoqScrKylJsbKxWr16t4cOHS5KOHDmiHTt26KmnnrqicQEAALuE7R6cDz74QM8995y2bdumAwcOaN26dRo1apRatWoVcPYmMzNTS5YskSSdOnVKkydP1v/+7/+qsLBQ69ev19133y2Xy6V7771XkuR0OjV27FhNmjRJa9as0Ycffqgf/ehHateunXr16hWucgAAQBQJ+QxOsOrXr6/FixcrOztbp0+fVmpqqvr166cFCxYEXC4qKCiQx+ORJMXExGj79u3629/+pi+++EKpqanq0aOHFi5cqAYNGviXeeaZZ1SvXj0NHz5cZ8+eVc+ePfXqq68qJiYmXOUAAIAoUqvfg1NXhPN7cDKm/edm6MLZ/a/qum3Dvgoe+yp4l+4rif1VE/62gse+Cl649lWd/B4cAACA2kLAAQAA1uESFT/VAABAVOASFQAAuKYRcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDr1Ij0A22RMW+5/XDi7fwRHUvexr4LHvgoe+yp4I17cqP9v/0n/c/ZX9fjbCl5d2FecwQGAa9SWwpM1dwKiFAEHAK5RnTIaRnoIQNg4jDEm0oOobV6vV06nUx6PR8nJyZEeDgAACEIo79+cwQEAANYJa8AZOHCg0tPTlZCQoNTUVN13330qKiqqdhmHw1Hp9PTTT/v7dO/evcL8kSNHhrMUAAAQRcIacHr06KE33nhDBQUFWrRokfbu3auhQ4dWu8yRI0cCpr/+9a9yOBwaMmRIQL9x48YF9HvxxRfDWQoAAIgiYf2Y+C9/+Uv/4xYtWmjatGkaNGiQysvLFRsbW+kyKSkpAc+XLl2qHj166Jvf/GZAe2JiYoW+AAAAUi3eg3PixAnl5eWpa9euVYabyx09elTLly/X2LFjK8zLy8uTy+VSmzZtNHnyZJWWlla5Hp/PJ6/XGzABAAB7hT3gTJ06VUlJSWrcuLEOHjyopUuXBr3sa6+9pgYNGmjw4MEB7aNHj9Y//vEPrV+/Xo8//rgWLVpUoc+lcnNz5XQ6/ZPb7b7iegAAQN0X8sfEc3JyNGPGjGr7bN68WZ06dZIkHTt2TCdOnNCBAwc0Y8YMOZ1Ovf3223I4HDVuKzMzU71799a8efOq7Zefn69OnTopPz9fHTt2rDDf5/PJ5/P5n3u9Xrndbj4mDgBAFAnlY+IhB5xjx47p2LFj1fbJyMhQQkJChfbDhw/L7XZr48aN6tKlS7Xr+Pe//6277rpL27Zt02233VZtX2OM4uPjNX/+fI0YMaLGGvgeHAAAok8o798h32TscrnkcrmuaGAXs9SlZ1Oq8vLLLysrK6vGcCNJO3fuVHl5uVJTU69oXAAAwC5huwfngw8+0HPPPadt27bpwIEDWrdunUaNGqVWrVoFnL3JzMzUkiVLApb1er1688039eCDD1ZY7969e/XEE09oy5YtKiws1IoVKzRs2DB16NBBd955Z7jKAQAAUSRsAad+/fpavHixevbsqZtvvlkPPPCA2rZtqw0bNig+Pt7fr6CgQB6PJ2DZBQsWyBijH/7whxXWGxcXpzVr1qhv3766+eab9Ytf/EJ9+vTRu+++q5iYmHCVAwAAogi/RcU9OAAARAV+iwoAAFzTCDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArFMv0gNA+GVMW+5/PGfYrRqS5Y7gaOq2S/dV4ez+ERxJdGB/Be/jw1/ovT3H9J0bXbq1+Q2RHk6dxt9V8NhXVeMMzjVm6UdFkR4CcE0a+Nz7eup/CjTwufcjPRTgmkDAucbcc1tapIcAAEDYOYwxJtKDqG1er1dOp1Mej0fJycmRHg6AawCXEoCvL5T3b+7BAYBaQKgBaheXqAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1aiXg+Hw+tW/fXg6HQ9u2bau2rzFGOTk5SktLU/369dW9e3ft3LmzwvomTJggl8ulpKQkDRw4UIcPHw5jBQAAIJrUSsCZMmWK0tLSgur71FNP6Q9/+IOee+45bd68WSkpKerdu7dKS0v9fSZOnKglS5ZowYIFeu+993Tq1CkNGDBA58+fD1cJAAAgioQ94KxcuVKrVq3S73//+xr7GmP0xz/+Ub/+9a81ePBgtW3bVq+99prOnDmj119/XZLk8Xj08ssva86cOerVq5c6dOigv//979q+fbvefffdcJcDAACiQFgDztGjRzVu3DjNnz9fiYmJNfbfv3+/iouL1adPH39bfHy8unXrpo0bN0qS8vPzVV5eHtAnLS1Nbdu29fe5nM/nk9frDZgAAIC9whZwjDEaM2aMHnroIXXq1CmoZYqLiyVJzZo1C2hv1qyZf15xcbHi4uLUsGHDKvtcLjc3V06n0z+53e5QywEAAFEk5ICTk5Mjh8NR7bRlyxbNmzdPXq9X06dPD3lQDocj4LkxpkLb5arrM336dHk8Hv906NChkMcEAACiR71QFxg/frxGjhxZbZ+MjAzNnDlTmzZtUnx8fMC8Tp06afTo0XrttdcqLJeSkiLpq7M0qamp/vaSkhL/WZ2UlBSVlZXp5MmTAWdxSkpK1LVr10rHEx8fX2EcAADAXiEHHJfLJZfLVWO/Z599VjNnzvQ/LyoqUt++fbVw4UJ17ty50mVatmyplJQUrV69Wh06dJAklZWVacOGDfrd734nScrKylJsbKxWr16t4cOHS5KOHDmiHTt26Kmnngq1HAAAYKGQA06w0tPTA55ff/31kqRWrVqpefPm/vbMzEzl5ubq3nvvlcPh0MSJEzVr1izddNNNuummmzRr1iwlJiZq1KhRkiSn06mxY8dq0qRJaty4sRo1aqTJkyerXbt26tWrV7jKAQAAUSRsASdYBQUF8ng8/udTpkzR2bNn9fDDD+vkyZPq3LmzVq1apQYNGvj7PPPMM6pXr56GDx+us2fPqmfPnnr11VcVExMTiRLqvIxpy/2PC2f3j+BI6j72VWjYX8FjXwWPfRU89lXVai3gZGRkyBhTof3yNofDoZycHOXk5FS5roSEBM2bN0/z5s272sMEAAAW4LeoAACAdRymstMqlvN6vXI6nfJ4PEpOTo70cAAAQBBCef/mDA4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFinXqQHgGtXxrTl/seFs/tHcCQAUDP+zYouBBwgCvAPa/Au3Vdzht2qIVnuCI6m7uNvC7biEhUAay39qCjSQwAQIZzBQcTwv0WE2z23pUV6CLAI/2ZFF4cxxkR6ELXN6/XK6XTK4/EoOTk50sMBAABBCOX9m0tUAADAOgQcAABgnVoJOD6fT+3bt5fD4dC2bduq7FdeXq6pU6eqXbt2SkpKUlpamu6//34VFQXeKNi9e3c5HI6AaeTIkWGuAgAARItaCThTpkxRWlrNN/udOXNGW7du1eOPP66tW7dq8eLF2rVrlwYOHFih77hx43TkyBH/9OKLL4Zj6AAAIAqF/VNUK1eu1KpVq7Ro0SKtXLmy2r5Op1OrV68OaJs3b55uv/12HTx4UOnp6f72xMREpaSkhGXMAAAguoX1DM7Ro0c1btw4zZ8/X4mJiVe0Do/HI4fDoRtuuCGgPS8vTy6XS23atNHkyZNVWlpa5Tp8Pp+8Xm/ABAAA7BW2MzjGGI0ZM0YPPfSQOnXqpMLCwpDXce7cOU2bNk2jRo0K+DjY6NGj1bJlS6WkpGjHjh2aPn26Pvroowpnfy7Kzc3VjBkzrrQUAAAQZUL+HpycnJwaw8LmzZu1ceNGLVy4UP/6178UExOjwsJCtWzZUh9++KHat29f43bKy8s1bNgwHTx4UOvXr6/28+75+fnq1KmT8vPz1bFjxwrzfT6ffD6f/7nX65Xb7eZ7cAAAiCKhfA9OyAHn2LFjOnbsWLV9MjIyNHLkSP33f/+3HA6Hv/38+fOKiYnR6NGj9dprr1W5fHl5uYYPH659+/Zp7dq1aty4cbXbM8YoPj5e8+fP14gRI2qsgS/6AwAg+oTy/h3yJSqXyyWXy1Vjv2effVYzZ870Py8qKlLfvn21cOFCde7cucrlLoab3bt3a926dTWGG0nauXOnysvLlZqaGlwRAADAamG7B+fSTzxJ0vXXXy9JatWqlZo3b+5vz8zMVG5uru699159+eWXGjp0qLZu3aq3335b58+fV3FxsSSpUaNGiouL0969e5WXl6cf/OAHcrlc+uSTTzRp0iR16NBBd955Z7jKAQAAUSTiP7ZZUFAgj8cjSTp8+LCWLVsmSRXu01m3bp26d++uuLg4rVmzRnPnztWpU6fkdrvVv39/ZWdnKyYmpraHDwAA6iB+bJN7cAAAiAr82CYAALimEXAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsE69SA8AqGsypi33Py6c3T+CI6n72FfBY18Fj32Fq4EzOAAAwDoEHAAAYB2HMcZEehC1zev1yul0yuPxKDk5OdLDAQAAQQjl/ZszOAAAwDoEHAAAYB0CDgAAsA4BBwAAWIeAAwAArEPAAQAA1iHgAAAA6xBwAACAdQg4AADAOgQcAABgHQIOAACwDgEHAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALBOrQQcn8+n9u3by+FwaNu2bdX2HTNmjBwOR8B0xx13VFjfhAkT5HK5lJSUpIEDB+rw4cNhrAAAAESTWgk4U6ZMUVpaWtD9+/XrpyNHjvinFStWBMyfOHGilixZogULFui9997TqVOnNGDAAJ0/f/5qDx0AAESheuHewMqVK7Vq1SotWrRIK1euDGqZ+Ph4paSkVDrP4/Ho5Zdf1vz589WrVy9J0t///ne53W69++676tu371UbOwAAiE5hPYNz9OhRjRs3TvPnz1diYmLQy61fv15NmzZV69atNW7cOJWUlPjn5efnq7y8XH369PG3paWlqW3bttq4cWOl6/P5fPJ6vQETAACwV9gCjjFGY8aM0UMPPaROnToFvdz3v/995eXlae3atZozZ442b96s733ve/L5fJKk4uJixcXFqWHDhgHLNWvWTMXFxZWuMzc3V06n0z+53e4rLwwAANR5IQecnJycCjcBXz5t2bJF8+bNk9fr1fTp00Na/4gRI9S/f3+1bdtWd999t1auXKldu3Zp+fLl1S5njJHD4ah03vTp0+XxePzToUOHQhoTAACILiHfgzN+/HiNHDmy2j4ZGRmaOXOmNm3apPj4+IB5nTp10ujRo/Xaa68Ftb3U1FS1aNFCu3fvliSlpKSorKxMJ0+eDDiLU1JSoq5du1a6jvj4+ArjAAAA9go54LhcLrlcrhr7Pfvss5o5c6b/eVFRkfr27auFCxeqc+fOQW/v+PHjOnTokFJTUyVJWVlZio2N1erVqzV8+HBJ0pEjR7Rjxw499dRTIVYDRIeMaf85g1k4u38ER1L3sa9Cw/6CrcL2Kar09PSA59dff70kqVWrVmrevLm/PTMzU7m5ubr33nt16tQp5eTkaMiQIUpNTVVhYaEee+wxuVwu3XvvvZIkp9OpsWPHatKkSWrcuLEaNWqkyZMnq127dv5PVQEAgGtb2D8mXpOCggJ5PB5JUkxMjLZv366//e1v+uKLL5SamqoePXpo4cKFatCggX+ZZ555RvXq1dPw4cN19uxZ9ezZU6+++qpiYmIiVQYAAKhDHMYYE+lB1Dav1yun0ymPx6Pk5ORIDwcAAAQhlPdvfosKAABYh4ADAACsQ8ABAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHXqRXoAkXDxB9S9Xm+ERwIAAIJ18X374vt4da7JgFNaWipJcrvdER4JAAAIVWlpqZxOZ7V9HCaYGGSZCxcuqKioSA0aNJDD4Qjbdrxer9xutw4dOqTk5OSwbaeuuNbqla69mq+1eqVrr2bqtV8012yMUWlpqdLS0nTdddXfZXNNnsG57rrr1Lx581rbXnJyctT9EX0d11q90rVX87VWr3Tt1Uy99ovWmms6c3MRNxkDAADrEHAAAIB1CDhhFB8fr+zsbMXHx0d6KLXiWqtXuvZqvtbqla69mqnXftdKzdfkTcYAAMBunMEBAADWIeAAAADrEHAAAIB1CDgAAMA6BBwAAGAdAo6kP/3pT7r11lv93+rYpUsXrVy50j9/zJgxcjgcAdMdd9zhn3/ixAlNmDBBN998sxITE5Wenq5f/OIX8ng8NW77hRdeUMuWLZWQkKCsrCz9+9//DphvjFFOTo7S0tJUv359de/eXTt37ozKenNzc/Xtb39bDRo0UNOmTTVo0CAVFBQE9Klp29FWc05OToX1pqSkBPSx6TXOyMiosF6Hw6FHHnkk6G1Hol5J+tnPfqZWrVqpfv36atKkie655x59+umnNW47EsdwJGuO1HEcqXqj9Ri+0nojdQyHhYFZtmyZWb58uSkoKDAFBQXmscceM7GxsWbHjh3GGGN+/OMfm379+pkjR474p+PHj/uX3759uxk8eLBZtmyZ2bNnj1mzZo256aabzJAhQ6rd7oIFC0xsbKz585//bD755BPz6KOPmqSkJHPgwAF/n9mzZ5sGDRqYRYsWme3bt5sRI0aY1NRU4/V6o67evn37mldeecXs2LHDbNu2zfTv39+kp6ebU6dO+fvUtO1oqzk7O9u0adMmYL0lJSUBfWx6jUtKSgLWuXr1aiPJrFu3zt8nHK/x163XGGNefPFFs2HDBrN//36Tn59v7r77buN2u82XX35Z5XYjdQxHsuZIHceRqjdaj+ErrTdSx3A4EHCq0LBhQ/OXv/zFGPPVi3nPPfeEtPwbb7xh4uLiTHl5eZV9br/9dvPQQw8FtGVmZppp06YZY4y5cOGCSUlJMbNnz/bPP3funHE6nea//uu/QhpPTWqj3suVlJQYSWbDhg3+tivZ9pWqjZqzs7PNbbfdVuV821/jRx991LRq1cpcuHDB31Zbr/HXrfejjz4yksyePXuq7FOXjmFjaqfmy0XyOK6Nem06hq/k9Y3kMfx1cYnqMufPn9eCBQt0+vRpdenSxd++fv16NW3aVK1bt9a4ceNUUlJS7Xo8Ho+Sk5NVr17lv2daVlam/Px89enTJ6C9T58+2rhxoyRp//79Ki4uDugTHx+vbt26+ft8XbVVb1XLSFKjRo0C2kPddqhqu+bdu3crLS1NLVu21MiRI7Vv3z7/PJtf47KyMv3973/XAw88IIfDETAvnK/x1aj39OnTeuWVV9SyZUu53e5K+9SVY1iqvZorE4njuLbrteEYvpLXN1LH8FUT6YRVV3z88ccmKSnJxMTEGKfTaZYvX+6ft2DBAvP222+b7du3m2XLlpnbbrvNtGnTxpw7d67SdR07dsykp6ebX//611Vu77PPPjOSzPvvvx/Q/uSTT5rWrVsbY4x5//33jSTz2WefBfQZN26c6dOnz5WWaoyp/Xovd+HCBXP33Xeb73znOwHtoW47FJGoecWKFeaf//yn+fjjj83q1atNt27dTLNmzcyxY8eMMXa/xgsXLjQxMTEVagvXa3w16n3++edNUlKSkWQyMzOr/Z9upI9hY2q/5svV9nEciXqj/Rj+Oq9vbR/DVxsB5//x+Xxm9+7dZvPmzWbatGnG5XKZnTt3Vtq3qKjIxMbGmkWLFlWY5/F4TOfOnU2/fv1MWVlZldu7+I/jxo0bA9pnzpxpbr75ZmPMfw6coqKigD4PPvig6du3b6glBqjtei/38MMPmxYtWphDhw5V26+6bYcq0jUbY8ypU6dMs2bNzJw5c4wxdr/Gffr0MQMGDKix39V6ja9GvV988YXZtWuX2bBhg7n77rtNx44dzdmzZytdR6SPYWNqv+bL1fZxHOl6jYm+Y/jr1Fvbx/DVRsCpQs+ePc1Pf/rTKuffeOONAddcjTHG6/WaLl26mJ49e9b4B+Tz+UxMTIxZvHhxQPsvfvELc9dddxljjNm7d6+RZLZu3RrQZ+DAgeb+++8PpZwahbveS40fP940b97c7Nu3L6j+lW37aqjNmi/Vq1cv/30btr7GhYWF5rrrrjNvvfVWUP3D8RpfSb2X8vl8JjEx0bz++utVzq9Lx7Ax4a/5UnXhOK7Nei8VTcfwpUKpty4cw18X9+BUwRgjn89X6bzjx4/r0KFDSk1N9bd5vV716dNHcXFxWrZsmRISEqpdf1xcnLKysrR69eqA9tWrV6tr166SpJYtWyolJSWgT1lZmTZs2ODvc7WEu96L2xg/frwWL16stWvXqmXLljUuU9m2r5baqPlyPp9P//d//+dfr22v8UWvvPKKmjZtqv79+9fYN1yvcaj1hrqOunYM1zTeq1Hzxfl15TiujXovF03HcKjruFRdOIa/tkikqrpm+vTp5l//+pfZv3+/+fjjj81jjz1mrrvuOrNq1SpTWlpqJk2aZDZu3Gj2799v1q1bZ7p06WK+8Y1v+D8C6PV6TefOnU27du3Mnj17Aj46d+nH8b73ve+ZefPm+Z9f/Ijpyy+/bD755BMzceJEk5SUZAoLC/19Zs+ebZxOp1m8eLHZvn27+eEPf/i1P34YqXp//vOfG6fTadavXx+wzJkzZ4wxJqhtR1vNkyZNMuvXrzf79u0zmzZtMgMGDDANGjSw9jU2xpjz58+b9PR0M3Xq1ArjCtdr/HXr3bt3r5k1a5bZsmWLOXDggNm4caO55557TKNGjczRo0errDdSx3Aka47UcRypeqP1GL7Seo2JzDEcDgQcY8wDDzxgWrRoYeLi4kyTJk1Mz549zapVq4wxxpw5c8b06dPHNGnSxMTGxpr09HTz4x//2Bw8eNC//Lp164ykSqf9+/f7+7Vo0cJkZ2cHbPv555/3b7tjx44BH7U05qub+LKzs01KSoqJj483d911l9m+fXtU1lvVMq+88krQ2462mi9+H0ZsbKxJS0szgwcPrnAN3abX2Bhj3nnnHSPJFBQUVBhXuF7jr1vvZ599Zr7//e+bpk2bmtjYWNO8eXMzatQo8+mnnwZsp64cw5GsOVLHcaTqjdZj+Ov8TUfiGA4HhzHGhPMMEQAAQG3jHhwAAGAdAg4AALAOAQcAAFiHgAMAAKxDwAEAANYh4AAAAOsQcAAAgHUIOAAAwDoEHAAAYB0CDgAAsA4BBwAAWOf/B3vacfVHG+3TAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%time\n", + "# Visualize all of the unique dates, one at a time.\n", + "for dt in unique_dates:\n", + " fig = plt.figure()\n", + " tmpdf = df[df[\"ut_datetime\"].dt.date == parser.parse(\"2019-08-28\").date()]\n", + " plt.scatter(*zip(*tmpdf[\"center_coord\"]), s=1, alpha=0.5)\n", + " plt.title(f\"{dt}\")\n", + "del tmpdf" + ] + }, + { + "cell_type": "markdown", + "id": "337ceedc", + "metadata": {}, + "source": [ + "# Recap and Master Function\n", + "Here is a master function that takes a repo_path and returns \\\n", + "(1) a Pandas dataframe with needed info, and\\\n", + "(2) a dictionary with the images in discrete piles (sets)." + ] + }, + { + "cell_type": "code", + "execution_count": 634, + "id": "fccdee6b", + "metadata": {}, + "outputs": [], + "source": [ + "def retrieve_image_sets(\n", + " repo_path,\n", + " basedir=\"default\",\n", + " desired_datasetTypes=[\"deepDiff_differenceExp\"],\n", + " overwrite=False,\n", + " overlap_uncertainty_radius_arcsec=30,\n", + "):\n", + " \"\"\"2/6/2024 COC\"\"\"\n", + " import lsst\n", + " import lsst.daf.butler as dafButler\n", + " import os\n", + " import time\n", + " from matplotlib import pyplot as plt\n", + " import progressbar\n", + " from concurrent.futures import ProcessPoolExecutor, as_completed\n", + " from astropy.time import Time # for converting Butler visitInfo.date (TAI) to UTC strings\n", + " from astropy import units as u\n", + " import pandas as pd\n", + " import pickle\n", + " from dateutil import parser\n", + "\n", + " if basedir == \"default\":\n", + " basedir = f'{os.environ[\"HOME\"]}/kbmod_tmp'\n", + " print(f'Changing \"default\" basedir to {basedir} now.')\n", + "\n", + " os.makedirs(basedir, exist_ok=True)\n", + "\n", + " # repo_path = f\"/epyc/users/smotherh/DEEP/PointingGroups/butler-repo\"\n", + " butler = dafButler.Butler(repo_path)\n", + "\n", + " all_collection_names = get_collection_names(butler=butler, basedir=basedir, verbose=True, export=True)\n", + " desired_collections = get_desired_collections(all_collections_list=all_collection_names)\n", + " # datasetTypes = getDatasetTypeStats(butler=butler, overwrite=False) # not used 2/6/2024 COC\n", + " # desired_datasetTypes = [\"deepDiff_differenceExp\"]\n", + " df, example_vdr_ref = get_vdr_data(\n", + " butler=butler, desired_collections=desired_collections, desired_datasetTypes=desired_datasetTypes\n", + " )\n", + " desired_instruments = getInstruments(butler=butler, vdr_ids=df[\"data_id\"])\n", + " df[\"uri\"] = getURIs(\n", + " butler=butler,\n", + " dataIds=df[\"data_id\"],\n", + " repo_path=repo_path,\n", + " desired_datasetTypes=desired_datasetTypes,\n", + " desired_collections=desired_collections,\n", + " overwrite=overwrite,\n", + " )\n", + "\n", + " df[\"ut\"] = getTimestamps(dataIds=df[\"data_id\"], overwrite=overwrite)\n", + " df[\"ut_datetime\"] = pd.to_datetime(df[\"ut\"])\n", + " overlapping_sets = find_overlapping_coords(\n", + " df=df, uncertainty_radius=overlap_uncertainty_radius_arcsec, overwrite=overwrite\n", + " )\n", + " return df, overlapping_sets" + ] + }, + { + "cell_type": "code", + "execution_count": 638, + "id": "cf221f77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Changing \"default\" basedir to /astro/users/coc123/kbmod_tmp now.\n", + "Found 1292 collections in the Butler. Wrote to \"/astro/users/coc123/kbmod_tmp/all_collection_names.lst\".\n", + "Found DECam. Adding to \"desired_instruments\" now.\n", + "WARNING: we are not iterating over all rows to find instruments, just taking the first one.\n", + "Recycled 47383 paths from /astro/users/coc123/kbmod_tmp/uri_cache.lst as overwrite was False.\n", + "Overwrite is False, so we will read the timestamps from file now...\n", + "Recycled 47383 from /astro/users/coc123/kbmod_tmp/vdr_timestamps.lst.\n", + "Recycling /astro/users/coc123/kbmod_tmp/overlapping_sets.pickle as overwrite=False.\n", + "CPU times: user 3.91 s, sys: 413 ms, total: 4.32 s\n", + "Wall time: 5.54 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# TIMING NOTE: this requires about 7 seconds to run ***with everything already cached***.\n", + "df1, overlapping_sets1 = retrieve_image_sets(\n", + " repo_path=f\"/epyc/users/smotherh/DEEP/PointingGroups/butler-repo\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3df17bc4", + "metadata": {}, + "source": [ + "# Next Steps\n", + "\n", + "In no particular order:\n", + "\n", + "1. User-specified (RA, Dec) pair.\n", + "2. Heat map / histogrammed results.\n", + "3. Sky patches approach.\n", + "4. Reflex correction." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "LSST w_2022_06", + "language": "python", + "name": "opt_lsst_w_2022_06" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/region_search/coc/RegionSearchTesting.ipynb b/notebooks/region_search/coc/RegionSearchTesting.ipynb new file mode 100644 index 000000000..4fd1681be --- /dev/null +++ b/notebooks/region_search/coc/RegionSearchTesting.ipynb @@ -0,0 +1,973 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "ab967f8a-b2eb-4af9-9b28-3552c80f2801", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: must do this before launching Jupyter Notebook!\n", + "# (1) source setupLSST.zsh\n", + "# (2) run the demodata/pipeline_check setup -r .\n", + "# (3) run the setup -r . for rc2_subset" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "71fb6969-878e-48d3-901b-47468694bfbb", + "metadata": {}, + "outputs": [], + "source": [ + "from lsst.daf.butler import Butler\n", + "import os\n", + "import time\n", + "from lsst import sphgeom" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2c3dcb5d-8bf6-4073-b332-70342da9a847", + "metadata": {}, + "outputs": [], + "source": [ + "repo_path = os.path.join(os.environ[\"RC2_SUBSET_DIR\"], \"SMALL_HSC\")\n", + "butler = Butler(repo_path)\n", + "registry = butler.registry\n", + "# collection = f\"u/{os.environ['USER']}/single_frame\"\n", + "# d = butler.registry.queryDatasets('calexp', physical_filter='HSC-R', collections=collection, instrument='HSC')\n", + "# calexp = butler.get('calexp', visit=23718, detector=41, collections=collection, instrument='HSC')\n", + "# calexp.visitInfo.boresightRaDec" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2f94e6a3-d7b4-45b0-9ba5-6a784559f604", + "metadata": {}, + "outputs": [], + "source": [ + "collection = f\"u/{os.environ['USER']}/single_frame\"\n", + "# vdr = butler.registry.queryDimensionRecords(\"visit_detector_region\")\n", + "# vdr = butler.registry.queryDimensionRecords(\"visit_detector_region\", collections=collection)\n", + "vdr = butler.registry.queryDimensionRecords(\n", + " \"visit_detector_region\",\n", + " datasets=\"calexp\",\n", + " collections=collection,\n", + " instrument=\"HSC\",\n", + " physical_filter=\"HSC-R\",\n", + ") # more specific, just the calex stuff" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3c65104b-fd14-4344-9b62-7c4eea2485d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First VDR record:\n", + "{instrument: 'HSC', detector: 41, visit: 1204}\n", + "ConvexPolygon([UnitVector3d(-0.8685370178854345, 0.49439163180184836, 0.03493369386397754), UnitVector3d(-0.8684746456459038, 0.4943508977169074, 0.036999726981556666), UnitVector3d(-0.8665881263897214, 0.4976505625849847, 0.036999145384733166), UnitVector3d(-0.8666527966506773, 0.4976875474603122, 0.03492900171060283)])\n" + ] + } + ], + "source": [ + "print(f\"First VDR record:\")\n", + "for i in vdr:\n", + " first_vdr_rec = i\n", + " break\n", + "print(first_vdr_rec.dataId) # {instrument: 'HSC', detector: 41, visit: 1204}\n", + "print(\n", + " first_vdr_rec.region\n", + ") # ConvexPolygon([UnitVector3d(-0.8685370178854345, 0.49439163180184836, 0.03493369386397754), UnitVector3d(-0.8684746456459038, 0.4943508977169074, 0.036999726981556666), UnitVector3d(-0.8665881263897214, 0.4976505625849847, 0.036999145384733166), UnitVector3d(-0.8666527966506773, 0.4976875474603122, 0.03492900171060283)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83e23099-2e0f-4e35-81c3-c64b05e77a24", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9ce47103-6350-47b0-a91d-369180706388", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Last VDR record:\n", + "{instrument: 'HSC', detector: 58, visit: 23718}\n", + "ConvexPolygon([UnitVector3d(-0.8652234119418918, 0.49972867464192744, 0.04073940559524716), UnitVector3d(-0.8652923227482628, 0.4997734451226977, 0.03867427753825506), UnitVector3d(-0.8671859739320048, 0.49648028531881305, 0.03867574052749735), UnitVector3d(-0.8671146883772648, 0.4964392228880602, 0.04074573816606947)])\n" + ] + } + ], + "source": [ + "print(f\"Last VDR record:\")\n", + "for i in vdr:\n", + " last_vdr_rec = i\n", + "print(last_vdr_rec.dataId) # {instrument: 'HSC', detector: 58, visit: 23718}\n", + "print(\n", + " last_vdr_rec.region\n", + ") # ConvexPolygon([UnitVector3d(-0.8652234119418918, 0.49972867464192744, 0.04073940559524716), UnitVector3d(-0.8652923227482628, 0.4997734451226977, 0.03867427753825506), UnitVector3d(-0.8671859739320048, 0.49648028531881305, 0.03867574052749735), UnitVector3d(-0.8671146883772648, 0.4964392228880602, 0.04074573816606947)])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5894c666-be1b-474e-86b4-0f16009b0415", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "first_vdr_rec.region.contains(first_vdr_rec.region) = True\n", + "first_vdr_rec.region.contains(last_vdr_rec.region) = False\n" + ] + } + ], + "source": [ + "# silly test first\n", + "print(\n", + " f\"first_vdr_rec.region.contains(first_vdr_rec.region) = {first_vdr_rec.region.contains(first_vdr_rec.region)}\"\n", + ") # should be True!!\n", + "print(\n", + " f\"first_vdr_rec.region.contains(last_vdr_rec.region) = {first_vdr_rec.region.contains(last_vdr_rec.region)}\"\n", + ") # False" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "060b973b-5591-42ba-ae65-5bf1e4c4f825", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Checking matches of first_vdr.region against all records region for matchces...\n", + "There were 1 matches:\n", + "{instrument: 'HSC', detector: 41, visit: 1204}\n" + ] + } + ], + "source": [ + "print(f\"Checking matches of first_vdr.region against all records region for matchces...\")\n", + "matches = []\n", + "for vdr_rec in vdr:\n", + " if vdr_rec.region.contains(first_vdr_rec.region):\n", + " matches.append(vdr_rec.dataId)\n", + "print(f\"There were {len(matches)} matches:\")\n", + "for i in matches:\n", + " print(i)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "507ce759-1e30-4be7-ac44-fbc434d8e753", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: find all corners, and double-check the overlap 11/16/2023 COC\n", + "# HSC/calib/gen2/20180117" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "09020453-1b37-4538-9aaf-a725bc979858", + "metadata": {}, + "outputs": [], + "source": [ + "# datasetRefs = registry.queryDatasets(datasetType='calexp', collections='HSC/calib/gen2/20180117')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "1e34d83b-eb07-4e2d-961c-8e9e47355ab2", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "calexp@{instrument: 'HSC', detector: 41, visit: 1204, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=4d673e07-c749-4d80-9dcf-b39615a802a4)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 1206, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=9f3ea177-6daa-4e00-9bcb-8b0616c9b855)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 1214, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=b3965daf-39b4-43f1-a1ac-49f6dd399c6f)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 1220, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=b65a71c7-6c0f-476d-9c52-4c93e4d8b12a)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 23694, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=bef09c5b-3fb5-49c2-80d1-327bc1120fd2)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 23704, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=f061d55c-64a5-4d74-8ce2-3eb822c6ffbd)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 23706, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=4a509cbe-f46a-46e6-85bc-f3274dd59524)\n", + "calexp@{instrument: 'HSC', detector: 41, visit: 23718, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=91b1f763-f776-42f4-a474-5ff3492acc0b)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 1204, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=87055aa3-9e11-4d64-b2ed-e0233c24c403)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 1206, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=5b87e4f6-5a95-4a3b-af93-22c4b8c0e6e0)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 1214, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=945468ce-3d0e-4030-b823-dd1d5a532876)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 1220, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=9d42d8cd-a07b-4b1e-b0d8-79666a1464b9)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 23694, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=df6a6120-72d3-43b0-b392-a83fdaf69398)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 23704, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=096f41bd-4216-4424-9681-104fde395d2c)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 23706, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=aa208f29-6d2f-4d78-b940-3250c7c50fb7)\n", + "calexp@{instrument: 'HSC', detector: 42, visit: 23718, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=733f733e-6a8d-4125-89fd-45624e8c71bc)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 1204, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=07536c48-b4c8-4c32-961f-b6261015d417)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 1206, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=de2250b1-920a-47ff-bf17-41ee4bcdbb2c)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 1214, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=7f2fc43c-ae5c-434e-b738-d58677e028a1)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 1220, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=9140c30b-654e-4f46-9cfe-56ce57877d5d)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 23694, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=022f049f-7369-4514-9372-dbfd2fa764ef)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 23704, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=4aa4a48b-35d9-4979-a33a-7355c032942e)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 23706, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=56320e78-6121-4047-913a-6fe064cd16ab)\n", + "calexp@{instrument: 'HSC', detector: 47, visit: 23718, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=596b32cb-5e35-4257-b9c7-2b45fc36630f)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 1204, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=53963b41-b927-461d-bf0e-5986a0471ecb)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 1206, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=f66b0a47-4135-4fd7-a317-da8a8d1d6170)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 1214, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=e8774d73-3efa-4076-bac8-b84a59f6605b)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 1220, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=ae7dde19-9233-42ef-8f91-e6428511d660)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 23694, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=6b05023d-86cf-4e77-a9a8-7d8591fc195c)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 23704, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=199133f5-8823-4458-a20a-0dbfaa2bbc3e)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 23706, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=05e57fc0-4ef1-46f5-bb62-38a08b1c4201)\n", + "calexp@{instrument: 'HSC', detector: 49, visit: 23718, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=b24c2672-2512-4e65-9740-74022802f82c)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 1204, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=81e47e75-83c4-4341-9a07-c35a6cb38a9a)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 1206, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=2c3d592f-9bba-48e3-abac-80fc92a7283a)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 1214, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=4978031c-4bde-4f2f-ba4c-8069e4afac53)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 1220, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=064a52d4-68b1-4d12-b3e8-65fa945cb518)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 23694, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=a3e09cb0-3304-4a6b-9e5a-251b96c857c5)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 23704, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=b7bca4ff-a559-4f7f-9691-3b86368dbf28)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 23706, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=aa1ff092-2bd7-4d62-873d-5e5f82717b22)\n", + "calexp@{instrument: 'HSC', detector: 50, visit: 23718, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=c7b5bc6d-0991-494b-921b-76a0e9b7f2d3)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 1204, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=151a35cb-b254-4474-84d6-5c3004fe56c9)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 1206, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=1a827f6f-c666-4edd-88a5-0002524485b7)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 1214, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=51f76937-7f49-4157-aae7-fed4281e46a8)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 1220, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=1015b0f9-3470-4ab7-9dfa-729a9888db29)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 23694, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=f4256be0-f277-4a16-b656-5294017cd9a4)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 23704, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=010d7808-855d-4f6b-80ec-49bc460d8a6f)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 23706, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=b5fcbe18-caf0-4e6b-810d-b9c79a7cba16)\n", + "calexp@{instrument: 'HSC', detector: 58, visit: 23718, ...} [sc=ExposureF] (run=u/colinchandler/single_frame/20231110T234552Z id=784f36f1-98f3-4ecd-b35e-7bd04cecbdb7)\n" + ] + } + ], + "source": [ + "# using collection = f\"u/{os.environ['USER']}/single_frame\"\n", + "for ref in butler.registry.queryDatasets(\n", + " \"calexp\", physical_filter=\"HSC-R\", collections=collection, instrument=\"HSC\"\n", + "):\n", + " print(ref)\n", + "\n", + "for ref in butler.registry.queryDatasets(\n", + " \"calexp\", physical_filter=\"HSC-R\", collections=collection, instrument=\"HSC\"\n", + "):\n", + " first_dsr = ref\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "4351a801-39e1-405b-8d5c-add0da0933be", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO do an example that spans filters and demonstrates .contains(), and also the one we have below with .intersects()." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "eddd8177-8447-4f6c-92aa-f793351b3b2a", + "metadata": {}, + "outputs": [], + "source": [ + "# all_ds = [ ds for ds in registry.queryDatasets('calexp',collections=collection, instrument='HSC, physical_filter='HSC-R')]\n", + "# lol now it's the same as above. Result count this way is 48. Removing physical_filter and instrument results in 240.\n", + "# NOTE: there are 48 in the directory structure, as so from bash:\n", + "# find /Users/colinchandler/lsst_stack_w_2023_44/rc2_subset/SMALL_HSC/u/colinchandler/single_frame/20231110T234552Z/calexp -name \\*HSC-R\\*.fits" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "8dc71a26-ca04-476d-8580-7862f8d4f79d", + "metadata": {}, + "outputs": [], + "source": [ + "dsq = registry.queryDatasets(\"calexp\", collections=collection, instrument=\"HSC\", physical_filter=\"HSC-R\")\n", + "dsrecords = [i for i in dsq] # 12/20/2023 COC\n", + "dataIds = [i.dataId for i in dsq]" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "550bfd1a-c129-4e8e-812b-18dc5a5ff543", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{band: 'r', instrument: 'HSC', detector: 41, physical_filter: 'HSC-R', visit_system: 0, visit: 1204}" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataIds[0].full" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "bcf75eb3-0fd1-4327-a94d-aab20ac7bf98", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "DatasetRef(DatasetType('calexp', {band, instrument, detector, physical_filter, visit_system, visit}, ExposureF), {instrument: 'HSC', detector: 41, visit: 1204, ...}, run='u/colinchandler/single_frame/20231110T234552Z', id=4d673e07-c749-4d80-9dcf-b39615a802a4)" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dsrecords[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "54f4ea99-6799-45b8-9dc5-3f12e1994fd6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "UUID('4d673e07-c749-4d80-9dcf-b39615a802a4')" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# here we get unique IDs back from the butler 12/20/2023 COC\n", + "testid = dsrecords[0].id\n", + "testid" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "b2258648-8939-4607-a408-331bc731f618", + "metadata": {}, + "outputs": [ + { + "ename": "UserExpressionSyntaxError", + "evalue": "Failed to parse user expression 'id=4d673e07-c749-4d80-9dcf-b39615a802a4'.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mParseError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/_predicate.py:136\u001b[0m, in \u001b[0;36mmake_string_expression_predicate\u001b[0;34m(string, dimensions, column_types, bind, data_id, defaults, dataset_type_name, allow_orphans)\u001b[0m\n\u001b[1;32m 135\u001b[0m parser \u001b[38;5;241m=\u001b[39m ParserYacc()\n\u001b[0;32m--> 136\u001b[0m tree \u001b[38;5;241m=\u001b[39m \u001b[43mparser\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparse\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstring\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 137\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/parser/parserYacc.py:270\u001b[0m, in \u001b[0;36mParserYacc.parse\u001b[0;34m(self, input, lexer, debug, tracking)\u001b[0m\n\u001b[1;32m 269\u001b[0m lexer \u001b[38;5;241m=\u001b[39m ParserLex\u001b[38;5;241m.\u001b[39mmake_lexer()\n\u001b[0;32m--> 270\u001b[0m tree \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparser\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparse\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlexer\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlexer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdebug\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdebug\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtracking\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtracking\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 271\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m tree\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/parser/ply/yacc.py:349\u001b[0m, in \u001b[0;36mLRParser.parse\u001b[0;34m(self, input, lexer, debug, tracking, tokenfunc)\u001b[0m\n\u001b[1;32m 348\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 349\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparseopt_notrack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlexer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdebug\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtracking\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtokenfunc\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/parser/ply/yacc.py:1214\u001b[0m, in \u001b[0;36mLRParser.parseopt_notrack\u001b[0;34m(self, input, lexer, debug, tracking, tokenfunc)\u001b[0m\n\u001b[1;32m 1213\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstate \u001b[38;5;241m=\u001b[39m state\n\u001b[0;32m-> 1214\u001b[0m tok \u001b[38;5;241m=\u001b[39m \u001b[43mcall_errorfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43merrorfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43merrtoken\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1215\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39merrorok:\n\u001b[1;32m 1216\u001b[0m \u001b[38;5;66;03m# User must have done some kind of panic\u001b[39;00m\n\u001b[1;32m 1217\u001b[0m \u001b[38;5;66;03m# mode recovery on their own. The\u001b[39;00m\n\u001b[1;32m 1218\u001b[0m \u001b[38;5;66;03m# returned token is the next lookahead\u001b[39;00m\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/parser/ply/yacc.py:202\u001b[0m, in \u001b[0;36mcall_errorfunc\u001b[0;34m(errorfunc, token, parser)\u001b[0m\n\u001b[1;32m 201\u001b[0m _restart \u001b[38;5;241m=\u001b[39m parser\u001b[38;5;241m.\u001b[39mrestart\n\u001b[0;32m--> 202\u001b[0m r \u001b[38;5;241m=\u001b[39m \u001b[43merrorfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtoken\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 203\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/parser/parserYacc.py:450\u001b[0m, in \u001b[0;36mParserYacc.p_error\u001b[0;34m(self, p)\u001b[0m\n\u001b[1;32m 449\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 450\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ParseError(p\u001b[38;5;241m.\u001b[39mlexer\u001b[38;5;241m.\u001b[39mlexdata, p\u001b[38;5;241m.\u001b[39mvalue, p\u001b[38;5;241m.\u001b[39mlexpos, p\u001b[38;5;241m.\u001b[39mlineno)\n", + "\u001b[0;31mParseError\u001b[0m: Syntax error at or near 'd673e07' (line: 1, pos: 5)", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mUserExpressionSyntaxError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[63], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m bulteridtest \u001b[38;5;241m=\u001b[39m \u001b[43mregistry\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mqueryDatasets\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mcalexp\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcollections\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcollection\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwhere\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mid=\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mtestid\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/_registry_shim.py:312\u001b[0m, in \u001b[0;36mRegistryShim.queryDatasets\u001b[0;34m(self, datasetType, collections, dimensions, dataId, where, findFirst, components, bind, check, **kwargs)\u001b[0m\n\u001b[1;32m 297\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mqueryDatasets\u001b[39m(\n\u001b[1;32m 298\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 299\u001b[0m datasetType: Any,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 310\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m DatasetQueryResults:\n\u001b[1;32m 311\u001b[0m \u001b[38;5;66;03m# Docstring inherited from a base class.\u001b[39;00m\n\u001b[0;32m--> 312\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_registry\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mqueryDatasets\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 313\u001b[0m \u001b[43m \u001b[49m\u001b[43mdatasetType\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 314\u001b[0m \u001b[43m \u001b[49m\u001b[43mcollections\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcollections\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 315\u001b[0m \u001b[43m \u001b[49m\u001b[43mdimensions\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdimensions\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 316\u001b[0m \u001b[43m \u001b[49m\u001b[43mdataId\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdataId\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 317\u001b[0m \u001b[43m \u001b[49m\u001b[43mwhere\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mwhere\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 318\u001b[0m \u001b[43m \u001b[49m\u001b[43mfindFirst\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfindFirst\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 319\u001b[0m \u001b[43m \u001b[49m\u001b[43mcomponents\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcomponents\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 320\u001b[0m \u001b[43m \u001b[49m\u001b[43mbind\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbind\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 321\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheck\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcheck\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 322\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 323\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/sql_registry.py:2097\u001b[0m, in \u001b[0;36mSqlRegistry.queryDatasets\u001b[0;34m(self, datasetType, collections, dimensions, dataId, where, findFirst, components, bind, check, **kwargs)\u001b[0m\n\u001b[1;32m 2094\u001b[0m dimension_names\u001b[38;5;241m.\u001b[39mupdate(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdimensions\u001b[38;5;241m.\u001b[39mextract(dimensions)\u001b[38;5;241m.\u001b[39mnames)\n\u001b[1;32m 2095\u001b[0m \u001b[38;5;66;03m# Construct the summary structure needed to construct a\u001b[39;00m\n\u001b[1;32m 2096\u001b[0m \u001b[38;5;66;03m# QueryBuilder.\u001b[39;00m\n\u001b[0;32m-> 2097\u001b[0m summary \u001b[38;5;241m=\u001b[39m \u001b[43mqueries\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mQuerySummary\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2098\u001b[0m \u001b[43m \u001b[49m\u001b[43mrequested\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mDimensionGraph\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdimensions\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnames\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdimension_names\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2099\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumn_types\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_managers\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcolumn_types\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2100\u001b[0m \u001b[43m \u001b[49m\u001b[43mdata_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdata_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2101\u001b[0m \u001b[43m \u001b[49m\u001b[43mexpression\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mwhere\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2102\u001b[0m \u001b[43m \u001b[49m\u001b[43mbind\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbind\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2103\u001b[0m \u001b[43m \u001b[49m\u001b[43mdefaults\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdefaults\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdataId\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2104\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheck\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcheck\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2105\u001b[0m \u001b[43m \u001b[49m\u001b[43mdatasets\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mparent_dataset_type\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2106\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2107\u001b[0m builder \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_makeQueryBuilder(summary)\n\u001b[1;32m 2108\u001b[0m \u001b[38;5;66;03m# Add the dataset subquery to the query, telling the QueryBuilder\u001b[39;00m\n\u001b[1;32m 2109\u001b[0m \u001b[38;5;66;03m# to include the rank of the selected collection in the results\u001b[39;00m\n\u001b[1;32m 2110\u001b[0m \u001b[38;5;66;03m# only if we need to findFirst. Note that if any of the\u001b[39;00m\n\u001b[1;32m 2111\u001b[0m \u001b[38;5;66;03m# collections are actually wildcard expressions, and\u001b[39;00m\n\u001b[1;32m 2112\u001b[0m \u001b[38;5;66;03m# findFirst=True, this will raise TypeError for us.\u001b[39;00m\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/_structs.py:384\u001b[0m, in \u001b[0;36mQuerySummary.__init__\u001b[0;34m(self, requested, column_types, data_id, expression, region, bind, defaults, datasets, order_by, limit, check)\u001b[0m\n\u001b[1;32m 382\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 383\u001b[0m dataset_type_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m--> 384\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mwhere \u001b[38;5;241m=\u001b[39m \u001b[43mQueryWhereClause\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcombine\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 385\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequested\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 386\u001b[0m \u001b[43m \u001b[49m\u001b[43mexpression\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexpression\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 387\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumn_types\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumn_types\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 388\u001b[0m \u001b[43m \u001b[49m\u001b[43mbind\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbind\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 389\u001b[0m \u001b[43m \u001b[49m\u001b[43mdata_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdata_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 390\u001b[0m \u001b[43m \u001b[49m\u001b[43mregion\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mregion\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 391\u001b[0m \u001b[43m \u001b[49m\u001b[43mdefaults\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdefaults\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 392\u001b[0m \u001b[43m \u001b[49m\u001b[43mdataset_type_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdataset_type_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 393\u001b[0m \u001b[43m \u001b[49m\u001b[43mallow_orphans\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcheck\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 394\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 395\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39morder_by \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01mif\u001b[39;00m order_by \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01melse\u001b[39;00m OrderByClause\u001b[38;5;241m.\u001b[39mparse_general(order_by, requested)\n\u001b[1;32m 396\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlimit \u001b[38;5;241m=\u001b[39m limit\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/_structs.py:114\u001b[0m, in \u001b[0;36mQueryWhereClause.combine\u001b[0;34m(cls, dimensions, expression, column_types, bind, data_id, region, defaults, dataset_type_name, allow_orphans)\u001b[0m\n\u001b[1;32m 112\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m defaults \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 113\u001b[0m defaults \u001b[38;5;241m=\u001b[39m DataCoordinate\u001b[38;5;241m.\u001b[39mmakeEmpty(dimensions\u001b[38;5;241m.\u001b[39muniverse)\n\u001b[0;32m--> 114\u001b[0m expression_predicate, governor_constraints \u001b[38;5;241m=\u001b[39m \u001b[43mmake_string_expression_predicate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 115\u001b[0m \u001b[43m \u001b[49m\u001b[43mexpression\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 116\u001b[0m \u001b[43m \u001b[49m\u001b[43mdimensions\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 117\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumn_types\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumn_types\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 118\u001b[0m \u001b[43m \u001b[49m\u001b[43mbind\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbind\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 119\u001b[0m \u001b[43m \u001b[49m\u001b[43mdata_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdata_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 120\u001b[0m \u001b[43m \u001b[49m\u001b[43mdefaults\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdefaults\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 121\u001b[0m \u001b[43m \u001b[49m\u001b[43mdataset_type_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdataset_type_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 122\u001b[0m \u001b[43m \u001b[49m\u001b[43mallow_orphans\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mallow_orphans\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 123\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 124\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m QueryWhereClause(\n\u001b[1;32m 125\u001b[0m expression_predicate,\n\u001b[1;32m 126\u001b[0m data_id,\n\u001b[1;32m 127\u001b[0m region\u001b[38;5;241m=\u001b[39mregion,\n\u001b[1;32m 128\u001b[0m governor_constraints\u001b[38;5;241m=\u001b[39mgovernor_constraints,\n\u001b[1;32m 129\u001b[0m )\n", + "File \u001b[0;32m~/lsst_stack_w_2023_44/stack/miniconda3-py38_4.9.2-7.0.1/DarwinX86/daf_butler/ge89626a060+c4141ea9c4/python/lsst/daf/butler/registry/queries/expressions/_predicate.py:138\u001b[0m, in \u001b[0;36mmake_string_expression_predicate\u001b[0;34m(string, dimensions, column_types, bind, data_id, defaults, dataset_type_name, allow_orphans)\u001b[0m\n\u001b[1;32m 136\u001b[0m tree \u001b[38;5;241m=\u001b[39m parser\u001b[38;5;241m.\u001b[39mparse(string)\n\u001b[1;32m 137\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[0;32m--> 138\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m UserExpressionSyntaxError(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFailed to parse user expression \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstring\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mexc\u001b[39;00m\n\u001b[1;32m 139\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m bind \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 140\u001b[0m bind \u001b[38;5;241m=\u001b[39m {}\n", + "\u001b[0;31mUserExpressionSyntaxError\u001b[0m: Failed to parse user expression 'id=4d673e07-c749-4d80-9dcf-b39615a802a4'." + ] + } + ], + "source": [ + "bulteridtest = registry.queryDatasets(\"calexp\", collections=collection, where=f\"id={testid}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "9beca3d0-2e1d-4459-a6ad-d859c12b64fa", + "metadata": {}, + "outputs": [], + "source": [ + "# vdr2 = butler.registry.queryDimensionRecords(\"visit_detector_region\", datasets='calexp', collections=collection, instrument='HSC', physical_filter='HSC-R')\n", + "# all_vdr = butler.registry.queryDatasets('calexp', physical_filter='HSC-R', collections=collection, instrument='HSC') # nope 11/27/2023 COC" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "8249ccc7-e595-44ce-8f33-2b2c74e10a56", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "There are 240 records in all_vdr.\n", + "Rate was 21614.0134 matches/second. (We performed 21406 matches in 0.9903759956359863 seconds.)\n" + ] + } + ], + "source": [ + "match_dict = {}\n", + "match_by_keys = {}\n", + "\n", + "# all_vdr = vdr\n", + "all_vdr = butler.registry.queryDimensionRecords(\n", + " \"visit_detector_region\", datasets=\"calexp\", collections=collection, instrument=\"HSC\"\n", + ") # removing , physical_filter='HSC-R'\n", + "all_vdr_count = 0\n", + "\n", + "for i in all_vdr:\n", + " all_vdr_count += 1\n", + "print(f\"There are {all_vdr_count} records in all_vdr.\")\n", + "\n", + "\n", + "def make_key(vdr):\n", + " this_key = f'{this_vdr.dataId[\"instrument\"]}_{this_vdr.dataId[\"detector\"]}_{this_vdr.dataId[\"visit\"]}'\n", + " return this_key\n", + "\n", + "\n", + "check_count = 0\n", + "start_time = time.time()\n", + "for i, this_vdr in enumerate(all_vdr):\n", + " this_key = make_key(this_vdr)\n", + " # print(this_key)\n", + " matches_ = []\n", + " matches_by_keys_ = []\n", + " # print(this_vdr.region)\n", + " for j, other_vdr in enumerate(all_vdr):\n", + " other_key = make_key(other_vdr)\n", + " if j == i:\n", + " continue\n", + " # if this_vdr.region.contains(other_vdr.region):\n", + " if this_vdr.region.intersects(other_vdr.region):\n", + " check_count += 1\n", + " # print('hi') # testing; many showed up here but not in dict?\n", + " matches.append(this_vdr)\n", + " matches_by_keys_.append(f\"{this_key} intersects {other_key}\")\n", + " match_dict[this_vdr] = matches_\n", + " match_by_keys[this_key] = matches_by_keys_\n", + "\n", + "elapsed = time.time() - start_time\n", + "rate = check_count / elapsed # seeing ~20,000/s on my MBP 11/27/2023 COC\n", + "print(f\"Rate was {round(rate,4)} matches/second. (We performed {check_count} matches in {elapsed} seconds.)\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "892331f6-f907-4eeb-aeb2-a963e7d20cc6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reminder: all_vdr_count = 240\n", + "HSC_41_322: 134\n", + "HSC_41_346: 95\n", + "HSC_41_358: 141\n", + "HSC_41_1178: 70\n", + "HSC_41_1184: 100\n", + "HSC_41_1204: 71\n", + "HSC_41_1206: 91\n", + "HSC_41_1214: 70\n", + "HSC_41_1220: 100\n", + "HSC_41_1242: 97\n", + "HSC_41_1248: 138\n", + "HSC_41_11690: 90\n", + "HSC_41_11694: 93\n", + "HSC_41_11696: 139\n", + "HSC_41_11698: 96\n", + "HSC_41_11704: 48\n", + "HSC_41_11710: 131\n", + "HSC_41_11724: 139\n", + "HSC_41_11738: 131\n", + "HSC_41_11740: 122\n", + "HSC_41_17900: 90\n", + "HSC_41_17904: 93\n", + "HSC_41_17906: 139\n", + "HSC_41_17926: 64\n", + "HSC_41_17948: 107\n", + "HSC_41_17950: 135\n", + "HSC_41_19680: 139\n", + "HSC_41_19684: 64\n", + "HSC_41_19694: 41\n", + "HSC_41_19696: 48\n", + "HSC_41_22632: 90\n", + "HSC_41_22662: 72\n", + "HSC_41_23694: 69\n", + "HSC_41_23704: 93\n", + "HSC_41_23706: 139\n", + "HSC_41_23718: 64\n", + "HSC_41_29336: 134\n", + "HSC_41_29350: 69\n", + "HSC_41_30482: 107\n", + "HSC_41_30490: 69\n", + "HSC_42_322: 107\n", + "HSC_42_346: 139\n", + "HSC_42_358: 100\n", + "HSC_42_1178: 74\n", + "HSC_42_1184: 60\n", + "HSC_42_1204: 72\n", + "HSC_42_1206: 122\n", + "HSC_42_1214: 74\n", + "HSC_42_1220: 60\n", + "HSC_42_1242: 139\n", + "HSC_42_1248: 103\n", + "HSC_42_11690: 81\n", + "HSC_42_11694: 128\n", + "HSC_42_11696: 110\n", + "HSC_42_11698: 57\n", + "HSC_42_11704: 94\n", + "HSC_42_11710: 107\n", + "HSC_42_11724: 110\n", + "HSC_42_11738: 107\n", + "HSC_42_11740: 118\n", + "HSC_42_17900: 81\n", + "HSC_42_17904: 128\n", + "HSC_42_17906: 110\n", + "HSC_42_17926: 92\n", + "HSC_42_17948: 134\n", + "HSC_42_17950: 88\n", + "HSC_42_19680: 110\n", + "HSC_42_19684: 92\n", + "HSC_42_19694: 69\n", + "HSC_42_19696: 94\n", + "HSC_42_22632: 141\n", + "HSC_42_22662: 68\n", + "HSC_42_23694: 72\n", + "HSC_42_23704: 128\n", + "HSC_42_23706: 110\n", + "HSC_42_23718: 92\n", + "HSC_42_29336: 63\n", + "HSC_42_29350: 42\n", + "HSC_42_30482: 134\n", + "HSC_42_30490: 42\n", + "HSC_47_322: 41\n", + "HSC_47_346: 37\n", + "HSC_47_358: 39\n", + "HSC_47_1178: 33\n", + "HSC_47_1184: 37\n", + "HSC_47_1204: 33\n", + "HSC_47_1206: 38\n", + "HSC_47_1214: 33\n", + "HSC_47_1220: 37\n", + "HSC_47_1242: 37\n", + "HSC_47_1248: 39\n", + "HSC_47_11690: 36\n", + "HSC_47_11694: 37\n", + "HSC_47_11696: 39\n", + "HSC_47_11698: 36\n", + "HSC_47_11704: 21\n", + "HSC_47_11710: 25\n", + "HSC_47_11724: 39\n", + "HSC_47_11738: 25\n", + "HSC_47_11740: 36\n", + "HSC_47_17900: 36\n", + "HSC_47_17904: 37\n", + "HSC_47_17906: 39\n", + "HSC_47_17926: 33\n", + "HSC_47_17948: 21\n", + "HSC_47_17950: 15\n", + "HSC_47_19680: 39\n", + "HSC_47_19684: 33\n", + "HSC_47_19694: 31\n", + "HSC_47_19696: 21\n", + "HSC_47_22632: 38\n", + "HSC_47_22662: 33\n", + "HSC_47_23694: 33\n", + "HSC_47_23704: 37\n", + "HSC_47_23706: 39\n", + "HSC_47_23718: 33\n", + "HSC_47_29336: 32\n", + "HSC_47_29350: 36\n", + "HSC_47_30482: 21\n", + "HSC_47_30490: 36\n", + "HSC_49_322: 137\n", + "HSC_49_346: 89\n", + "HSC_49_358: 148\n", + "HSC_49_1178: 113\n", + "HSC_49_1184: 145\n", + "HSC_49_1204: 116\n", + "HSC_49_1206: 96\n", + "HSC_49_1214: 113\n", + "HSC_49_1220: 145\n", + "HSC_49_1242: 92\n", + "HSC_49_1248: 142\n", + "HSC_49_11690: 113\n", + "HSC_49_11694: 94\n", + "HSC_49_11696: 134\n", + "HSC_49_11698: 149\n", + "HSC_49_11704: 68\n", + "HSC_49_11710: 87\n", + "HSC_49_11724: 134\n", + "HSC_49_11738: 87\n", + "HSC_49_11740: 89\n", + "HSC_49_17900: 113\n", + "HSC_49_17904: 94\n", + "HSC_49_17906: 134\n", + "HSC_49_17926: 99\n", + "HSC_49_17948: 55\n", + "HSC_49_17950: 68\n", + "HSC_49_19680: 134\n", + "HSC_49_19684: 99\n", + "HSC_49_19694: 82\n", + "HSC_49_19696: 68\n", + "HSC_49_22632: 81\n", + "HSC_49_22662: 116\n", + "HSC_49_23694: 113\n", + "HSC_49_23704: 94\n", + "HSC_49_23706: 134\n", + "HSC_49_23718: 99\n", + "HSC_49_29336: 102\n", + "HSC_49_29350: 141\n", + "HSC_49_30482: 55\n", + "HSC_49_30490: 141\n", + "HSC_50_322: 122\n", + "HSC_50_346: 149\n", + "HSC_50_358: 117\n", + "HSC_50_1178: 128\n", + "HSC_50_1184: 91\n", + "HSC_50_1204: 127\n", + "HSC_50_1206: 144\n", + "HSC_50_1214: 128\n", + "HSC_50_1220: 91\n", + "HSC_50_1242: 149\n", + "HSC_50_1248: 118\n", + "HSC_50_11690: 119\n", + "HSC_50_11694: 144\n", + "HSC_50_11696: 119\n", + "HSC_50_11698: 90\n", + "HSC_50_11704: 125\n", + "HSC_50_11710: 78\n", + "HSC_50_11724: 119\n", + "HSC_50_11738: 78\n", + "HSC_50_11740: 100\n", + "HSC_50_17900: 119\n", + "HSC_50_17904: 144\n", + "HSC_50_17906: 119\n", + "HSC_50_17926: 150\n", + "HSC_50_17948: 81\n", + "HSC_50_17950: 55\n", + "HSC_50_19680: 119\n", + "HSC_50_19684: 150\n", + "HSC_50_19694: 141\n", + "HSC_50_19696: 125\n", + "HSC_50_22632: 151\n", + "HSC_50_22662: 122\n", + "HSC_50_23694: 127\n", + "HSC_50_23704: 144\n", + "HSC_50_23706: 119\n", + "HSC_50_23718: 150\n", + "HSC_50_29336: 57\n", + "HSC_50_29350: 85\n", + "HSC_50_30482: 81\n", + "HSC_50_30490: 85\n", + "HSC_58_322: 76\n", + "HSC_58_346: 86\n", + "HSC_58_358: 74\n", + "HSC_58_1178: 119\n", + "HSC_58_1184: 77\n", + "HSC_58_1204: 125\n", + "HSC_58_1206: 94\n", + "HSC_58_1214: 119\n", + "HSC_58_1220: 77\n", + "HSC_58_1242: 90\n", + "HSC_58_1248: 74\n", + "HSC_58_11690: 91\n", + "HSC_58_11694: 91\n", + "HSC_58_11696: 73\n", + "HSC_58_11698: 79\n", + "HSC_58_11704: 83\n", + "HSC_58_11710: 31\n", + "HSC_58_11724: 73\n", + "HSC_58_11738: 31\n", + "HSC_58_11740: 50\n", + "HSC_58_17900: 91\n", + "HSC_58_17904: 91\n", + "HSC_58_17906: 73\n", + "HSC_58_17926: 117\n", + "HSC_58_17948: 27\n", + "HSC_58_17950: 17\n", + "HSC_58_19680: 73\n", + "HSC_58_19684: 117\n", + "HSC_58_19694: 149\n", + "HSC_58_19696: 83\n", + "HSC_58_22632: 87\n", + "HSC_58_22662: 116\n", + "HSC_58_23694: 123\n", + "HSC_58_23704: 91\n", + "HSC_58_23706: 73\n", + "HSC_58_23718: 117\n", + "HSC_58_29336: 30\n", + "HSC_58_29350: 102\n", + "HSC_58_30482: 27\n", + "HSC_58_30490: 101\n" + ] + } + ], + "source": [ + "# for i, a_vdr in enumerate(match_dict.keys()): print(f'{i}: {len(match_dict[a_vdr])}')\n", + "# for i, a_vdr in enumerate(match_dict.keys()): print(f'{len(match_dict[a_vdr])!s:>5} matches for {a_vdr.dataId}')\n", + "# print(match_by_keys.keys())\n", + "print(f\"Reminder: all_vdr_count = {all_vdr_count}\")\n", + "for i, a_key in enumerate(match_by_keys.keys()):\n", + " print(f\"{a_key}: {len(match_by_keys[a_key])}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6f87f462-789b-424b-b2a6-85080d7e3bab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6000000" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_chips_month_lsst = 1000 * 30 * 200\n", + "n_chips_month_lsst" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "f2f388e6-763a-4127-b731-fa9ac48b6dab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Brute force chip-chip (all): 51.1 years (single-thread) to match 1 month of LSST chips.\n" + ] + } + ], + "source": [ + "compute_yrs_for_lsst_month = ((n_chips_month_lsst * n_chips_month_lsst) / rate) / 60 / 60 / 24 / 365\n", + "print(\n", + " f\"Brute force chip-chip (all): {round(compute_yrs_for_lsst_month,1)} years (single-thread) to match 1 month of LSST chips.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "2326484e-d4c1-4f3b-a01d-d78136d24af5", + "metadata": {}, + "outputs": [], + "source": [ + "# vdr = butler.registry.queryDimensionRecords(\"visit_detector_region\", datasets='calexp', collections=collection, instrument='HSC', physical_filter='HSC-R') # more specific, just the calex stuff\n", + "# butler.registry.queryDatasets('calexp', physical_filter='HSC-R', collections=collection, instrument='HSC')\n", + "datasetRefs = butler.registry.queryDatasets(\n", + " \"calexp\",\n", + " # physical_filter='HSC-R',\n", + " collections=collection,\n", + " instrument=\"HSC\",\n", + ")\n", + "# datasetRefs = butler.registry.queryDatasets(datasetType='calexp',\n", + "# collection = f\"u/{os.environ['USER']}/single_frame\", # recall\n", + "# # band='i', detector=175,\n", + "# # where='visit > 192000 and visit < 193000'\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "6e129012-cf56-4b50-a543-a7b8d5c378a5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{band: 'y', instrument: 'HSC', detector: 41, physical_filter: 'HSC-Y', visit_system: 0, visit: 322}\n", + "{band: 'y', instrument: 'HSC', detector: 41, physical_filter: 'HSC-Y', visit_system: 0, visit: 346}\n", + "{band: 'y', instrument: 'HSC', detector: 41, physical_filter: 'HSC-Y', visit_system: 0, visit: 358}\n", + "{band: 'z', instrument: 'HSC', detector: 41, physical_filter: 'HSC-Z', visit_system: 0, visit: 1178}\n", + "{band: 'z', instrument: 'HSC', detector: 41, physical_filter: 'HSC-Z', visit_system: 0, visit: 1184}\n", + "...\n", + "There are 239 records total.\n" + ] + } + ], + "source": [ + "thiscounter = 0\n", + "for i, ref in enumerate(datasetRefs.expanded()):\n", + " thiscounter = i\n", + " if i < 5:\n", + " print(ref.dataId.full)\n", + "print(\"...\") # like this idea in the DP0.2 notebook 12/4/2023 COC\n", + "print(f\"There are {thiscounter} records total.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "69cf7f05-1373-4054-94fc-fc7273596723", + "metadata": {}, + "outputs": [], + "source": [ + "# for i, ref in enumerate(datasetRefs.expanded()):\n", + "# bbox = butler.get('calexp.bbox', dataId=ref.dataId)\n", + "# wcs = butler.get('calexp.wcs', dataId=ref.dataId)\n", + "# crnr_ra, crnr_dec = get_corners_radec(wcs, bbox)\n", + "# tmp = ''\n", + "# for c in range(4):\n", + "# tmp += f'({crnr_ra[c]:.3f},{crnr_dec[c]:.3f}) '\n", + "# print(i, tmp)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "5a8e930d-7b9c-4bb0-95a1-ab5e5abbf284", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Example region: ConvexPolygon([UnitVector3d(-0.8679157412883791, 0.4953768322146207, 0.03638763703307745), UnitVector3d(-0.8678508112373716, 0.49533452653358495, 0.038453559762199246), UnitVector3d(-0.8659604515352447, 0.49863199273540854, 0.03845298684875526), UnitVector3d(-0.8660276891555382, 0.4986705548678019, 0.036382953752507885)])\n" + ] + } + ], + "source": [ + "pointing_regions = []\n", + "for i, ref in enumerate(datasetRefs.expanded()):\n", + " if i == 0:\n", + " print(f\"Example region: {ref.dataId.region}\")\n", + " tmpref = ref\n", + " pointing_regions.append(ref.dataId.region)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "64834ef2-eba6-4512-a117-1acc14425bbf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConvexPolygon([UnitVector3d(-0.8679157412883791, 0.4953768322146207, 0.03638763703307745), UnitVector3d(-0.8678508112373716, 0.49533452653358495, 0.038453559762199246), UnitVector3d(-0.8659604515352447, 0.49863199273540854, 0.03845298684875526), UnitVector3d(-0.8660276891555382, 0.4986705548678019, 0.036382953752507885)])" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tmpref.dataId.region" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "06ce5618-40e8-46e7-b825-ea0fe0c5e847", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Box(NormalizedAngleInterval.fromRadians(0.0, 0.05235987755982989), AngleInterval.fromRadians(-1.5707963267948966, -1.5184364492350666))" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# from separate Patches on Sky Notebook\n", + "r = b\"b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xd6\\xeb{\\xf3\\xe9\\xce\\xaa?\\x18-DT\\xfb!\\xf9\\xbf\\xb9M\\xa8\\x04\\x84K\\xf8\\xbf\"\n", + "rd = sphgeom.Region.decode(r)\n", + "rd" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "870f142c-d186-4880-b782-3c03da562276", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Checked 240 times, found 0 matches.\n" + ] + } + ], + "source": [ + "c = 0\n", + "intersect_count = 0\n", + "for i in pointing_regions:\n", + " c += 1\n", + " if i.intersects(rd):\n", + " print(f\"Intersects!\")\n", + " intersect_count += 1\n", + "print(f\"Checked {c} times, found {intersect_count} matches.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/region_search/sky_patches.ipynb b/notebooks/region_search/sky_patches.ipynb index ee4ec036b..105295c5e 100644 --- a/notebooks/region_search/sky_patches.ipynb +++ b/notebooks/region_search/sky_patches.ipynb @@ -66,6 +66,7 @@ "source": [ "# We will set up a dictionary with some values we will need for a given \"instrument\" here.\n", "chipDict = {}\n", + "# matches_per_sec refers to how fast we can compare region hashes using LSST sphgeom\n", "\n", "# Dark Energy Camera; Cerro Tololo Inter-American Observatory (CTIO), Chile\n", "chipDict[\"DECam\"] = {\"chipsize_arcmin\": [9, 18]} # 0.263\"/pixel, (2048,4096) pix/chip = (8.98,17.95')/chip\n", @@ -221,7 +222,7 @@ } ], "source": [ - "# test very confined Dec case\n", + "# test very confined Declination case\n", "patches_result, patches_centers, info = generate_patches(\n", " arcminutes=(15, 15), overlap_percentage=0, decRange=[-90, -89.75], export=False\n", ")\n", @@ -279,7 +280,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 1, "id": "52946064", "metadata": {}, "outputs": [], @@ -342,7 +343,7 @@ " ax.set_xlim(xrange[0], xrange[1])\n", " ax.set_ylim(yrange[0], yrange[1])\n", "\n", - " plt.grid(True)\n", + " plt.grid(False)\n", "\n", " outfile_base = f\"{subfolder}/patches\"\n", " if title != None:\n", From d7bacfad20405900bab4d0c28c2b0796909e73c2 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:17:28 -0500 Subject: [PATCH 22/27] Implementation --- data/demo_config.yml | 1 - docs/source/user_manual/search_params.rst | 3 + src/kbmod/analysis_utils.py | 143 ------------------ src/kbmod/configuration.py | 1 + src/kbmod/filters/stamp_filters.py | 170 +++++++++++++++++++++- src/kbmod/run_search.py | 21 ++- src/kbmod/search/common.h | 17 +++ src/kbmod/search/raw_image.cpp | 18 ++- tests/test_analysis_utils.py | 126 ---------------- tests/test_configuration.py | 1 + tests/test_raw_image.py | 16 ++ tests/test_stamp_filters.py | 124 ++++++++++++++++ 12 files changed, 353 insertions(+), 288 deletions(-) diff --git a/data/demo_config.yml b/data/demo_config.yml index 1d88e80c5..12d8b8585 100644 --- a/data/demo_config.yml +++ b/data/demo_config.yml @@ -55,7 +55,6 @@ mom_lims: - 37.5 - 37.5 - 1.5 -- 1.5 - 1.0 - 1.0 num_cores: 1 diff --git a/docs/source/user_manual/search_params.rst b/docs/source/user_manual/search_params.rst index 32efcce6c..c6f48e94f 100644 --- a/docs/source/user_manual/search_params.rst +++ b/docs/source/user_manual/search_params.rst @@ -155,6 +155,9 @@ This document serves to provide a quick overview of the existing parameters and | | | Can be use used in addition to | | | | outputting individual result files. | +------------------------+-----------------------------+----------------------------------------+ +| ``save_all_stamps`` | True | Save the individual stamps for each | +| | | result and timestep. | ++------------------------+-----------------------------+----------------------------------------+ | ``sigmaG_lims`` | [25, 75] | The percentiles to use in sigmaG | | | | filtering, if | | | | ``filter_type= clipped_sigmaG``. | diff --git a/src/kbmod/analysis_utils.py b/src/kbmod/analysis_utils.py index 4da73ca78..709e7a66f 100644 --- a/src/kbmod/analysis_utils.py +++ b/src/kbmod/analysis_utils.py @@ -121,149 +121,6 @@ def load_and_filter_results( res_num += chunk_size return keep - def get_all_stamps(self, result_list, search, stamp_radius): - """Get the stamps for the final results from a kbmod search. - - Parameters - ---------- - result_list : `ResultList` - The values from trajectories. The stamps are inserted into this data structure. - search : `kbmod.StackSearch` - The search object - stamp_radius : int - The radius of the stamps to create. - """ - stamp_edge = stamp_radius * 2 + 1 - for row in result_list.results: - stamps = kb.StampCreator.get_stamps(search.get_imagestack(), row.trajectory, stamp_radius) - # TODO: a way to avoid a copy here would be to do - # np.array([s.image for s in stamps], dtype=np.single, copy=False) - # but that could cause a problem with reference counting at the m - # moment. The real fix is to make the stamps return Image not - # RawImage, return the Image and avoid a reference to a private - # attribute. This risks collecting RawImage but leaving a dangling - # ref to its private field. That's a fix for another time. - row.all_stamps = np.array([stamp.image for stamp in stamps]) - - def apply_stamp_filter( - self, - result_list, - search, - center_thresh=0.03, - peak_offset=[2.0, 2.0], - mom_lims=[35.5, 35.5, 1.0, 0.25, 0.25], - chunk_size=1000000, - stamp_type="sum", - stamp_radius=10, - ): - """This function filters result postage stamps based on their Gaussian - Moments. Results with stamps that are similar to a Gaussian are kept. - - Parameters - ---------- - result_list : `ResultList` - The values from trajectories. This data gets modified directly by - the filtering. - search : `kbmod.StackSearch` - The search object. - center_thresh : float - The fraction of the total flux that must be contained in a single - central pixel. - peak_offset : list of floats - How far the brightest pixel in the stamp can be from the central - pixel. - mom_lims : list of floats - The maximum limit of the xx, yy, xy, x, and y central moments of - the stamp. - chunk_size : int - How many stamps to load and filter at a time. - stamp_type : string - Which method to use to generate stamps. - One of 'median', 'cpp_median', 'mean', 'cpp_mean', or 'sum'. - stamp_radius : int - The radius of the stamp. - """ - # Set the stamp creation and filtering parameters. - params = kb.StampParameters() - params.radius = stamp_radius - params.do_filtering = True - params.center_thresh = center_thresh - params.peak_offset_x = peak_offset[0] - params.peak_offset_y = peak_offset[1] - params.m20_limit = mom_lims[0] - params.m02_limit = mom_lims[1] - params.m11_limit = mom_lims[2] - params.m10_limit = mom_lims[3] - params.m01_limit = mom_lims[4] - - if stamp_type == "cpp_median" or stamp_type == "median": - params.stamp_type = kb.StampType.STAMP_MEDIAN - elif stamp_type == "cpp_mean" or stamp_type == "mean": - params.stamp_type = kb.StampType.STAMP_MEAN - else: - params.stamp_type = kb.StampType.STAMP_SUM - - # Save some useful helper data. - num_times = search.get_num_images() - all_valid_inds = [] - - # Run the stamp creation and filtering in batches of chunk_size. - print("---------------------------------------") - print("Applying Stamp Filtering") - print("---------------------------------------", flush=True) - start_time = time.time() - start_idx = 0 - if result_list.num_results() <= 0: - print("Skipping. Nothing to filter.") - return - - print("Stamp filtering %i results" % result_list.num_results()) - while start_idx < result_list.num_results(): - end_idx = min([start_idx + chunk_size, result_list.num_results()]) - - # Create a subslice of the results and the Boolean indices. - # Note that the sum stamp type does not filter out lc_index. - inds_to_use = [i for i in range(start_idx, end_idx)] - trj_slice = [result_list.results[i].trajectory for i in inds_to_use] - if params.stamp_type != kb.StampType.STAMP_SUM: - bool_slice = [result_list.results[i].valid_indices_as_booleans() for i in inds_to_use] - else: - # For the sum stamp, use all the indices for each trajectory. - all_true = [True] * num_times - bool_slice = [all_true for _ in inds_to_use] - - # Create and filter the results, using the GPU if there is one and enough - # trajectories to make it worthwhile. - stamps_slice = kb.StampCreator.get_coadded_stamps( - search.get_imagestack(), - trj_slice, - bool_slice, - params, - kb.HAS_GPU and len(trj_slice) > 100, - ) - # TODO: a way to avoid a copy here would be to do - # np.array([s.image for s in stamps], dtype=np.single, copy=False) - # but that could cause a problem with reference counting at the m - # moment. The real fix is to make the stamps return Image not - # RawImage and avoid reference to an private attribute and risking - # collecting RawImage but leaving a dangling ref to the attribute. - # That's a fix for another time so I'm leaving it as a copy here - for ind, stamp in enumerate(stamps_slice): - if stamp.width > 1: - result_list.results[ind + start_idx].stamp = np.array(stamp.image) - all_valid_inds.append(ind + start_idx) - - # Move to the next chunk. - start_idx += chunk_size - - # Do the actual filtering of results - result_list.filter_results(all_valid_inds) - print("Keeping %i results" % result_list.num_results(), flush=True) - - end_time = time.time() - time_elapsed = end_time - start_time - print("{:.2f}s elapsed".format(time_elapsed)) - def apply_clustering(self, result_list, cluster_params): """This function clusters results that have similar trajectories. diff --git a/src/kbmod/configuration.py b/src/kbmod/configuration.py index c6fdeeba4..88e59063b 100644 --- a/src/kbmod/configuration.py +++ b/src/kbmod/configuration.py @@ -74,6 +74,7 @@ def __init__(self): "repeated_flag_keys": default_repeated_flag_keys, "res_filepath": None, "result_filename": None, + "save_all_stamps": True, "sigmaG_lims": [25, 75], "stamp_radius": 10, "stamp_type": "sum", diff --git a/src/kbmod/filters/stamp_filters.py b/src/kbmod/filters/stamp_filters.py index 3d5db49aa..4354c26c5 100644 --- a/src/kbmod/filters/stamp_filters.py +++ b/src/kbmod/filters/stamp_filters.py @@ -5,9 +5,20 @@ """ import abc +import numpy as np +import time +from kbmod.configuration import SearchConfiguration from kbmod.result_list import ResultRow -from kbmod.search import KB_NO_DATA, RawImage +from kbmod.search import ( + HAS_GPU, + KB_NO_DATA, + ImageStack, + RawImage, + StampCreator, + StampParameters, + StampType, +) class BaseStampFilter(abc.ABC): @@ -237,3 +248,160 @@ def keep_row(self, row: ResultRow): """ image = RawImage(row.stamp) return image.center_is_local_max(self.flux_thresh, self.local_max) + + +def extract_search_parameters_from_config(config): + """Create an initialized StampParameters object from the configuration settings + while doing some validity checking. + + Parameters + ---------- + config : `SearchConfiguration` + The configuration object. + + Returns + ------- + params : `StampParameters` + The StampParameters object with all fields set. + + Raises + ------ + Raises a ``ValueError`` if parameter validation fails. + Raises a ``KeyError`` if a required parameter is not found. + """ + params = StampParameters() + + # Construction parameters + params.radius = config["stamp_radius"] + if params.radius < 0: + raise ValueError(f"Invalid stamp radius {params.radius}") + + stamp_type = config["stamp_type"] + if stamp_type == "cpp_median" or stamp_type == "median": + params.stamp_type = StampType.STAMP_MEDIAN + elif stamp_type == "cpp_mean" or stamp_type == "mean": + params.stamp_type = StampType.STAMP_MEAN + elif stamp_type == "cpp_sum" or stamp_type == "sum": + params.stamp_type = StampType.STAMP_SUM + else: + raise ValueError(f"Unrecognized stamp type: {stamp_type}") + + # Filtering parameters (with validity checking) + params.do_filtering = config["do_stamp_filter"] + params.center_thresh = config["center_thresh"] + + peak_offset = config["peak_offset"] + if len(peak_offset) != 2: + raise ValueError(f"Expected length 2 list for peak_offset. Found {peak_offset}") + params.peak_offset_x = peak_offset[0] + params.peak_offset_y = peak_offset[1] + + mom_lims = config["mom_lims"] + if len(mom_lims) != 5: + raise ValueError(f"Expected length 5 list for mom_lims. Found {mom_lims}") + params.m20_limit = mom_lims[0] + params.m02_limit = mom_lims[1] + params.m11_limit = mom_lims[2] + params.m10_limit = mom_lims[3] + params.m01_limit = mom_lims[4] + + return params + + +def get_coadds_and_filter(result_list, im_stack, stamp_params, chunk_size=1000000, debug=False): + """Create the co-added postage stamps and filter them based on their statistical + properties. Results with stamps that are similar to a Gaussian are kept. + + Parameters + ---------- + result_list : `ResultList` + The current set of results. Modified directly. + im_stack : `ImageStack` + The images from which to build the co-added stamps. + stamp_params : `StampParameters` or `SearchConfiguration` + The filtering parameters for the stamps. + chunk_size : `int` + How many stamps to load and filter at a time. Used to control memory. + debug : `bool` + Output verbose debugging messages. + """ + if type(stamp_params) is SearchConfiguration: + stamp_params = extract_search_parameters_from_config(stamp_params) + + if debug: + print("---------------------------------------") + print("Applying Stamp Filtering") + print("---------------------------------------") + if result_list.num_results() <= 0: + print("Skipping. Nothing to filter.") + else: + print(f"Stamp filtering {result_list.num_results()} results.") + print(stamp_params) + print(f"Using chunksize = {chunk_size}") + + # Run the stamp creation and filtering in batches of chunk_size. + start_time = time.time() + start_idx = 0 + all_valid_inds = [] + while start_idx < result_list.num_results(): + end_idx = min([start_idx + chunk_size, result_list.num_results()]) + + # Create a subslice of the results and the Boolean indices. + # Note that the sum stamp type does not filter out lc_index. + inds_to_use = [i for i in range(start_idx, end_idx)] + trj_slice = [result_list.results[i].trajectory for i in inds_to_use] + if stamp_params.stamp_type != StampType.STAMP_SUM: + bool_slice = [result_list.results[i].valid_indices_as_booleans() for i in inds_to_use] + else: + # For the sum stamp, use all the indices for each trajectory. + all_true = [True] * im_stack.img_count() + bool_slice = [all_true for _ in inds_to_use] + + # Create and filter the results, using the GPU if there is one and enough + # trajectories to make it worthwhile. + stamps_slice = StampCreator.get_coadded_stamps( + im_stack, + trj_slice, + bool_slice, + stamp_params, + HAS_GPU and len(trj_slice) > 100, + ) + # TODO: a way to avoid a copy here would be to do + # np.array([s.image for s in stamps], dtype=np.single, copy=False) + # but that could cause a problem with reference counting at the m + # moment. The real fix is to make the stamps return Image not + # RawImage and avoid reference to an private attribute and risking + # collecting RawImage but leaving a dangling ref to the attribute. + # That's a fix for another time so I'm leaving it as a copy here + for ind, stamp in enumerate(stamps_slice): + if stamp.width > 1: + result_list.results[ind + start_idx].stamp = np.array(stamp.image) + all_valid_inds.append(ind + start_idx) + + # Move to the next chunk. + start_idx += chunk_size + + # Do the actual filtering of results + result_list.filter_results(all_valid_inds) + if debug: + print("Keeping %i results" % result_list.num_results(), flush=True) + time_elapsed = time.time() - start_time + print("{:.2f}s elapsed".format(time_elapsed)) + + +def append_all_stamps(result_list, im_stack, stamp_radius): + """Get the stamps for the final results from a kbmod search. These are appended + onto the corresponding entries in a ResultList. + + Parameters + ---------- + result_list : `ResultList` + The current set of results. Modified directly. + im_stack : `ImageStack` + The stack of images. + stamp_radius : `int` + The radius of the stamps to create. + """ + for row in result_list.results: + stamps = StampCreator.get_stamps(im_stack, row.trajectory, stamp_radius) + row.all_stamps = np.array([stamp.image for stamp in stamps]) diff --git a/src/kbmod/run_search.py b/src/kbmod/run_search.py index 0065ccae3..9ca49f1ec 100644 --- a/src/kbmod/run_search.py +++ b/src/kbmod/run_search.py @@ -13,11 +13,12 @@ import kbmod.search as kb from .analysis_utils import PostProcess -from .data_interface import load_input_from_config, load_input_from_file from .configuration import SearchConfiguration +from .data_interface import load_input_from_config, load_input_from_file +from .filters.sigma_g_filter import SigmaGClipping +from .filters.stamp_filters import append_all_stamps, get_coadds_and_filter from .masking import apply_mask_operations from .result_list import * -from .filters.sigma_g_filter import SigmaGClipping from .work_unit import WorkUnit @@ -161,14 +162,11 @@ def run_search(self, config, stack): max_lh=config["max_lh"], ) if config["do_stamp_filter"]: - kb_post_process.apply_stamp_filter( + get_coadds_and_filter( keep, - search, - center_thresh=config["center_thresh"], - peak_offset=config["peak_offset"], - mom_lims=config["mom_lims"], - stamp_type=config["stamp_type"], - stamp_radius=config["stamp_radius"], + search.get_imagestack(), + config, + debug=config["debug"], ) if config["do_clustering"]: @@ -180,8 +178,9 @@ def run_search(self, config, stack): cluster_params["mjd"] = np.array(mjds) kb_post_process.apply_clustering(keep, cluster_params) - # Extract all the stamps. - kb_post_process.get_all_stamps(keep, search, config["stamp_radius"]) + # Extract all the stamps for all time steps and append them onto the result rows. + if config["save_all_stamps"]: + append_all_stamps(keep, search.get_imagestack(), config["stamp_radius"]) # TODO - Re-enable the known object counting once we have a way to pass # A WCS into the WorkUnit. diff --git a/src/kbmod/search/common.h b/src/kbmod/search/common.h index b7c89663e..285d51876 100644 --- a/src/kbmod/search/common.h +++ b/src/kbmod/search/common.h @@ -106,6 +106,22 @@ struct StampParameters { float m11_limit; float m02_limit; float m20_limit; + + const std::string to_string() const { + // If filtering is turned off, output the minimal information on a single line. + // Otherwise dump the full statistics on multiple lines. + if (!do_filtering) { + return ("Type: " + std::to_string(stamp_type) + " Radius: " + std::to_string(radius) + + " Filtering: false"); + } else { + return ("Type: " + std::to_string(stamp_type) + "\nRadius: " + std::to_string(radius) + + "\nFiltering: true" + "\nCenter Thresh: " + std::to_string(center_thresh) + + "\nPeak Offset: x=" + std::to_string(peak_offset_x) + " y=" + + std::to_string(peak_offset_y) + "\nMoment Limits: m01=" + std::to_string(m01_limit) + + " m10=" + std::to_string(m10_limit) + " m11=" + std::to_string(m11_limit) + + " m02=" + std::to_string(m02_limit) + " m20=" + std::to_string(m02_limit)); + } + } }; // Basic image moments use for analysis. @@ -162,6 +178,7 @@ static void image_moments_bindings(py::module &m) { static void stamp_parameters_bindings(py::module &m) { py::class_(m, "StampParameters", pydocs::DOC_StampParameters) .def(py::init<>()) + .def("__str__", &StampParameters::to_string) .def_readwrite("radius", &StampParameters::radius) .def_readwrite("stamp_type", &StampParameters::stamp_type) .def_readwrite("do_filtering", &StampParameters::do_filtering) diff --git a/src/kbmod/search/raw_image.cpp b/src/kbmod/search/raw_image.cpp index a2e541355..f4f40637f 100644 --- a/src/kbmod/search/raw_image.cpp +++ b/src/kbmod/search/raw_image.cpp @@ -114,13 +114,19 @@ RawImage RawImage::create_stamp(const Point& p, const int radius, const bool kee if (radius < 0) throw std::runtime_error("stamp radius must be at least 0"); const int dim = radius * 2 + 1; - // can't address this instance of non-uniform index handling with Point - // and Index, because at a base level it adopts a different definition of - // the pixel grid to coordinate system transformation. - auto [corner, anchor, w, h] = indexing::anchored_block({(int)p.y, (int)p.x}, radius, width, height); - Image stamp = Image::Constant(dim, dim, NO_DATA); - stamp.block(anchor.i, anchor.j, h, w) = image.block(corner.i, corner.j, h, w); + + // Eigen gets uphappy if the stamp does not overlap at all. In this case, skip + // the computation and leave the entire stamp set to NO_DATA. + Index idx = p.to_index(); + if ((idx.j + radius >= 0) && (idx.j - radius < (int)width) && + (idx.i + radius >= 0) && (idx.i - radius < (int)height)) { + // can't address this instance of non-uniform index handling with Point + // and Index, because at a base level it adopts a different definition of + // the pixel grid to coordinate system transformation. + auto [corner, anchor, w, h] = indexing::anchored_block({(int)p.y, (int)p.x}, radius, width, height); + stamp.block(anchor.i, anchor.j, h, w) = image.block(corner.i, corner.j, h, w); + } if (!keep_no_data) stamp = (stamp.array() == NO_DATA).select(0.0, stamp); diff --git a/tests/test_analysis_utils.py b/tests/test_analysis_utils.py index 9d8523962..506202de0 100644 --- a/tests/test_analysis_utils.py +++ b/tests/test_analysis_utils.py @@ -131,132 +131,6 @@ def setUp(self): row.set_psi_phi(get_psi_curves[i], get_phi_curves[i]) self.curve_result_set.append_result(row) - @unittest.skipIf(not HAS_GPU, "Skipping test (no GPU detected)") - def test_apply_stamp_filter(self): - # object properties - self.object_flux = 250.0 - self.start_x = 4 - self.start_y = 3 - self.vxel = 2.0 - self.vyel = 1.0 - - for i in range(self.img_count): - time = i / self.img_count - add_fake_object( - self.imlist[i], - self.start_x + time * self.vxel + 0.5, - self.start_y + time * self.vyel + 0.5, - self.object_flux, - self.p, - ) - - stack = ImageStack(self.imlist) - search = StackSearch(stack) - search.search( - self.angle_steps, - self.velocity_steps, - self.min_angle, - self.max_angle, - self.min_vel, - self.max_vel, - int(self.img_count / 2), - ) - - zeroed_times = np.array(stack.build_zeroed_times()) - kb_post_process = PostProcess(self.config, zeroed_times) - - keep = kb_post_process.load_and_filter_results( - search, - self.config["lh_level"], - chunk_size=self.config["chunk_size"], - max_lh=self.config["max_lh"], - ) - - # Apply the stamp filter with default parameters. - kb_post_process.apply_stamp_filter(keep, search) - - # Check that we get at least one result and those results have stamps. - self.assertGreater(keep.num_results(), 0) - for i in range(keep.num_results()): - self.assertIsNotNone(keep.results[i].stamp) - - def test_apply_stamp_filter_2(self): - # Also confirms that apply_stamp_filter works with a chunksize < number - # of results. - - # object properties - self.object_flux = 250.0 - self.start_x = 4 - self.start_y = 3 - self.vxel = 2.0 - self.vyel = 1.0 - - for i in range(self.img_count): - time = i / self.img_count - add_fake_object( - self.imlist[i], - self.start_x + time * self.vxel, - self.start_y + time * self.vyel, - self.object_flux, - self.p, - ) - - stack = ImageStack(self.imlist) - search = StackSearch(stack) - - # Create a first Trajectory that matches perfectly. - trj = Trajectory() - trj.x = self.start_x - trj.y = self.start_y - trj.vx = self.vxel - trj.vy = self.vyel - - # Create a second Trajectory that isn't any good. - trj2 = Trajectory() - trj2.x = 1 - trj2.y = 1 - trj2.vx = 0 - trj2.vy = 0 - - # Create a third Trajectory that is close to good, but offset. - trj3 = Trajectory() - trj3.x = trj.x + 2 - trj3.y = trj.y + 2 - trj3.vx = trj.vx - trj3.vy = trj.vy - - # Create a fourth Trajectory that is just close enough - trj4 = Trajectory() - trj4.x = trj.x + 1 - trj4.y = trj.y + 1 - trj4.vx = trj.vx - trj4.vy = trj.vy - - # Create the ResultList. - keep = ResultList(self.time_list) - keep.append_result(ResultRow(trj, self.img_count)) - keep.append_result(ResultRow(trj2, self.img_count)) - keep.append_result(ResultRow(trj3, self.img_count)) - keep.append_result(ResultRow(trj4, self.img_count)) - - # Create the post processing object. - kb_post_process = PostProcess(self.config, self.time_list) - keep2 = kb_post_process.apply_stamp_filter( - keep, - search, - center_thresh=0.03, - peak_offset=[1.5, 1.5], - mom_lims=[35.5, 35.5, 1.0, 1.0, 1.0], - chunk_size=1, - stamp_type="cpp_mean", - stamp_radius=5, - ) - - # The check that the correct indices and number of stamps are saved. - self.assertEqual(keep.num_results(), 2) - self.assertEqual(keep.results[0].trajectory.x, self.start_x) - self.assertEqual(keep.results[1].trajectory.x, self.start_x + 1) - def test_clustering(self): cluster_params = {} cluster_params["x_size"] = self.dim_x diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 7140352cd..da9f90761 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -7,6 +7,7 @@ from yaml import safe_load from kbmod.configuration import SearchConfiguration +from kbmod.search import StampParameters, StampType class test_configuration(unittest.TestCase): diff --git a/tests/test_raw_image.py b/tests/test_raw_image.py index be1a48d57..6d70434ca 100644 --- a/tests/test_raw_image.py +++ b/tests/test_raw_image.py @@ -374,6 +374,22 @@ def test_make_stamp(self): expected = np.array([[0.0, 100.0, 101.0], [0.0, 110.0, 111.0], [0.0, 0.0, 0.0]]) self.assertTrue((stamp.image == expected).all()) + # Test a stamp that is completely out of bounds. + stamp = img.create_stamp(20.5, 20.5, 1, False) + expected = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.assertTrue((stamp.image == expected).all()) + + # Test a stamp that overlaps at a single corner pixel. + stamp = img.create_stamp(-1.5, -1.5, 1, True) + expected = np.array( + [ + [KB_NO_DATA, KB_NO_DATA, KB_NO_DATA], + [KB_NO_DATA, KB_NO_DATA, KB_NO_DATA], + [KB_NO_DATA, KB_NO_DATA, 0.0], + ] + ) + self.assertTrue((stamp.image == expected).all()) + def test_read_write_file(self): """Test file writes and reads correctly.""" img = RawImage(self.array, 10.0) diff --git a/tests/test_stamp_filters.py b/tests/test_stamp_filters.py index 61c44cf73..32e62272a 100644 --- a/tests/test_stamp_filters.py +++ b/tests/test_stamp_filters.py @@ -1,5 +1,8 @@ +import numpy as np import unittest +from kbmod.configuration import SearchConfiguration +from kbmod.fake_data_creator import add_fake_object, FakeDataSet from kbmod.filters.stamp_filters import * from kbmod.result_list import * from kbmod.search import * @@ -137,6 +140,127 @@ def test_center_filtering(self): self.assertFalse(StampCenterFilter(3, True, 0.4).keep_row(row)) self.assertTrue(StampCenterFilter(3, False, 0.2).keep_row(row)) + def test_extract_search_parameters_from_config(self): + config_dict = { + "center_thresh": 0.05, + "do_stamp_filter": True, + "mom_lims": [50.0, 51.0, 1.0, 2.0, 3.0], + "peak_offset": [1.5, 3.5], + "stamp_type": "median", + "stamp_radius": 7, + } + config = SearchConfiguration.from_dict(config_dict) + + params = extract_search_parameters_from_config(config) + self.assertEqual(params.radius, 7) + self.assertEqual(params.stamp_type, StampType.STAMP_MEDIAN) + self.assertEqual(params.do_filtering, True) + self.assertAlmostEqual(params.center_thresh, 0.05) + self.assertAlmostEqual(params.peak_offset_x, 1.5) + self.assertAlmostEqual(params.peak_offset_y, 3.5) + self.assertAlmostEqual(params.m20_limit, 50.0) + self.assertAlmostEqual(params.m02_limit, 51.0) + self.assertAlmostEqual(params.m11_limit, 1.0) + self.assertAlmostEqual(params.m10_limit, 2.0) + self.assertAlmostEqual(params.m01_limit, 3.0) + + # Test bad configurations + config.set("stamp_radius", -1) + self.assertRaises(ValueError, extract_search_parameters_from_config, config) + config.set("stamp_radius", 7) + + config.set("stamp_type", "broken") + self.assertRaises(ValueError, extract_search_parameters_from_config, config) + config.set("stamp_type", "median") + + config.set("peak_offset", [50.0]) + self.assertRaises(ValueError, extract_search_parameters_from_config, config) + config.set("peak_offset", [1.5, 3.5]) + + config.set("mom_lims", [50.0, 51.0, 1.0, 3.0]) + self.assertRaises(ValueError, extract_search_parameters_from_config, config) + config.set("mom_lims", [50.0, 51.0, 1.0, 2.0, 3.0]) + + @unittest.skipIf(not HAS_GPU, "Skipping test (no GPU detected)") + def test_get_coadds_and_filter(self): + image_count = 10 + ds = FakeDataSet( + 25, # width + 35, # height + image_count, # time steps + 1.0, # noise level + 0.5, # psf value + 1, # observations per day + True, # Use a fixed seed for testing + ) + + # Insert a single fake object with known parameters. + trj = make_trajectory(8, 7, 2.0, 1.0, flux=250.0) + ds.insert_object(trj) + + # Second Trajectory that isn't any good. + trj2 = make_trajectory(1, 1, 0.0, 0.0) + + # Third Trajectory that is close to good, but offset. + trj3 = make_trajectory(trj.x + 2, trj.y + 2, trj.vx, trj.vy) + + # Create a fourth Trajectory that is just close enough + trj4 = make_trajectory(trj.x + 1, trj.y + 1, trj.vx, trj.vy) + + # Create the ResultList. + keep = ResultList(ds.times) + keep.append_result(ResultRow(trj, image_count)) + keep.append_result(ResultRow(trj2, image_count)) + keep.append_result(ResultRow(trj3, image_count)) + keep.append_result(ResultRow(trj4, image_count)) + + # Create the stamp parameters we need. + config_dict = { + "center_thresh": 0.03, + "do_stamp_filter": True, + "mom_lims": [35.5, 35.5, 1.0, 1.0, 1.0], + "peak_offset": [1.5, 1.5], + "stamp_type": "cpp_mean", + "stamp_radius": 5, + } + config = SearchConfiguration.from_dict(config_dict) + + # Do the filtering. + get_coadds_and_filter(keep, ds.stack, config, chunk_size=1, debug=False) + + # The check that the correct indices and number of stamps are saved. + self.assertEqual(keep.num_results(), 2) + self.assertEqual(keep.results[0].trajectory.x, trj.x) + self.assertEqual(keep.results[1].trajectory.x, trj.x + 1) + self.assertIsNotNone(keep.results[0].stamp) + self.assertIsNotNone(keep.results[1].stamp) + + def test_append_all_stamps(self): + image_count = 10 + ds = FakeDataSet( + 25, # width + 35, # height + image_count, # time steps + 1.0, # noise level + 0.5, # psf value + 1, # observations per day + True, # Use a fixed seed for testing + ) + + # Make a few results with different trajectories. + keep = ResultList(ds.times) + keep.append_result(ResultRow(make_trajectory(8, 7, 2.0, 1.0), image_count)) + keep.append_result(ResultRow(make_trajectory(10, 22, -2.0, -1.0), image_count)) + keep.append_result(ResultRow(make_trajectory(8, 7, -2.0, -1.0), image_count)) + + append_all_stamps(keep, ds.stack, 5) + for row in keep.results: + self.assertIsNotNone(row.all_stamps) + self.assertEqual(len(row.all_stamps), image_count) + for i in range(image_count): + self.assertEqual(np.shape(row.all_stamps[i])[0], 11) + self.assertEqual(np.shape(row.all_stamps[i])[1], 11) + if __name__ == "__main__": unittest.main() From cc2ea338d6d887d44099ee92fece97cad4c1ccc8 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:21:22 -0500 Subject: [PATCH 23/27] Formatting --- src/kbmod/search/raw_image.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/kbmod/search/raw_image.cpp b/src/kbmod/search/raw_image.cpp index f4f40637f..019f18a85 100644 --- a/src/kbmod/search/raw_image.cpp +++ b/src/kbmod/search/raw_image.cpp @@ -116,11 +116,11 @@ RawImage RawImage::create_stamp(const Point& p, const int radius, const bool kee const int dim = radius * 2 + 1; Image stamp = Image::Constant(dim, dim, NO_DATA); - // Eigen gets uphappy if the stamp does not overlap at all. In this case, skip - // the computation and leave the entire stamp set to NO_DATA. + // Eigen gets uphappy if the stamp does not overlap at all. In this case, skip + // the computation and leave the entire stamp set to NO_DATA. Index idx = p.to_index(); - if ((idx.j + radius >= 0) && (idx.j - radius < (int)width) && - (idx.i + radius >= 0) && (idx.i - radius < (int)height)) { + if ((idx.j + radius >= 0) && (idx.j - radius < (int)width) && (idx.i + radius >= 0) && + (idx.i - radius < (int)height)) { // can't address this instance of non-uniform index handling with Point // and Index, because at a base level it adopts a different definition of // the pixel grid to coordinate system transformation. From 30759be71f6eda83481defb7e3b0618114e6acaa Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:30:42 -0500 Subject: [PATCH 24/27] Remove unneeded import --- tests/test_configuration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index da9f90761..7140352cd 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -7,7 +7,6 @@ from yaml import safe_load from kbmod.configuration import SearchConfiguration -from kbmod.search import StampParameters, StampType class test_configuration(unittest.TestCase): From 5cc08bd8dc0be90eebc56008cd8705a4e0467373 Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:49:42 -0500 Subject: [PATCH 25/27] Fix typo --- src/kbmod/search/raw_image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kbmod/search/raw_image.cpp b/src/kbmod/search/raw_image.cpp index 019f18a85..739c576c8 100644 --- a/src/kbmod/search/raw_image.cpp +++ b/src/kbmod/search/raw_image.cpp @@ -116,7 +116,7 @@ RawImage RawImage::create_stamp(const Point& p, const int radius, const bool kee const int dim = radius * 2 + 1; Image stamp = Image::Constant(dim, dim, NO_DATA); - // Eigen gets uphappy if the stamp does not overlap at all. In this case, skip + // Eigen gets unhappy if the stamp does not overlap at all. In this case, skip // the computation and leave the entire stamp set to NO_DATA. Index idx = p.to_index(); if ((idx.j + radius >= 0) && (idx.j - radius < (int)width) && (idx.i + radius >= 0) && From 545d1073570635e518032dd37a09a8424f7131a8 Mon Sep 17 00:00:00 2001 From: Colin Orion Chandler Date: Wed, 7 Feb 2024 16:12:34 -0800 Subject: [PATCH 26/27] Region searching (#461) * Plot correction, clarifying text disable gridlines for the sky plotting * Create Region Searching Workbook.ipynb This contains the essence of Region Search for now. * Creating a structure for scratch notebooks Brainstorming, demos, and testing to share. * Update RegionSearchTesting.ipynb black --target-version py38 --line-length 110 is what works, but only if black was installed with [jupyter] * Update Region Searching Workbook.ipynb Consolidation into a single Pandas dataframe, Notebook cleanup, master function in preparation for a demo, TODO items added/updated, and a Next Steps added to the end. * Update Region Searching Workbook.ipynb Fixed broken/missing get_timestamps. Other minor fixes too. * Update Region Searching Workbook.ipynb Fixed hanging function with a pass. --- .../Region Searching Workbook.ipynb | 586 ++++++++++-------- 1 file changed, 311 insertions(+), 275 deletions(-) diff --git a/notebooks/region_search/Region Searching Workbook.ipynb b/notebooks/region_search/Region Searching Workbook.ipynb index 5d7e6da65..deb393ea1 100644 --- a/notebooks/region_search/Region Searching Workbook.ipynb +++ b/notebooks/region_search/Region Searching Workbook.ipynb @@ -5,7 +5,7 @@ "id": "d94d8d2b", "metadata": {}, "source": [ - "# Butler Interface for User\n", + "# Region Searching for KBMOD\n", "\n", "The point of this notebook is to do step-by-step exploration of the DEEP dataset that was first run through KBMOD for the first set of papers. That work was led by Hayden Smotherman, hence the reference to that name.\n", "\n", @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 339, + "execution_count": 1, "id": "d0189084", "metadata": {}, "outputs": [], @@ -45,6 +45,7 @@ "import lsst\n", "import lsst.daf.butler as dafButler\n", "import os\n", + "import glob\n", "import time\n", "from matplotlib import pyplot as plt\n", "import progressbar\n", @@ -58,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": 476, + "execution_count": 2, "id": "ceeec168", "metadata": {}, "outputs": [ @@ -82,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "693492d4", "metadata": {}, "outputs": [], @@ -94,7 +95,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "b13eb927", "metadata": {}, "outputs": [], @@ -127,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 465, + "execution_count": 5, "id": "e6a546f0", "metadata": {}, "outputs": [], @@ -157,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 466, + "execution_count": 6, "id": "3006e4fd", "metadata": {}, "outputs": [ @@ -175,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 468, + "execution_count": 7, "id": "b409c810", "metadata": {}, "outputs": [], @@ -234,7 +235,7 @@ }, { "cell_type": "code", - "execution_count": 473, + "execution_count": 8, "id": "dc9a4efc", "metadata": {}, "outputs": [ @@ -251,7 +252,7 @@ " 'PointingGroup023/imdiff_r']" ] }, - "execution_count": 473, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -263,7 +264,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "id": "4537e06a", "metadata": {}, "outputs": [], @@ -298,7 +299,7 @@ }, { "cell_type": "code", - "execution_count": 169, + "execution_count": 10, "id": "47c8c37c", "metadata": {}, "outputs": [], @@ -369,24 +370,110 @@ }, { "cell_type": "code", - "execution_count": 170, + "execution_count": 11, "id": "b741015f", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100% (129 of 129) |######################| Elapsed Time: 0:02:28 Time: 0:02:28\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ - "Saving 46 datasetTypes to /astro/users/coc123/kbmod_tmp/dataset_types.csv now...\n", - "CPU times: user 1min 57s, sys: 2.96 s, total: 2min\n", - "Wall time: 2min 28s\n" + "Recycling /astro/users/coc123/kbmod_tmp/dataset_types.csv as overwrite was False...\n", + "assembleCoadd_config,8\n", + "\n", + "assembleCoadd_log,268\n", + "\n", + "assembleCoadd_metadata,700\n", + "\n", + "cal_ref_cat,122856\n", + "\n", + "calexp,47403\n", + "\n", + "calexpBackground,47403\n", + "\n", + "calibrate_config,8\n", + "\n", + "calibrate_log,17961\n", + "\n", + "calibrate_metadata,47403\n", + "\n", + "characterizeImage_config,8\n", + "\n", + "characterizeImage_log,18290\n", + "\n", + "characterizeImage_metadata,47423\n", + "\n", + "deepCoadd,693\n", + "\n", + "deepCoadd_directWarp,167085\n", + "\n", + "deepCoadd_inputMap,693\n", + "\n", + "deepCoadd_psfMatchedWarp,167085\n", + "\n", + "deepDiff_diaSrc,47383\n", + "\n", + "deepDiff_diaSrc_schema,8\n", + "\n", + "deepDiff_differenceExp,47383\n", + "\n", + "deepDiff_warpedExp,29445\n", + "\n", + "gaia_DR1_v1,524283\n", + "\n", + "icExp,47423\n", + "\n", + "icExpBackground,47423\n", + "\n", + "icSrc,47423\n", + "\n", + "icSrc_schema,8\n", + "\n", + "imageDifference_config,8\n", + "\n", + "imageDifference_log,17942\n", + "\n", + "imageDifference_metadata,47383\n", + "\n", + "isr_config,8\n", + "\n", + "isr_log,18290\n", + "\n", + "isr_metadata,48422\n", + "\n", + "makeWarp_config,8\n", + "\n", + "makeWarp_log,64924\n", + "\n", + "makeWarp_metadata,167085\n", + "\n", + "overscanRaw,48422\n", + "\n", + "overscan_config,8\n", + "\n", + "overscan_log,18290\n", + "\n", + "overscan_metadata,48422\n", + "\n", + "packages,32\n", + "\n", + "postISRCCD,48422\n", + "\n", + "ps1_pv3_3pi_20170110,130924\n", + "\n", + "raw,48422\n", + "\n", + "skyMap,1\n", + "\n", + "src,47403\n", + "\n", + "srcMatch,47403\n", + "\n", + "src_schema,8\n", + "\n", + "Read 46 datasetTypes from disk.\n", + "CPU times: user 1.55 ms, sys: 1.26 ms, total: 2.81 ms\n", + "Wall time: 3.19 ms\n" ] } ], @@ -398,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 12, "id": "6406f04c", "metadata": {}, "outputs": [ @@ -462,12 +549,12 @@ "\n", "print(f\"Across all collections, we see the following numbers by datasetType: \")\n", "for dt in datasetTypes:\n", - " print(f\"{datasetTypes[dt]!s:10} {dt.name}\")" + " print(f\"{datasetTypes[dt]!s:10} {dt}\")" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 13, "id": "53a03c75", "metadata": {}, "outputs": [], @@ -492,7 +579,7 @@ }, { "cell_type": "code", - "execution_count": 608, + "execution_count": 14, "id": "c47e5588", "metadata": {}, "outputs": [], @@ -536,7 +623,7 @@ }, { "cell_type": "code", - "execution_count": 611, + "execution_count": 15, "id": "eb222e02", "metadata": {}, "outputs": [ @@ -544,8 +631,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 1.82 s, sys: 83.2 ms, total: 1.91 s\n", - "Wall time: 2.4 s\n" + "CPU times: user 2.23 s, sys: 122 ms, total: 2.35 s\n", + "Wall time: 2.87 s\n" ] } ], @@ -559,7 +646,7 @@ }, { "cell_type": "code", - "execution_count": 612, + "execution_count": 16, "id": "6e9462e1", "metadata": {}, "outputs": [ @@ -691,7 +778,7 @@ "[47383 rows x 3 columns]" ] }, - "execution_count": 612, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -702,7 +789,7 @@ }, { "cell_type": "code", - "execution_count": 610, + "execution_count": 17, "id": "e04e2f35", "metadata": {}, "outputs": [ @@ -720,7 +807,7 @@ }, { "cell_type": "code", - "execution_count": 559, + "execution_count": 18, "id": "08d5b443", "metadata": {}, "outputs": [ @@ -737,7 +824,7 @@ "visit_detector_region.RecordClass(instrument='DECam', detector=62, visit=946176, region=ConvexPolygon([UnitVector3d(0.9876086828694174, -0.13336028508776862, -0.08272922024438323), UnitVector3d(0.9873378171284917, -0.13332652431396907, -0.08595389916869185), UnitVector3d(0.9881047366097594, -0.12752395595185462, -0.08594573955553172), UnitVector3d(0.9883760335240734, -0.12755303452468866, -0.0827226676235914)]))" ] }, - "execution_count": 559, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -749,7 +836,7 @@ }, { "cell_type": "code", - "execution_count": 560, + "execution_count": 19, "id": "e28031c2", "metadata": {}, "outputs": [ @@ -759,7 +846,7 @@ "{instrument: 'DECam', detector: 62, visit: 946176}" ] }, - "execution_count": 560, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -771,7 +858,7 @@ }, { "cell_type": "code", - "execution_count": 561, + "execution_count": 20, "id": "761f3610", "metadata": {}, "outputs": [ @@ -794,7 +881,7 @@ }, { "cell_type": "code", - "execution_count": 562, + "execution_count": 21, "id": "5d6b2106", "metadata": {}, "outputs": [ @@ -804,7 +891,7 @@ "b'p\\xddnE\\x86}\\x9a\\xef?\\x0f\\xc3\\x84\\'\\xf3\\x11\\xc1\\xbf\\x80\\x8a_\\xff\\xbd-\\xb5\\xbf\\x04xUzE\\x98\\xef?\\x94\\x15\\xcf\\xf2\\xd7\\x10\\xc1\\xbf\\x9d\\xa9\\xe4!\\x13\\x01\\xb6\\xbf\\x1d_\\x18\\xd3\\x8d\\x9e\\xef?\\x80\\x87\"z\\xb4R\\xc0\\xbff\\x1d\\x9f<\\x8a\\x00\\xb6\\xbf\\xe4Z\\x84\\xc6\\xc6\\xa0\\xef?\\x1f\\x01\\xe5g\\xa8S\\xc0\\xbf\\xba\\xc9\\x14\\x10P-\\xb5\\xbf'" ] }, - "execution_count": 562, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -816,7 +903,7 @@ }, { "cell_type": "code", - "execution_count": 563, + "execution_count": 22, "id": "aa653a8b", "metadata": {}, "outputs": [ @@ -826,7 +913,7 @@ "False" ] }, - "execution_count": 563, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -849,7 +936,7 @@ }, { "cell_type": "code", - "execution_count": 587, + "execution_count": 23, "id": "acda18f4", "metadata": {}, "outputs": [ @@ -857,8 +944,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 323 ms, sys: 68 ms, total: 391 ms\n", - "Wall time: 582 ms\n" + "CPU times: user 544 ms, sys: 41.7 ms, total: 586 ms\n", + "Wall time: 649 ms\n" ] } ], @@ -874,7 +961,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "bb076b4b", "metadata": {}, "outputs": [], @@ -897,7 +984,7 @@ }, { "cell_type": "code", - "execution_count": 564, + "execution_count": 25, "id": "0228bb4d", "metadata": {}, "outputs": [], @@ -925,7 +1012,7 @@ }, { "cell_type": "code", - "execution_count": 613, + "execution_count": 26, "id": "6c3bb244", "metadata": {}, "outputs": [ @@ -935,8 +1022,8 @@ "text": [ "Found DECam. Adding to \"desired_instruments\" now.\n", "WARNING: we are not iterating over all rows to find instruments, just taking the first one.\n", - "CPU times: user 140 ms, sys: 19 ms, total: 159 ms\n", - "Wall time: 199 ms\n" + "CPU times: user 1.27 s, sys: 285 ms, total: 1.56 s\n", + "Wall time: 1.61 s\n" ] } ], @@ -957,7 +1044,7 @@ }, { "cell_type": "code", - "execution_count": 614, + "execution_count": 27, "id": "dfc37b54", "metadata": {}, "outputs": [ @@ -965,8 +1052,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 875 ms, sys: 108 ms, total: 983 ms\n", - "Wall time: 1.02 s\n" + "CPU times: user 1.16 s, sys: 135 ms, total: 1.3 s\n", + "Wall time: 1.33 s\n" ] } ], @@ -982,7 +1069,7 @@ }, { "cell_type": "code", - "execution_count": 529, + "execution_count": 28, "id": "789f1d42", "metadata": {}, "outputs": [ @@ -992,7 +1079,7 @@ "62" ] }, - "execution_count": 529, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1003,7 +1090,7 @@ }, { "cell_type": "code", - "execution_count": 530, + "execution_count": 29, "id": "351e8d03", "metadata": {}, "outputs": [ @@ -1013,7 +1100,7 @@ "'VR'" ] }, - "execution_count": 530, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1024,17 +1111,17 @@ }, { "cell_type": "code", - "execution_count": 531, + "execution_count": 30, "id": "1c1e5431", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 531, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" }, @@ -1056,7 +1143,7 @@ }, { "cell_type": "code", - "execution_count": 532, + "execution_count": 31, "id": "ba1b7ab6", "metadata": {}, "outputs": [ @@ -1069,7 +1156,7 @@ "Pixel Scale: 0.262593 arcsec/pixel" ] }, - "execution_count": 532, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -1081,7 +1168,7 @@ }, { "cell_type": "code", - "execution_count": 503, + "execution_count": 32, "id": "4270995e", "metadata": { "scrolled": true @@ -1311,7 +1398,7 @@ }, { "cell_type": "code", - "execution_count": 272, + "execution_count": 33, "id": "7bbc916e", "metadata": {}, "outputs": [], @@ -1322,7 +1409,7 @@ }, { "cell_type": "code", - "execution_count": 567, + "execution_count": 34, "id": "e0f8f8f9", "metadata": {}, "outputs": [], @@ -1404,7 +1491,7 @@ }, { "cell_type": "code", - "execution_count": 628, + "execution_count": 35, "id": "6dd2ea6a", "metadata": {}, "outputs": [ @@ -1413,8 +1500,8 @@ "output_type": "stream", "text": [ "Recycled 47383 paths from /astro/users/coc123/kbmod_tmp/uri_cache.lst as overwrite was False.\n", - "CPU times: user 45 ms, sys: 32.1 ms, total: 77.1 ms\n", - "Wall time: 76.7 ms\n" + "CPU times: user 32 ms, sys: 21.5 ms, total: 53.5 ms\n", + "Wall time: 53.7 ms\n" ] } ], @@ -1435,7 +1522,7 @@ }, { "cell_type": "code", - "execution_count": 617, + "execution_count": 36, "id": "eae8d366", "metadata": {}, "outputs": [ @@ -1445,7 +1532,7 @@ "'file:///epyc/users/smotherh/DEEP/PointingGroups/butler-repo/PointingGroup021/imdiff_r/20210723T174135Z/deepDiff_differenceExp/20190927/VR/VR_DECam_c0007_6300.0_2600.0/898286/deepDiff_differenceExp_DECam_VR_VR_DECam_c0007_6300_0_2600_0_898286_S29_PointingGroup021_imdiff_r_20210723T174135Z.fits'" ] }, - "execution_count": 617, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } @@ -1466,7 +1553,7 @@ }, { "cell_type": "code", - "execution_count": 619, + "execution_count": 37, "id": "df7ff31f", "metadata": {}, "outputs": [ @@ -1475,8 +1562,8 @@ "output_type": "stream", "text": [ "0 DateTime(\"2019-09-27T00:20:59.932016000\", TAI) 120.0 (351.3806941054, -5.2403083277)\n", - "CPU times: user 126 ms, sys: 12 ms, total: 138 ms\n", - "Wall time: 175 ms\n" + "CPU times: user 93.4 ms, sys: 11.5 ms, total: 105 ms\n", + "Wall time: 145 ms\n" ] } ], @@ -1492,7 +1579,7 @@ }, { "cell_type": "code", - "execution_count": 572, + "execution_count": 38, "id": "3ccadfaa", "metadata": {}, "outputs": [ @@ -1502,7 +1589,7 @@ "lsst.daf.base.dateTime.dateTime.DateTime" ] }, - "execution_count": 572, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -1514,7 +1601,7 @@ }, { "cell_type": "code", - "execution_count": 573, + "execution_count": 39, "id": "d73d8119", "metadata": {}, "outputs": [ @@ -1524,73 +1611,77 @@ "'2019-09-27T00:20:22.932'" ] }, - "execution_count": 573, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Let's convert to a plain string, UTC (handles 37 s offset).\n", - "t = Time(testing, format=\"isot\", scale=\"tai\")\n", + "t = Time(str(visitInfo.date).split('\"')[1], format=\"isot\", scale=\"tai\")\n", "str(t.utc)" ] }, { "cell_type": "code", - "execution_count": 574, - "id": "89822691", + "execution_count": 40, + "id": "4cb53994", "metadata": {}, "outputs": [], "source": [ - "def getTimestamps(dataIds, overwrite=False):\n", - " \"\"\"Get timestamps for a bunch of dataIds.\n", - " Convert the LSST/Butler TAI to UTC in the process.\n", - " Do this all in a chunked, multiprocessing way.\n", - " Takes about 3 minutes as of 2/1/2024 (Hayden DEEP).\n", - " BUT if we have the values cached, just read those instead, unless overwrite is True.\n", - " 2/1/2024 COC\n", - " \"\"\"\n", - " # thank you ChatGPT 4 for helping parallelize\n", + "# New parallel version with order-preservation redone 2/7/2024 COC\n", "\n", - " timestamps = []\n", "\n", - " import glob\n", + "# Define get_timestamps at the top level of your module\n", + "def get_timestamps(dataIds_chunk, repo_path, desired_collections):\n", + " chunked_data = []\n", + " butler = dafButler.Butler(repo_path)\n", + " for dataId in dataIds_chunk:\n", + " try:\n", + " visitInfo = butler.get(\"calexp.visitInfo\", dataId=dataId, collections=desired_collections)\n", + " t = Time(str(visitInfo.date).split('\"')[1], format=\"isot\", scale=\"tai\")\n", + " tutc = str(t.utc)\n", + " chunked_data.append(tutc)\n", + " except Exception as e:\n", + " print(f\"Failed to retrieve timestamp for dataId {dataId}: {e}\")\n", + " return chunked_data\n", "\n", - " cache_file = f\"{basedir}/vdr_timestamps.lst\"\n", "\n", - " cache_file_exists = False\n", - " if len(glob.glob(cache_file)) > 0:\n", - " cache_file_exists = True\n", + "def getTimestamps(dataIds, overwrite=False):\n", + " timestamps = []\n", + " cache_file = f\"{basedir}/vdr_timestamps.lst\"\n", "\n", - " if overwrite == False and cache_file_exists == True:\n", - " print(f\"Overwrite is False, so we will read the timestamps from file now...\")\n", + " if not overwrite and glob.glob(cache_file):\n", + " print(\"Overwrite is False, so we will read the timestamps from file now...\")\n", " with open(cache_file, \"r\") as f:\n", - " for line in f:\n", - " timestamps.append(line.strip())\n", + " timestamps = [line.strip() for line in f]\n", " print(f\"Recycled {len(timestamps)} from {cache_file}.\")\n", " return timestamps\n", "\n", - " if overwrite or not cache_file_exists:\n", - " timestamps = [] # Re-initialize timestamps here to ensure it's fresh\n", + " def chunked_dataIds(dataIds, chunk_size=200):\n", + " for i in range(0, len(dataIds), chunk_size):\n", + " yield dataIds[i : i + chunk_size]\n", "\n", - " with ProcessPoolExecutor() as executor:\n", - " dataId_chunks = list(chunked_dataIds(dataIds))\n", - " # Initialize progress bar\n", - " with progressbar.ProgressBar(max_value=len(dataId_chunks)) as bar:\n", - " # Use map for preserving order and simplifying the code\n", - " results = executor.map(get_timestamps, dataId_chunks)\n", + " dataId_chunks = list(chunked_dataIds(dataIds))\n", + "\n", + " with ProcessPoolExecutor() as executor:\n", + " # Adjust the executor.map call to pass additional arguments to get_timestamps\n", + " result_chunks = list(\n", + " executor.map(\n", + " get_timestamps,\n", + " dataId_chunks,\n", + " [repo_path] * len(dataId_chunks),\n", + " [desired_collections] * len(dataId_chunks),\n", + " )\n", + " )\n", "\n", - " # Process results and maintain the order\n", - " for i, chunk_result in enumerate(results):\n", - " timestamps.extend(chunk_result) # Correctly extend with the result of each future\n", - " bar.update(i)\n", + " timestamps = [timestamp for chunk in result_chunks for timestamp in chunk]\n", "\n", - " # Write to cache if necessary\n", - " if overwrite or not cache_file_exists:\n", - " with open(cache_file, \"w\") as f:\n", - " for ts in timestamps:\n", - " print(ts, file=f)\n", - " print(f\"Wrote {len(timestamps)} lines to {cache_file} for future use.\")\n", + " if overwrite or not glob.glob(cache_file):\n", + " with open(cache_file, \"w\") as f:\n", + " for ts in timestamps:\n", + " print(ts, file=f)\n", + " print(f\"Wrote {len(timestamps)} lines to {cache_file} for future use.\")\n", "\n", " print(f\"Obtained {len(timestamps)} timestamps.\")\n", " return timestamps" @@ -1598,32 +1689,7 @@ }, { "cell_type": "code", - "execution_count": 620, - "id": "5ea2f086", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2019-09-27T00:20:22.932'" - ] - }, - "execution_count": 620, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Double-check that we can convert Butler timestamp (TAI) to UTC string\n", - "visitInfo = butler.get(\"calexp.visitInfo\", dataId=df[\"data_id\"].iloc()[0], collections=desired_collections)\n", - "t = Time(str(visitInfo.date).split('\"')[1], format=\"isot\", scale=\"tai\")\n", - "tutc = str(t.utc)\n", - "tutc" - ] - }, - { - "cell_type": "code", - "execution_count": 621, + "execution_count": 41, "id": "7e0d33c0", "metadata": {}, "outputs": [ @@ -1633,8 +1699,8 @@ "text": [ "Overwrite is False, so we will read the timestamps from file now...\n", "Recycled 47383 from /astro/users/coc123/kbmod_tmp/vdr_timestamps.lst.\n", - "CPU times: user 24.1 ms, sys: 9.07 ms, total: 33.2 ms\n", - "Wall time: 30.1 ms\n" + "CPU times: user 16.8 ms, sys: 5.06 ms, total: 21.9 ms\n", + "Wall time: 21.4 ms\n" ] } ], @@ -1647,7 +1713,7 @@ }, { "cell_type": "code", - "execution_count": 622, + "execution_count": 42, "id": "adddf1e4", "metadata": {}, "outputs": [ @@ -1668,7 +1734,7 @@ "Name: ut, Length: 47383, dtype: object" ] }, - "execution_count": 622, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -1679,65 +1745,10 @@ }, { "cell_type": "code", - "execution_count": 364, + "execution_count": 43, "id": "a789a331", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 : 2019-09-27T00:20:22.932 for {instrument: 'DECam', detector: 1, visit: 898286}. Dict had: 2019-09-27\n", - "1000 : 2019-09-27T02:24:03.066 for {instrument: 'DECam', detector: 12, visit: 898336}. Dict had: 2019-08-29\n", - "2000 : 2019-09-27T00:32:44.405 for {instrument: 'DECam', detector: 23, visit: 898291}. Dict had: 2019-08-29\n", - "3000 : 2019-09-27T02:36:24.497 for {instrument: 'DECam', detector: 33, visit: 898341}. Dict had: 2019-08-29\n", - "4000 : 2019-09-27T00:45:05.306 for {instrument: 'DECam', detector: 44, visit: 898296}. Dict had: 2019-08-29\n", - "5000 : 2019-09-27T02:51:16.295 for {instrument: 'DECam', detector: 54, visit: 898347}. Dict had: 2019-08-29\n", - "6000 : 2019-08-29T07:25:55.714 for {instrument: 'DECam', detector: 4, visit: 891512}. Dict had: 2019-08-30\n", - "7000 : 2019-08-29T06:36:25.673 for {instrument: 'DECam', detector: 14, visit: 891492}. Dict had: 2019-08-30\n", - "8000 : 2019-08-29T05:46:48.259 for {instrument: 'DECam', detector: 24, visit: 891472}. Dict had: 2019-08-29\n", - "9000 : 2019-08-29T09:11:52.823 for {instrument: 'DECam', detector: 33, visit: 891554}. Dict had: 2020-10-19\n", - "10000: 2019-08-29T08:24:43.475 for {instrument: 'DECam', detector: 43, visit: 891535}. Dict had: 2020-10-19\n", - "11000: 2019-08-29T07:39:44.133 for {instrument: 'DECam', detector: 53, visit: 891517}. Dict had: 2020-10-19\n", - "12000: 2020-10-19T03:57:38.040 for {instrument: 'DECam', detector: 1, visit: 946776}. Dict had: 2020-10-19\n", - "13000: 2020-10-19T03:55:07.913 for {instrument: 'DECam', detector: 13, visit: 946775}. Dict had: 2020-10-19\n", - "14000: 2020-10-19T03:52:39.901 for {instrument: 'DECam', detector: 24, visit: 946774}. Dict had: 2020-10-19\n", - "15000: 2020-10-19T03:50:11.828 for {instrument: 'DECam', detector: 35, visit: 946773}. Dict had: 2019-09-27\n", - "16000: 2020-10-19T03:47:43.087 for {instrument: 'DECam', detector: 46, visit: 946772}. Dict had: 2019-09-27\n", - "17000: 2020-10-19T03:45:14.564 for {instrument: 'DECam', detector: 57, visit: 946771}. Dict had: 2019-09-27\n", - "18000: 2019-08-30T07:36:47.123 for {instrument: 'DECam', detector: 7, visit: 891898}. Dict had: 2019-09-27\n", - "19000: 2019-08-30T07:11:50.193 for {instrument: 'DECam', detector: 17, visit: 891888}. Dict had: 2019-09-27\n", - "20000: 2019-08-30T06:47:05.343 for {instrument: 'DECam', detector: 27, visit: 891878}. Dict had: 2019-08-30\n", - "21000: 2019-08-30T06:22:19.105 for {instrument: 'DECam', detector: 37, visit: 891868}. Dict had: 2019-08-30\n", - "22000: 2019-08-30T05:57:29.152 for {instrument: 'DECam', detector: 47, visit: 891858}. Dict had: 2019-08-30\n", - "23000: 2019-08-30T05:32:32.635 for {instrument: 'DECam', detector: 57, visit: 891848}. Dict had: 2019-08-30\n", - "24000: 2019-09-28T01:46:11.541 for {instrument: 'DECam', detector: 6, visit: 898736}. Dict had: 2019-09-28\n", - "25000: 2019-09-28T03:00:31.172 for {instrument: 'DECam', detector: 16, visit: 898766}. Dict had: 2019-09-28\n", - "26000: 2019-09-28T00:17:03.748 for {instrument: 'DECam', detector: 27, visit: 898700}. Dict had: 2019-09-28\n", - "27000: 2019-09-28T01:31:18.723 for {instrument: 'DECam', detector: 37, visit: 898730}. Dict had: 2019-09-28\n", - "28000: 2019-09-28T02:45:39.745 for {instrument: 'DECam', detector: 47, visit: 898760}. Dict had: 2019-09-28\n", - "29000: 2019-09-28T04:00:09.666 for {instrument: 'DECam', detector: 57, visit: 898790}. Dict had: 2019-08-28\n", - "30000: 2019-08-28T06:43:48.523 for {instrument: 'DECam', detector: 7, visit: 891114}. Dict had: 2019-08-28\n", - "31000: 2019-08-28T05:29:20.494 for {instrument: 'DECam', detector: 17, visit: 891084}. Dict had: 2019-08-28\n", - "32000: 2019-08-28T08:31:09.143 for {instrument: 'DECam', detector: 26, visit: 891157}. Dict had: 2019-08-28\n", - "33000: 2019-08-28T07:21:26.793 for {instrument: 'DECam', detector: 36, visit: 891129}. Dict had: 2019-08-28\n", - "34000: 2019-08-28T06:06:34.210 for {instrument: 'DECam', detector: 46, visit: 891099}. Dict had: 2019-08-28\n", - "35000: 2019-08-28T09:08:44.560 for {instrument: 'DECam', detector: 55, visit: 891172}. Dict had: 2019-09-28\n", - "36000: 2019-09-29T00:19:06.349 for {instrument: 'DECam', detector: 5, visit: 899020}. Dict had: 2019-09-29\n", - "37000: 2019-09-29T01:58:29.441 for {instrument: 'DECam', detector: 15, visit: 899060}. Dict had: 2019-09-29\n", - "38000: 2019-09-29T03:37:44.590 for {instrument: 'DECam', detector: 25, visit: 899100}. Dict had: 2019-09-29\n", - "39000: 2019-09-29T01:18:42.912 for {instrument: 'DECam', detector: 36, visit: 899044}. Dict had: 2019-09-29\n", - "40000: 2019-09-29T02:58:02.739 for {instrument: 'DECam', detector: 46, visit: 899084}. Dict had: 2019-09-29\n", - "41000: 2019-09-29T00:38:58.257 for {instrument: 'DECam', detector: 57, visit: 899028}. Dict had: 2019-09-29\n", - "42000: 2020-10-17T03:45:59.925 for {instrument: 'DECam', detector: 6, visit: 946166}. Dict had: 2020-10-17\n", - "43000: 2020-10-17T01:26:44.472 for {instrument: 'DECam', detector: 17, visit: 946110}. Dict had: 2020-10-17\n", - "44000: 2020-10-17T03:06:21.338 for {instrument: 'DECam', detector: 27, visit: 946150}. Dict had: 2020-10-17\n", - "45000: 2020-10-17T00:46:58.038 for {instrument: 'DECam', detector: 38, visit: 946094}. Dict had: 2020-10-17\n", - "46000: 2020-10-17T02:26:33.210 for {instrument: 'DECam', detector: 48, visit: 946134}. Dict had: 2020-10-17\n", - "47000: 2020-10-17T04:05:48.949 for {instrument: 'DECam', detector: 58, visit: 946174}. Dict had: 2020-10-17\n" - ] - } - ], + "outputs": [], "source": [ "# This is for coming back to later to make sure the stamps line up\n", "# IGNORE FOR NOW 2/6/2024 COC\n", @@ -1770,7 +1781,7 @@ }, { "cell_type": "code", - "execution_count": 578, + "execution_count": 44, "id": "e45d7199", "metadata": {}, "outputs": [], @@ -1793,7 +1804,7 @@ }, { "cell_type": "code", - "execution_count": 579, + "execution_count": 45, "id": "9b278b50", "metadata": {}, "outputs": [ @@ -1806,7 +1817,7 @@ " (352.64644359666477, -4.7450820235469715)]" ] }, - "execution_count": 579, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } @@ -1819,7 +1830,7 @@ }, { "cell_type": "code", - "execution_count": 580, + "execution_count": 46, "id": "b37bfe47", "metadata": {}, "outputs": [], @@ -1842,7 +1853,7 @@ }, { "cell_type": "code", - "execution_count": 581, + "execution_count": 47, "id": "ef329736", "metadata": {}, "outputs": [ @@ -1853,7 +1864,7 @@ " (-4.930880058593892, -4.7450820235469715))" ] }, - "execution_count": 581, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -1876,7 +1887,7 @@ }, { "cell_type": "code", - "execution_count": 582, + "execution_count": 48, "id": "4c89be9e", "metadata": {}, "outputs": [], @@ -1895,7 +1906,7 @@ }, { "cell_type": "code", - "execution_count": 182, + "execution_count": 49, "id": "8e1e764c", "metadata": {}, "outputs": [ @@ -1905,18 +1916,18 @@ "(352.477974357993, -4.837981041070432)" ] }, - "execution_count": 182, + "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "getCenterRaDec(tmpref.region)" + "getCenterRaDec(example_vdr_ref.region)" ] }, { "cell_type": "code", - "execution_count": 624, + "execution_count": 50, "id": "8e54db29", "metadata": {}, "outputs": [ @@ -1924,8 +1935,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 363 ms, sys: 19 ms, total: 382 ms\n", - "Wall time: 379 ms\n" + "CPU times: user 403 ms, sys: 15.8 ms, total: 419 ms\n", + "Wall time: 419 ms\n" ] } ], @@ -1936,7 +1947,7 @@ }, { "cell_type": "code", - "execution_count": 625, + "execution_count": 51, "id": "64908868", "metadata": {}, "outputs": [ @@ -1946,7 +1957,7 @@ "47383" ] }, - "execution_count": 625, + "execution_count": 51, "metadata": {}, "output_type": "execute_result" } @@ -1957,7 +1968,7 @@ }, { "cell_type": "code", - "execution_count": 626, + "execution_count": 52, "id": "625de417", "metadata": {}, "outputs": [ @@ -1967,7 +1978,7 @@ "(351.0694028401149, -4.336598368890197)" ] }, - "execution_count": 626, + "execution_count": 52, "metadata": {}, "output_type": "execute_result" } @@ -1978,7 +1989,7 @@ }, { "cell_type": "code", - "execution_count": 627, + "execution_count": 53, "id": "67b57215", "metadata": {}, "outputs": [ @@ -1986,17 +1997,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 1.2 s, sys: 26 ms, total: 1.23 s\n", - "Wall time: 1.22 s\n" + "CPU times: user 1.62 s, sys: 71.3 ms, total: 1.69 s\n", + "Wall time: 1.72 s\n" ] }, { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 627, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" }, @@ -2035,7 +2046,7 @@ }, { "cell_type": "code", - "execution_count": 554, + "execution_count": 54, "id": "c2dc4e1a", "metadata": {}, "outputs": [ @@ -2045,7 +2056,7 @@ "{instrument: 'DECam', detector: 1, visit: 898286}" ] }, - "execution_count": 554, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -2057,17 +2068,17 @@ }, { "cell_type": "code", - "execution_count": 435, + "execution_count": 55, "id": "580ac4ed", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 435, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" }, @@ -2093,7 +2104,7 @@ }, { "cell_type": "code", - "execution_count": 237, + "execution_count": 56, "id": "6ee544b8", "metadata": {}, "outputs": [], @@ -2116,7 +2127,7 @@ }, { "cell_type": "code", - "execution_count": 589, + "execution_count": 57, "id": "2c770982", "metadata": {}, "outputs": [], @@ -2162,7 +2173,7 @@ }, { "cell_type": "code", - "execution_count": 202, + "execution_count": 58, "id": "2fcdd8b3", "metadata": {}, "outputs": [], @@ -2203,7 +2214,7 @@ }, { "cell_type": "code", - "execution_count": 204, + "execution_count": 59, "id": "c0916372", "metadata": {}, "outputs": [ @@ -2220,18 +2231,18 @@ "(189361, 1895.111766130883)" ] }, - "execution_count": 204, + "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "getHTMstuff(vdr_centers[0][0], vdr_centers[0][1], verbose=True)" + "getHTMstuff(df[\"center_coord\"].iloc()[0][0], df[\"center_coord\"].iloc()[0][1], verbose=True)" ] }, { "cell_type": "code", - "execution_count": 588, + "execution_count": 60, "id": "47c447c5", "metadata": {}, "outputs": [ @@ -2270,7 +2281,7 @@ }, { "cell_type": "code", - "execution_count": 441, + "execution_count": 61, "id": "048e38e1", "metadata": {}, "outputs": [], @@ -2349,7 +2360,7 @@ }, { "cell_type": "code", - "execution_count": 442, + "execution_count": 62, "id": "25710a99", "metadata": {}, "outputs": [ @@ -2358,8 +2369,8 @@ "output_type": "stream", "text": [ "Recycling /astro/users/coc123/kbmod_tmp/overlapping_sets.pickle as overwrite=False.\n", - "CPU times: user 173 ms, sys: 22.1 ms, total: 195 ms\n", - "Wall time: 192 ms\n" + "CPU times: user 425 ms, sys: 134 ms, total: 559 ms\n", + "Wall time: 559 ms\n" ] } ], @@ -2374,7 +2385,7 @@ }, { "cell_type": "code", - "execution_count": 590, + "execution_count": 63, "id": "5f2d5a03", "metadata": {}, "outputs": [ @@ -2390,9 +2401,17 @@ "print(f\"There are {len(overlapping_sets.keys())} discrete chip-level pointings.\") # should be 488" ] }, + { + "cell_type": "markdown", + "id": "4e362e41", + "metadata": {}, + "source": [ + "#### Exploring the overlapping_sets" + ] + }, { "cell_type": "code", - "execution_count": 591, + "execution_count": 64, "id": "9ed24e28", "metadata": {}, "outputs": [ @@ -2400,17 +2419,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 503 ms, sys: 24.9 ms, total: 527 ms\n", - "Wall time: 526 ms\n" + "CPU times: user 481 ms, sys: 18 ms, total: 499 ms\n", + "Wall time: 503 ms\n" ] }, { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 591, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" }, @@ -2443,7 +2462,7 @@ }, { "cell_type": "code", - "execution_count": 594, + "execution_count": 65, "id": "55ffd374", "metadata": {}, "outputs": [ @@ -2453,7 +2472,7 @@ "'2019-09-27T00:20:22.932'" ] }, - "execution_count": 594, + "execution_count": 65, "metadata": {}, "output_type": "execute_result" } @@ -2464,7 +2483,7 @@ }, { "cell_type": "code", - "execution_count": 595, + "execution_count": 66, "id": "fcd39c41", "metadata": {}, "outputs": [ @@ -2472,15 +2491,15 @@ "name": "stderr", "output_type": "stream", "text": [ - ":19: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed two minor releases later. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap(obj)`` instead.\n" + ":17: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed two minor releases later. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap(obj)`` instead.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 6.24 s, sys: 526 ms, total: 6.77 s\n", - "Wall time: 6.24 s\n" + "CPU times: user 9.24 s, sys: 745 ms, total: 9.98 s\n", + "Wall time: 9.51 s\n" ] }, { @@ -2536,19 +2555,19 @@ }, { "cell_type": "code", - "execution_count": 596, + "execution_count": 67, "id": "418c97ee", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Index(['data_id', 'region', 'detector', 'uri', 'center_coord', 'ut',\n", + "Index(['data_id', 'region', 'detector', 'uri', 'ut', 'center_coord',\n", " 'ut_datetime'],\n", " dtype='object')" ] }, - "execution_count": 596, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } @@ -2559,7 +2578,7 @@ }, { "cell_type": "code", - "execution_count": 597, + "execution_count": 68, "id": "d06ed515", "metadata": {}, "outputs": [ @@ -2576,7 +2595,7 @@ " datetime.date(2020, 10, 19)]" ] }, - "execution_count": 597, + "execution_count": 68, "metadata": {}, "output_type": "execute_result" } @@ -2597,7 +2616,7 @@ }, { "cell_type": "code", - "execution_count": 598, + "execution_count": 69, "id": "58158f47", "metadata": {}, "outputs": [ @@ -2608,13 +2627,13 @@ "region ConvexPolygon([UnitVector3d(0.9847372525065534...\n", "detector 1\n", "uri file:///epyc/users/smotherh/DEEP/PointingGroup...\n", - "center_coord (351.0694028401149, -4.336598368890197)\n", "ut 2019-09-27T00:20:22.932\n", + "center_coord (351.0694028401149, -4.336598368890197)\n", "ut_datetime 2019-09-27 00:20:22.932000\n", "Name: 0, dtype: object" ] }, - "execution_count": 598, + "execution_count": 69, "metadata": {}, "output_type": "execute_result" } @@ -2625,7 +2644,7 @@ }, { "cell_type": "code", - "execution_count": 599, + "execution_count": 70, "id": "2ff625e7", "metadata": {}, "outputs": [ @@ -2635,7 +2654,7 @@ "datetime.date(2019, 9, 27)" ] }, - "execution_count": 599, + "execution_count": 70, "metadata": {}, "output_type": "execute_result" } @@ -2646,7 +2665,7 @@ }, { "cell_type": "code", - "execution_count": 600, + "execution_count": 71, "id": "c3b48b13", "metadata": {}, "outputs": [ @@ -2656,7 +2675,7 @@ "(351.0694028401149, -4.336598368890197)" ] }, - "execution_count": 600, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } @@ -2667,7 +2686,7 @@ }, { "cell_type": "code", - "execution_count": 601, + "execution_count": 72, "id": "16506478", "metadata": {}, "outputs": [ @@ -2677,7 +2696,7 @@ "6267" ] }, - "execution_count": 601, + "execution_count": 72, "metadata": {}, "output_type": "execute_result" } @@ -2690,7 +2709,7 @@ }, { "cell_type": "code", - "execution_count": 603, + "execution_count": 73, "id": "166c9137", "metadata": {}, "outputs": [ @@ -2698,8 +2717,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 933 ms, sys: 33 ms, total: 966 ms\n", - "Wall time: 962 ms\n" + "CPU times: user 1.03 s, sys: 19 ms, total: 1.05 s\n", + "Wall time: 1.06 s\n" ] }, { @@ -2794,6 +2813,22 @@ "del tmpdf" ] }, + { + "cell_type": "code", + "execution_count": 77, + "id": "bfb23aef", + "metadata": {}, + "outputs": [], + "source": [ + "def ra_dec_search_overlapping_sets(df, overlapping_sets):\n", + " \"\"\"\n", + " 2/6/2024 COC\n", + " Implementing an extremely basic (RA, Dec) query functionality.\n", + " This will work within the overlapping_sets framework.\n", + " \"\"\"\n", + " pass" + ] + }, { "cell_type": "markdown", "id": "337ceedc", @@ -2807,7 +2842,7 @@ }, { "cell_type": "code", - "execution_count": 634, + "execution_count": 75, "id": "fccdee6b", "metadata": {}, "outputs": [], @@ -2823,6 +2858,7 @@ " import lsst\n", " import lsst.daf.butler as dafButler\n", " import os\n", + " import glob\n", " import time\n", " from matplotlib import pyplot as plt\n", " import progressbar\n", @@ -2869,7 +2905,7 @@ }, { "cell_type": "code", - "execution_count": 638, + "execution_count": 76, "id": "cf221f77", "metadata": {}, "outputs": [ @@ -2885,8 +2921,8 @@ "Overwrite is False, so we will read the timestamps from file now...\n", "Recycled 47383 from /astro/users/coc123/kbmod_tmp/vdr_timestamps.lst.\n", "Recycling /astro/users/coc123/kbmod_tmp/overlapping_sets.pickle as overwrite=False.\n", - "CPU times: user 3.91 s, sys: 413 ms, total: 4.32 s\n", - "Wall time: 5.54 s\n" + "CPU times: user 4.44 s, sys: 465 ms, total: 4.91 s\n", + "Wall time: 6.28 s\n" ] } ], From a2a2a030167649999322984260476670d17ae13f Mon Sep 17 00:00:00 2001 From: Jeremy Kubica <104161096+jeremykubica@users.noreply.github.com> Date: Thu, 8 Feb 2024 10:28:32 -0500 Subject: [PATCH 27/27] Add some basic checks --- src/kbmod/search/psf.cpp | 7 ++++++- src/kbmod/search/pydocs/psf_docs.h | 18 +++++++++++------- tests/test_psf.py | 8 ++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/kbmod/search/psf.cpp b/src/kbmod/search/psf.cpp index 0ec947b8b..fa2735cbb 100644 --- a/src/kbmod/search/psf.cpp +++ b/src/kbmod/search/psf.cpp @@ -9,6 +9,10 @@ PSF::PSF() : kernel(1, 1.0) { } PSF::PSF(float stdev) { + if (stdev <= 0.0) { + throw std::runtime_error("PSF stdev must be > 0.0."); + } + width = stdev; float simple_gauss[MAX_KERNEL_RADIUS]; double psf_coverage = 0.0; @@ -28,7 +32,7 @@ PSF::PSF(float stdev) { i++; } - radius = i - 1; // This value is good for + radius = i - 1; dim = 2 * radius + 1; // Create 2D gaussain by multiplying with itself @@ -155,6 +159,7 @@ static void psf_bindings(py::module& m) { .def(py::init()) .def(py::init>()) .def(py::init()) + .def("__str__", &psf::print) .def("set_array", &psf::set_array, pydocs::DOC_PSF_set_array) .def("get_std", &psf::get_std, pydocs::DOC_PSF_get_std) .def("get_sum", &psf::get_sum, pydocs::DOC_PSF_get_sum) diff --git a/src/kbmod/search/pydocs/psf_docs.h b/src/kbmod/search/pydocs/psf_docs.h index 91ea88399..a58fb07f4 100644 --- a/src/kbmod/search/pydocs/psf_docs.h +++ b/src/kbmod/search/pydocs/psf_docs.h @@ -8,18 +8,22 @@ static const auto DOC_PSF = R"doc( Parameters ---------- stdev : `float`, optional - Standard deviation of the Gaussian PSF. + Standard deviation of the Gaussian PSF. Must be > 0.0. psf : `PSF`, optional Another PSF object. arr : `numpy.array`, optional A realization of the PSF. - Notes - ----- - When instantiated with another `psf` object, returns its copy. - When instantiated with an array-like object, that array must be - a square matrix and have an odd number of dimensions. Only one - of the arguments is required. + Notes + ----- + When instantiated with another `psf` object, returns its copy. + When instantiated with an array-like object, that array must be + a square matrix and have an odd number of dimensions. Only one + of the arguments is required. + + Raises + ------ + Raises a ``RuntimeError`` when given an invalid stdev. )doc"; static const auto DOC_PSF_set_array = R"doc( diff --git a/tests/test_psf.py b/tests/test_psf.py index f8cecb58e..a7eba0ea9 100644 --- a/tests/test_psf.py +++ b/tests/test_psf.py @@ -19,6 +19,14 @@ def test_make_noop(self): self.assertEqual(len(kernel0), 1) self.assertEqual(kernel0[0], 1.0) + def test_make_invalud(self): + # Raise an error if creating a PSF with a negative stdev. + self.assertRaises(RuntimeError, PSF, -1.0) + + def test_to_string(self): + result = self.psf_list[0].__str__() + self.assertGreater(len(result), 1) + def test_make_and_copy(self): psf1 = PSF(1.0) self.assertEqual(psf1.get_size(), 25)