From ef2a7ef35da36ef5082d976dd12b2f1e237686e6 Mon Sep 17 00:00:00 2001 From: dahlend Date: Fri, 3 May 2024 17:17:50 -0700 Subject: [PATCH 01/12] neos visits --- src/neospy_core/src/fov/contiguous_fov.rs | 421 ------------------ src/neospy_core/src/fov/generic.rs | 168 +++++++ src/neospy_core/src/fov/mod.rs | 18 +- src/neospy_core/src/fov/neos.rs | 208 +++++++++ src/neospy_core/src/fov/wise.rs | 70 +++ .../src/fov/{joint_fov.rs => ztf.rs} | 97 +++- 6 files changed, 553 insertions(+), 429 deletions(-) delete mode 100644 src/neospy_core/src/fov/contiguous_fov.rs create mode 100644 src/neospy_core/src/fov/generic.rs create mode 100644 src/neospy_core/src/fov/neos.rs create mode 100644 src/neospy_core/src/fov/wise.rs rename src/neospy_core/src/fov/{joint_fov.rs => ztf.rs} (54%) diff --git a/src/neospy_core/src/fov/contiguous_fov.rs b/src/neospy_core/src/fov/contiguous_fov.rs deleted file mode 100644 index ce8125a..0000000 --- a/src/neospy_core/src/fov/contiguous_fov.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! # Definitions of contiguous field of views -//! These field of views are made up of single contiguous patches of sky, typically single image sensors. - -use std::fmt::Debug; - -use nalgebra::Vector3; -use serde::{Deserialize, Serialize}; - -use super::{Contains, FovLike, OnSkyRectangle, SkyPatch, SphericalCone, FOV}; -use crate::{ - constants::{NEOS_HEIGHT, NEOS_WIDTH, WISE_WIDTH}, - state::State, -}; - -/// WISE or NEOWISE frame data, all bands -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct WiseCmos { - /// State of the observer - observer: State, - - /// Patch of sky - pub patch: OnSkyRectangle, - - /// Rotation of the FOV. - pub rotation: f64, - - /// Frame number of the fov - pub frame_num: usize, - - /// Scan ID of the fov - pub scan_id: Box, -} - -impl WiseCmos { - /// Create a Wise fov - pub fn new( - pointing: Vector3, - rotation: f64, - observer: State, - frame_num: usize, - scan_id: Box, - ) -> Self { - let patch = OnSkyRectangle::new(pointing, rotation, WISE_WIDTH, WISE_WIDTH, observer.frame); - Self { - patch, - observer, - frame_num, - rotation, - scan_id, - } - } -} - -impl FovLike for WiseCmos { - #[inline] - fn get_fov(&self, index: usize) -> FOV { - if index != 0 { - panic!("Wise FOV only has a single patch") - } - FOV::Wise(self.clone()) - } - - #[inline] - fn observer(&self) -> &State { - &self.observer - } - - #[inline] - fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { - (0, self.patch.contains(obs_to_obj)) - } - - #[inline] - fn n_patches(&self) -> usize { - 1 - } -} - -/// NEOS frame data, a single detector on a single band -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct NeosCmos { - /// State of the observer - observer: State, - - /// Patch of sky - pub patch: OnSkyRectangle, - - /// Rotation of the FOV. - pub rotation: f64, - - /// Side ID - pub side_id: u16, - - /// Stack ID - pub stack_id: u8, - - /// Quad ID - pub quad_id: u8, - - /// Loop ID - pub loop_id: u8, - - /// Subloop ID - pub subloop_id: u8, - - /// Exposure ID - pub exposure_id: u8, - - /// Wavelength band, either 1 or 2 for NC1 or NC2 - pub band: u8, - - /// CMOS ID - /// ID number of the CMOS chip, 0, 1, 2, or 3 - pub cmos_id: u8, -} - -impl NeosCmos { - /// Create a NEOS FOV - #[allow(clippy::too_many_arguments)] - pub fn new( - pointing: Vector3, - rotation: f64, - observer: State, - side_id: u16, - stack_id: u8, - quad_id: u8, - loop_id: u8, - subloop_id: u8, - exposure_id: u8, - cmos_id: u8, - band: u8, - ) -> Self { - let patch = - OnSkyRectangle::new(pointing, rotation, NEOS_WIDTH, NEOS_HEIGHT, observer.frame); - Self { - observer, - patch, - side_id, - stack_id, - quad_id, - loop_id, - subloop_id, - exposure_id, - cmos_id, - band, - rotation, - } - } -} - -impl FovLike for NeosCmos { - fn get_fov(&self, index: usize) -> FOV { - if index != 0 { - panic!("FOV only has a single patch") - } - FOV::NeosCmos(self.clone()) - } - - #[inline] - fn observer(&self) -> &State { - &self.observer - } - - #[inline] - fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { - (0, self.patch.contains(obs_to_obj)) - } - - #[inline] - fn n_patches(&self) -> usize { - 1 - } -} - -/// ZTF frame data, single quad of a single chip -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ZtfCcdQuad { - /// State of the observer - observer: State, - - /// Patch of sky - pub patch: OnSkyRectangle, - - /// Field ID - pub field: u32, - - /// File Frac Day - /// String representation of the filename for this frame. - pub filefracday: u64, - - /// Magnitude limit of this frame - pub maglimit: f64, - - /// Filter ID - pub fid: usize, - - /// Filter code used for the frame - pub filtercode: Box, - - /// Image Type Code - pub imgtypecode: Box, - - /// Which CCID was the frame taken with - pub ccdid: u8, - - /// Quadrant ID - pub qid: u8, -} - -impl ZtfCcdQuad { - /// Create a ZTF field of view - #[allow(clippy::too_many_arguments)] - pub fn new( - corners: [Vector3; 4], - observer: State, - field: u32, - filefracday: u64, - ccdid: u8, - filtercode: Box, - imgtypecode: Box, - qid: u8, - maglimit: f64, - fid: usize, - ) -> Self { - let patch = OnSkyRectangle::from_corners(corners, observer.frame); - Self { - patch, - observer, - field, - filefracday, - ccdid, - filtercode, - imgtypecode, - qid, - maglimit, - fid, - } - } -} - -impl FovLike for ZtfCcdQuad { - fn get_fov(&self, index: usize) -> FOV { - if index != 0 { - panic!("FOV only has a single patch") - } - FOV::ZtfCcdQuad(self.clone()) - } - - #[inline] - fn observer(&self) -> &State { - &self.observer - } - - #[inline] - fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { - (0, self.patch.contains(obs_to_obj)) - } - - #[inline] - fn n_patches(&self) -> usize { - 1 - } -} - -/// Generic rectangular FOV -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct GenericRectangle { - observer: State, - - /// Patch of sky - pub patch: OnSkyRectangle, - - /// Rotation of the FOV. - pub rotation: f64, -} - -impl GenericRectangle { - /// Create a new Generic Rectangular FOV - pub fn new( - pointing: Vector3, - rotation: f64, - lon_width: f64, - lat_width: f64, - observer: State, - ) -> Self { - let patch = OnSkyRectangle::new(pointing, rotation, lon_width, lat_width, observer.frame); - Self { - observer, - patch, - rotation, - } - } - - /// Latitudinal width of the FOV. - #[inline] - pub fn lat_width(&self) -> f64 { - self.patch.lat_width() - } - - /// Longitudinal width of the FOV. - #[inline] - pub fn lon_width(&self) -> f64 { - self.patch.lon_width() - } -} - -impl FovLike for GenericRectangle { - #[inline] - fn get_fov(&self, index: usize) -> FOV { - if index != 0 { - panic!("FOV only has a single patch") - } - FOV::GenericRectangle(self.clone()) - } - - #[inline] - fn observer(&self) -> &State { - &self.observer - } - - #[inline] - fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { - (0, self.patch.contains(obs_to_obj)) - } - - #[inline] - fn n_patches(&self) -> usize { - 1 - } -} - -/// Generic rectangular FOV -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct GenericCone { - observer: State, - - /// Patch of sky - pub patch: SphericalCone, -} -impl GenericCone { - /// Create a new Generic Conic FOV - pub fn new(pointing: Vector3, angle: f64, observer: State) -> Self { - let patch = SphericalCone::new(&pointing, angle, observer.frame); - Self { observer, patch } - } -} - -impl FovLike for GenericCone { - #[inline] - fn get_fov(&self, index: usize) -> FOV { - if index != 0 { - panic!("FOV only has a single patch") - } - FOV::GenericCone(self.clone()) - } - - #[inline] - fn observer(&self) -> &State { - &self.observer - } - - #[inline] - fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { - (0, self.patch.contains(obs_to_obj)) - } - - #[inline] - fn n_patches(&self) -> usize { - 1 - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::GMS_SQRT; - use crate::prelude::*; - use crate::state::Desig; - - #[test] - fn test_check_visible() { - let circular = State::new( - Desig::Empty, - 2451545.0, - [0.0, 1., 0.0].into(), - [-GMS_SQRT, 0.0, 0.0].into(), - Frame::Ecliptic, - 0, - ); - let circular_back = State::new( - Desig::Empty, - 2451545.0, - [1.0, 0.0, 0.0].into(), - [0.0, GMS_SQRT, 0.0].into(), - Frame::Ecliptic, - 0, - ); - - for offset in [-10.0, -5.0, 0.0, 5.0, 10.0] { - let off_state = propagate_n_body_spk( - circular_back.clone(), - circular_back.jd - offset, - false, - None, - ) - .unwrap(); - - let vec = Vector3::from(circular_back.pos) - Vector3::from(circular.pos); - - let fov = GenericRectangle::new(vec, 0.0001, 0.01, 0.01, circular.clone()); - assert!(fov.check_two_body(&off_state).is_ok()); - assert!(fov.check_n_body(&off_state).is_ok()); - - assert!(fov - .check_visible(&[off_state], 6.0) - .first() - .unwrap() - .is_some()); - } - } -} diff --git a/src/neospy_core/src/fov/generic.rs b/src/neospy_core/src/fov/generic.rs new file mode 100644 index 0000000..79aaebc --- /dev/null +++ b/src/neospy_core/src/fov/generic.rs @@ -0,0 +1,168 @@ +//! # Definitions of contiguous field of views +//! These field of views are made up of single contiguous patches of sky, typically single image sensors. + +use std::fmt::Debug; + +use nalgebra::Vector3; +use serde::{Deserialize, Serialize}; + +use super::{Contains, FovLike, OnSkyRectangle, SkyPatch, SphericalCone, FOV}; +use crate::state::State; + +/// Generic rectangular FOV +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GenericRectangle { + observer: State, + + /// Patch of sky + pub patch: OnSkyRectangle, + + /// Rotation of the FOV. + pub rotation: f64, +} + +impl GenericRectangle { + /// Create a new Generic Rectangular FOV + pub fn new( + pointing: Vector3, + rotation: f64, + lon_width: f64, + lat_width: f64, + observer: State, + ) -> Self { + let patch = OnSkyRectangle::new(pointing, rotation, lon_width, lat_width, observer.frame); + Self { + observer, + patch, + rotation, + } + } + + /// Latitudinal width of the FOV. + #[inline] + pub fn lat_width(&self) -> f64 { + self.patch.lat_width() + } + + /// Longitudinal width of the FOV. + #[inline] + pub fn lon_width(&self) -> f64 { + self.patch.lon_width() + } +} + +impl FovLike for GenericRectangle { + #[inline] + fn get_fov(&self, index: usize) -> FOV { + if index != 0 { + panic!("FOV only has a single patch") + } + FOV::GenericRectangle(self.clone()) + } + + #[inline] + fn observer(&self) -> &State { + &self.observer + } + + #[inline] + fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { + (0, self.patch.contains(obs_to_obj)) + } + + #[inline] + fn n_patches(&self) -> usize { + 1 + } +} + +/// Generic rectangular FOV +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GenericCone { + observer: State, + + /// Patch of sky + pub patch: SphericalCone, +} +impl GenericCone { + /// Create a new Generic Conic FOV + pub fn new(pointing: Vector3, angle: f64, observer: State) -> Self { + let patch = SphericalCone::new(&pointing, angle, observer.frame); + Self { observer, patch } + } +} + +impl FovLike for GenericCone { + #[inline] + fn get_fov(&self, index: usize) -> FOV { + if index != 0 { + panic!("FOV only has a single patch") + } + FOV::GenericCone(self.clone()) + } + + #[inline] + fn observer(&self) -> &State { + &self.observer + } + + #[inline] + fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { + (0, self.patch.contains(obs_to_obj)) + } + + #[inline] + fn n_patches(&self) -> usize { + 1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::GMS_SQRT; + use crate::prelude::*; + use crate::state::Desig; + + #[test] + fn test_check_visible() { + let circular = State::new( + Desig::Empty, + 2451545.0, + [0.0, 1., 0.0].into(), + [-GMS_SQRT, 0.0, 0.0].into(), + Frame::Ecliptic, + 0, + ); + let circular_back = State::new( + Desig::Empty, + 2451545.0, + [1.0, 0.0, 0.0].into(), + [0.0, GMS_SQRT, 0.0].into(), + Frame::Ecliptic, + 0, + ); + + for offset in [-10.0, -5.0, 0.0, 5.0, 10.0] { + let off_state = propagate_n_body_spk( + circular_back.clone(), + circular_back.jd - offset, + false, + None, + ) + .unwrap(); + + let vec = Vector3::from(circular_back.pos) - Vector3::from(circular.pos); + + let fov = GenericRectangle::new(vec, 0.0001, 0.01, 0.01, circular.clone()); + assert!(fov.check_two_body(&off_state).is_ok()); + assert!(fov.check_n_body(&off_state).is_ok()); + + assert!(fov + .check_visible(&[off_state], 6.0) + .first() + .unwrap() + .is_some()); + } + } +} diff --git a/src/neospy_core/src/fov/mod.rs b/src/neospy_core/src/fov/mod.rs index cf907d7..1a58adc 100644 --- a/src/neospy_core/src/fov/mod.rs +++ b/src/neospy_core/src/fov/mod.rs @@ -1,14 +1,18 @@ //! # Field of View //! On-Sky field of view checks. -pub mod contiguous_fov; pub mod fov_like; -pub mod joint_fov; +pub mod generic; +pub mod neos; pub mod patches; +pub mod wise; +pub mod ztf; -pub use contiguous_fov::*; pub use fov_like::*; -pub use joint_fov::*; +pub use generic::*; +pub use neos::*; pub use patches::*; +pub use wise::*; +pub use ztf::*; use serde::{Deserialize, Serialize}; @@ -35,6 +39,9 @@ pub enum FOV { /// Full ZTF field of up to 64 individual files. ZtfField(ZtfField), + + /// NEOS Visit. + NeosVisit(NeosVisit), } impl FOV { @@ -47,6 +54,7 @@ impl FOV { FOV::GenericCone(fov) => fov.check_visible(states, dt_limit), FOV::GenericRectangle(fov) => fov.check_visible(states, dt_limit), FOV::ZtfField(fov) => fov.check_visible(states, dt_limit), + FOV::NeosVisit(fov) => fov.check_visible(states, dt_limit), } } @@ -59,6 +67,7 @@ impl FOV { FOV::GenericCone(fov) => fov.observer(), FOV::GenericRectangle(fov) => fov.observer(), FOV::ZtfField(fov) => fov.observer(), + FOV::NeosVisit(fov) => fov.observer(), } } @@ -71,6 +80,7 @@ impl FOV { FOV::GenericCone(fov) => fov.check_spks(obj_ids), FOV::GenericRectangle(fov) => fov.check_spks(obj_ids), FOV::ZtfField(fov) => fov.check_spks(obj_ids), + FOV::NeosVisit(fov) => fov.check_spks(obj_ids), } } } diff --git a/src/neospy_core/src/fov/neos.rs b/src/neospy_core/src/fov/neos.rs new file mode 100644 index 0000000..61165fd --- /dev/null +++ b/src/neospy_core/src/fov/neos.rs @@ -0,0 +1,208 @@ +//! # NEOS field of views +use super::{closest_inside, Contains, FovLike, OnSkyRectangle, SkyPatch, FOV}; +use crate::constants::{NEOS_HEIGHT, NEOS_WIDTH}; +use crate::prelude::*; +use nalgebra::Vector3; +use serde::{Deserialize, Serialize}; + +/// NEOS frame data, a single detector on a single band +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct NeosCmos { + /// State of the observer + observer: State, + + /// Patch of sky + pub patch: OnSkyRectangle, + + /// Rotation of the FOV. + pub rotation: f64, + + /// Side ID + pub side_id: u16, + + /// Stack ID + pub stack_id: u8, + + /// Quad ID + pub quad_id: u8, + + /// Loop ID + pub loop_id: u8, + + /// Subloop ID + pub subloop_id: u8, + + /// Exposure ID + pub exposure_id: u8, + + /// Wavelength band, either 1 or 2 for NC1 or NC2 + pub band: u8, + + /// CMOS ID + /// ID number of the CMOS chip, 0, 1, 2, or 3 + pub cmos_id: u8, +} + +impl NeosCmos { + /// Create a NEOS FOV + #[allow(clippy::too_many_arguments)] + pub fn new( + pointing: Vector3, + rotation: f64, + observer: State, + side_id: u16, + stack_id: u8, + quad_id: u8, + loop_id: u8, + subloop_id: u8, + exposure_id: u8, + cmos_id: u8, + band: u8, + ) -> Self { + let patch = + OnSkyRectangle::new(pointing, rotation, NEOS_WIDTH, NEOS_HEIGHT, observer.frame); + Self { + observer, + patch, + side_id, + stack_id, + quad_id, + loop_id, + subloop_id, + exposure_id, + cmos_id, + band, + rotation, + } + } +} + +impl FovLike for NeosCmos { + fn get_fov(&self, index: usize) -> FOV { + if index != 0 { + panic!("FOV only has a single patch") + } + FOV::NeosCmos(self.clone()) + } + + #[inline] + fn observer(&self) -> &State { + &self.observer + } + + #[inline] + fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { + (0, self.patch.contains(obs_to_obj)) + } + + #[inline] + fn n_patches(&self) -> usize { + 1 + } +} + +/// NEOS frame data, all 8 chips of a visit. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct NeosVisit { + /// Individual CMOS fields + chips: Box<[NeosCmos; 8]>, + + /// Observer position + observer: State, + + /// Rotation of the FOV. + pub rotation: f64, + + /// Side ID + pub side_id: u16, + + /// Stack ID + pub stack_id: u8, + + /// Quad ID + pub quad_id: u8, + + /// Loop ID + pub loop_id: u8, + + /// Subloop ID + pub subloop_id: u8, + + /// Exposure ID + pub exposure_id: u8, +} + +impl NeosVisit { + /// Construct a new NeosVisit from a list of cmos fovs. + /// These cmos fovs must be from the same metadata when appropriate. + pub fn new(chips: Vec) -> Result { + if chips.len() != 8 { + return Err(NEOSpyError::ValueError( + "Visit must contains 8 NeosCmos fovs".into(), + )); + } + let chips: Box<[NeosCmos; 8]> = Box::new(chips.try_into().unwrap()); + + let first = chips.first().unwrap(); + + let observer = first.observer().clone(); + let side_id = first.side_id; + let stack_id = first.stack_id; + let quad_id = first.quad_id; + let loop_id = first.loop_id; + let subloop_id = first.subloop_id; + let exposure_id = first.exposure_id; + let rotation = first.rotation; + + for ccd in chips.iter() { + if ccd.side_id != side_id + || ccd.stack_id != stack_id + || ccd.quad_id != quad_id + || ccd.loop_id != loop_id + || ccd.subloop_id != subloop_id + || ccd.exposure_id != exposure_id + || ccd.rotation != rotation + || ccd.observer().jd != observer.jd + { + return Err(NEOSpyError::ValueError( + "All NeosCmos must have matching values.".into(), + )); + } + } + Ok(Self { + chips, + observer, + rotation, + side_id, + stack_id, + quad_id, + loop_id, + subloop_id, + exposure_id, + }) + } +} + +impl FovLike for NeosVisit { + fn get_fov(&self, index: usize) -> FOV { + FOV::NeosCmos(self.chips[index].clone()) + } + + fn observer(&self) -> &State { + &self.observer + } + + fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { + closest_inside( + &self + .chips + .iter() + .map(|x| x.contains(obs_to_obj).1) + .collect::>(), + ) + } + + fn n_patches(&self) -> usize { + 8 + } +} diff --git a/src/neospy_core/src/fov/wise.rs b/src/neospy_core/src/fov/wise.rs new file mode 100644 index 0000000..a349c36 --- /dev/null +++ b/src/neospy_core/src/fov/wise.rs @@ -0,0 +1,70 @@ +//! # WISE Fov definitions. +use super::{Contains, FovLike, OnSkyRectangle, SkyPatch, FOV}; +use crate::constants::WISE_WIDTH; +use crate::prelude::*; +use nalgebra::Vector3; +use serde::{Deserialize, Serialize}; + +/// WISE or NEOWISE frame data, all bands +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WiseCmos { + /// State of the observer + observer: State, + + /// Patch of sky + pub patch: OnSkyRectangle, + + /// Rotation of the FOV. + pub rotation: f64, + + /// Frame number of the fov + pub frame_num: usize, + + /// Scan ID of the fov + pub scan_id: Box, +} + +impl WiseCmos { + /// Create a Wise fov + pub fn new( + pointing: Vector3, + rotation: f64, + observer: State, + frame_num: usize, + scan_id: Box, + ) -> Self { + let patch = OnSkyRectangle::new(pointing, rotation, WISE_WIDTH, WISE_WIDTH, observer.frame); + Self { + patch, + observer, + frame_num, + rotation, + scan_id, + } + } +} + +impl FovLike for WiseCmos { + #[inline] + fn get_fov(&self, index: usize) -> FOV { + if index != 0 { + panic!("Wise FOV only has a single patch") + } + FOV::Wise(self.clone()) + } + + #[inline] + fn observer(&self) -> &State { + &self.observer + } + + #[inline] + fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { + (0, self.patch.contains(obs_to_obj)) + } + + #[inline] + fn n_patches(&self) -> usize { + 1 + } +} diff --git a/src/neospy_core/src/fov/joint_fov.rs b/src/neospy_core/src/fov/ztf.rs similarity index 54% rename from src/neospy_core/src/fov/joint_fov.rs rename to src/neospy_core/src/fov/ztf.rs index a72f07e..54ba845 100644 --- a/src/neospy_core/src/fov/joint_fov.rs +++ b/src/neospy_core/src/fov/ztf.rs @@ -1,11 +1,100 @@ -//! # Definitios of joint field of views -//! These field of views are made up of multiple contiguous patches of sky, typically full image arrays. -use super::contiguous_fov::ZtfCcdQuad; -use super::{closest_inside, Contains, FovLike, FOV}; +//! # ZTF Fov definitions. + +use super::{closest_inside, Contains, FovLike, OnSkyRectangle, SkyPatch, FOV}; use crate::prelude::*; use nalgebra::Vector3; use serde::{Deserialize, Serialize}; +/// ZTF frame data, single quad of a single chip +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ZtfCcdQuad { + /// State of the observer + observer: State, + + /// Patch of sky + pub patch: OnSkyRectangle, + + /// Field ID + pub field: u32, + + /// File Frac Day + /// String representation of the filename for this frame. + pub filefracday: u64, + + /// Magnitude limit of this frame + pub maglimit: f64, + + /// Filter ID + pub fid: usize, + + /// Filter code used for the frame + pub filtercode: Box, + + /// Image Type Code + pub imgtypecode: Box, + + /// Which CCID was the frame taken with + pub ccdid: u8, + + /// Quadrant ID + pub qid: u8, +} + +impl ZtfCcdQuad { + /// Create a ZTF field of view + #[allow(clippy::too_many_arguments)] + pub fn new( + corners: [Vector3; 4], + observer: State, + field: u32, + filefracday: u64, + ccdid: u8, + filtercode: Box, + imgtypecode: Box, + qid: u8, + maglimit: f64, + fid: usize, + ) -> Self { + let patch = OnSkyRectangle::from_corners(corners, observer.frame); + Self { + patch, + observer, + field, + filefracday, + ccdid, + filtercode, + imgtypecode, + qid, + maglimit, + fid, + } + } +} + +impl FovLike for ZtfCcdQuad { + fn get_fov(&self, index: usize) -> FOV { + if index != 0 { + panic!("FOV only has a single patch") + } + FOV::ZtfCcdQuad(self.clone()) + } + + #[inline] + fn observer(&self) -> &State { + &self.observer + } + + #[inline] + fn contains(&self, obs_to_obj: &Vector3) -> (usize, Contains) { + (0, self.patch.contains(obs_to_obj)) + } + + #[inline] + fn n_patches(&self) -> usize { + 1 + } +} + /// ZTF frame data, single quad of a single chip #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ZtfField { From 23897fa926111b827d641b57480471464b2da637 Mon Sep 17 00:00:00 2001 From: dahlend Date: Mon, 13 May 2024 12:25:55 -0700 Subject: [PATCH 02/12] begin adding NEOS Visits (broken) --- src/neospy/rust/fovs/definitions.rs | 111 ++++++++++++++++++++++++++++ src/neospy_core/src/fov/neos.rs | 67 +++++++++++++++-- 2 files changed, 173 insertions(+), 5 deletions(-) diff --git a/src/neospy/rust/fovs/definitions.rs b/src/neospy/rust/fovs/definitions.rs index 0346b5b..d9a5857 100644 --- a/src/neospy/rust/fovs/definitions.rs +++ b/src/neospy/rust/fovs/definitions.rs @@ -17,8 +17,15 @@ pub struct PyWiseCmos(pub fov::WiseCmos); #[pyclass(module = "neospy", frozen, name = "NeosCmos")] #[derive(Clone, Debug)] #[allow(clippy::upper_case_acronyms)] + pub struct PyNeosCmos(pub fov::NeosCmos); +/// Field of view of a NEOS Visit. +#[pyclass(module = "neospy", frozen, name = "NeosVisit")] +#[derive(Clone, Debug)] +#[allow(clippy::upper_case_acronyms)] +pub struct PyNeosVisit(pub fov::NeosVisit); + /// Field of view of a Single ZTF chips/quad combination. #[pyclass(module = "neospy", frozen, name = "ZtfCcdQuad")] #[derive(Clone, Debug)] @@ -46,6 +53,7 @@ pub enum AllowedFOV { Rectangle(PyGenericRectangle), ZTF(PyZtfCcdQuad), ZTFField(PyZtfField), + NEOSVisit(PyNeosVisit), } impl AllowedFOV { @@ -56,6 +64,7 @@ impl AllowedFOV { AllowedFOV::Rectangle(fov) => fov.0.observer().jd, AllowedFOV::ZTF(fov) => fov.0.observer().jd, AllowedFOV::ZTFField(fov) => fov.0.observer().jd, + AllowedFOV::NEOSVisit(fov) => fov.0.observer().jd, } } @@ -67,6 +76,7 @@ impl AllowedFOV { AllowedFOV::NEOS(fov) => fov.0.get_fov(idx), AllowedFOV::ZTF(fov) => fov.0.get_fov(idx), AllowedFOV::ZTFField(fov) => fov.0.get_fov(idx), + AllowedFOV::NEOSVisit(fov) => fov.0.get_fov(idx), } } @@ -77,6 +87,7 @@ impl AllowedFOV { AllowedFOV::NEOS(fov) => fov::FOV::NeosCmos(fov.0), AllowedFOV::ZTF(fov) => fov::FOV::ZtfCcdQuad(fov.0), AllowedFOV::ZTFField(fov) => fov::FOV::ZtfField(fov.0), + AllowedFOV::NEOSVisit(fov) => fov::FOV::NeosVisit(fov.0), } } @@ -87,6 +98,7 @@ impl AllowedFOV { AllowedFOV::NEOS(fov) => fov.__repr__(), AllowedFOV::ZTF(fov) => fov.__repr__(), AllowedFOV::ZTFField(fov) => fov.__repr__(), + AllowedFOV::NEOSVisit(fov) => fov.__repr__(), } } } @@ -99,6 +111,7 @@ impl IntoPy for AllowedFOV { Self::Rectangle(fov) => fov.into_py(py), Self::ZTF(fov) => fov.into_py(py), Self::ZTFField(fov) => fov.into_py(py), + Self::NEOSVisit(fov) => fov.into_py(py), } } } @@ -111,6 +124,7 @@ impl From for AllowedFOV { fov::FOV::NeosCmos(fov) => AllowedFOV::NEOS(PyNeosCmos(fov)), fov::FOV::GenericRectangle(fov) => AllowedFOV::Rectangle(PyGenericRectangle(fov)), fov::FOV::ZtfField(fov) => AllowedFOV::ZTFField(PyZtfField(fov)), + fov::FOV::NeosVisit(fov) => AllowedFOV::NEOSVisit(PyNeosVisit(fov)), _ => { unimplemented!("Python interface doesn't support this FOV.") } @@ -335,6 +349,103 @@ impl PyNeosCmos { ) } } +#[pymethods] +#[allow(clippy::too_many_arguments)] +impl PyNeosVisit { + #[new] + pub fn new( + pointing: VectorLike, + rotation: f64, + observer: PyState, + side_id: u16, + stack_id: u8, + quad_id: u8, + loop_id: u8, + subloop_id: u8, + exposure_id: u8, + cmos_id: u8, + band: u8, + ) -> Self { + let pointing = pointing.into_vector(crate::frame::PyFrames::Ecliptic); + let pointing = pointing.raw.into(); + PyNeosVisit(fov::NeosVisit::new( + pointing, + rotation.to_radians(), + observer.0, + side_id, + stack_id, + quad_id, + loop_id, + subloop_id, + exposure_id, + cmos_id, + band, + )) + } + + #[getter] + pub fn observer(&self) -> PyState { + self.0.observer().clone().into() + } + + #[getter] + pub fn pointing(&self) -> Vector { + Vector::new( + self.0.patch.pointing().into_inner().into(), + self.0.observer().frame.into(), + ) + } + + #[getter] + pub fn side_id(&self) -> u16 { + self.0.side_id + } + + #[getter] + pub fn stack_id(&self) -> u8 { + self.0.stack_id + } + + #[getter] + pub fn quad_id(&self) -> u8 { + self.0.quad_id + } + + #[getter] + pub fn loop_id(&self) -> u8 { + self.0.loop_id + } + + #[getter] + pub fn subloop_id(&self) -> u8 { + self.0.subloop_id + } + + #[getter] + pub fn exposure_id(&self) -> u8 { + self.0.exposure_id + } + + #[getter] + pub fn rotation(&self) -> f64 { + self.0.rotation + } + + fn __repr__(&self) -> String { + format!( + "NEOSVisit(pointing={}, rotation={}, observer={}, side_id={}, stack_id={}, quad_id={}, loop_id={}, subloop_id={}, exposure_id={})", + self.pointing().__repr__(), + self.rotation(), + self.observer().__repr__(), + self.side_id(), + self.stack_id(), + self.quad_id(), + self.loop_id(), + self.subloop_id(), + self.exposure_id() + ) + } +} #[pymethods] #[allow(clippy::too_many_arguments)] diff --git a/src/neospy_core/src/fov/neos.rs b/src/neospy_core/src/fov/neos.rs index 61165fd..65552a4 100644 --- a/src/neospy_core/src/fov/neos.rs +++ b/src/neospy_core/src/fov/neos.rs @@ -1,6 +1,7 @@ //! # NEOS field of views use super::{closest_inside, Contains, FovLike, OnSkyRectangle, SkyPatch, FOV}; use crate::constants::{NEOS_HEIGHT, NEOS_WIDTH}; +use crate::frames::rotate_around; use crate::prelude::*; use nalgebra::Vector3; use serde::{Deserialize, Serialize}; @@ -101,11 +102,11 @@ impl FovLike for NeosCmos { } } -/// NEOS frame data, all 8 chips of a visit. +/// NEOS frame data, all 4 chips of a visit. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct NeosVisit { /// Individual CMOS fields - chips: Box<[NeosCmos; 8]>, + chips: Box<[NeosCmos; 4]>, /// Observer position observer: State, @@ -136,12 +137,12 @@ impl NeosVisit { /// Construct a new NeosVisit from a list of cmos fovs. /// These cmos fovs must be from the same metadata when appropriate. pub fn new(chips: Vec) -> Result { - if chips.len() != 8 { + if chips.len() != 4 { return Err(NEOSpyError::ValueError( - "Visit must contains 8 NeosCmos fovs".into(), + "Visit must contains 4 NeosCmos fovs".into(), )); } - let chips: Box<[NeosCmos; 8]> = Box::new(chips.try_into().unwrap()); + let chips: Box<[NeosCmos; 4]> = Box::new(chips.try_into().unwrap()); let first = chips.first().unwrap(); @@ -181,6 +182,62 @@ impl NeosVisit { exposure_id, }) } + + /// x_width is the longer dimension in radians + pub fn from_pointing( + x_width: f64, + y_width: f64, + gap_fraction: f64, + pointing: Vector3, + rotation: f64, + observer: State, + side_id: u16, + stack_id: u8, + quad_id: u8, + loop_id: u8, + subloop_id: u8, + exposure_id: u8, + cmos_id: u8, + band: u8, + ) -> Self { + let chip_x_width = (1.0 - 3.0 * gap_fraction) * x_width / 4.0; + + // Rotate the Z axis to match the defined rotation angle, this vector is not + // orthogonal to the pointing vector, but is in the correct plane of the final + // up vector. + let up_vec = rotate_around(&Vector3::new(0.0, 0.0, 1.0), pointing, -rotation); + + // construct the vector orthogonal to the pointing and rotate z axis vectors. + // left = cross(up, pointing) + let left_vec = pointing.cross(&up_vec); + + // Given the new left vector, and the existing orthogonal pointing vector, + // construct a new up vector which is in the same plane as it was before, but now + // orthogonal to the two existing vectors. + // up = cross(pointing, left) + let up_vec = pointing.cross(&left_vec); + + // +------+-+------+-+------+-+------+ ^ + // | 1 |g| 2 |g| 3 |g| 4 | | + // | |a| |a| |a| | y + // | |p| |p| |p| | | + // +------+-+------+-+------+-+------+ _ + // <-cf-> x -> + // + // pointing vector is in the middle of the 'a' in the central gap. + + let outer: Vector3 = rotate_around(&left_vec, up_vec, -lon_width / 2.0); + let n2: Vector3 = rotate_around(&(-left_vec), up_vec, lon_width / 2.0); + + let long_top: Vector3 = rotate_around(&up_vec, left_vec, y_width / 2.0); + let long_bottom: Vector3 = rotate_around(&(-up_vec), left_vec, -y_width / 2.0); + + // construct the 4 normal vectors + Self { + edge_normals: [n1.into(), n2.into(), n3.into(), n4.into()], + frame, + } + } } impl FovLike for NeosVisit { From b10dbc3f3b2b15e4d0e37f396b2790bb037514ec Mon Sep 17 00:00:00 2001 From: dahlend Date: Fri, 24 May 2024 09:23:50 -0700 Subject: [PATCH 03/12] cleanup --- src/neospy/spice.py | 1 - src/neospy/ztf.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/neospy/spice.py b/src/neospy/spice.py index ad7b421..8dd67f1 100644 --- a/src/neospy/spice.py +++ b/src/neospy/spice.py @@ -54,7 +54,6 @@ class SpiceKernels: This allows for the loading of additional spice kernels along with querying. """ - _constants: Optional[dict[str, float]] = None _name_cache: dict = {} @classmethod diff --git a/src/neospy/ztf.py b/src/neospy/ztf.py index 42d6e7c..e2cd6bd 100644 --- a/src/neospy/ztf.py +++ b/src/neospy/ztf.py @@ -13,7 +13,6 @@ "ztf.ztf_current_path_raw": "ZTF Raw Product Paths", "ztf.ztf_current_path_ref": "ZTF Reference Product Paths", "ztf.ztf_current_path_sci": "ZTF Science Product Paths", - "ztf_objects": "ZTF Objects", "ztf_objects_dr16": "ZTF Data Release 16 Objects", "ztf_objects_dr17": "ZTF Data Release 17 Objects", "ztf_objects_dr18": "ZTF Data Release 18 Objects", From ec0a947bb37a91b06547a56fbcb653348313509f Mon Sep 17 00:00:00 2001 From: dahlend Date: Fri, 24 May 2024 10:55:37 -0700 Subject: [PATCH 04/12] static checks --- src/neospy/rust/fovs/checks.rs | 29 ++++++++++++- src/neospy/rust/fovs/collection.rs | 1 + src/neospy/rust/fovs/definitions.rs | 64 ++++++++++++++--------------- src/neospy/rust/lib.rs | 1 + src/neospy_core/src/fov/fov_like.rs | 40 ++++++++++++++++++ src/neospy_core/src/fov/generic.rs | 6 +++ src/neospy_core/src/fov/mod.rs | 15 +++++++ src/neospy_core/src/fov/neos.rs | 50 +++++++++++++++++++--- 8 files changed, 167 insertions(+), 39 deletions(-) diff --git a/src/neospy/rust/fovs/checks.rs b/src/neospy/rust/fovs/checks.rs index 51722b7..9bb2f2a 100644 --- a/src/neospy/rust/fovs/checks.rs +++ b/src/neospy/rust/fovs/checks.rs @@ -3,7 +3,7 @@ use neospy_core::propagation::propagate_n_body_spk; use pyo3::prelude::*; use rayon::prelude::*; -use crate::simult_states::{PySimultaneousStates, SimulStateLike}; +use crate::{simult_states::{PySimultaneousStates, SimulStateLike}, vector::{Vector, VectorLike}}; #[pyfunction] #[pyo3(name = "fov_checks")] @@ -79,3 +79,30 @@ pub fn fov_spk_checks_py(obj_ids: Vec, fovs: FOVListLike) -> Vec, fovs: FOVListLike) -> Vec<(Vec, AllowedFOV)> { + let fovs = fovs.into_sorted_vec_fov(); + let pos:Vec<_> = pos.into_iter().map(|p| p.into_vec(crate::frame::PyFrames::Ecliptic)).collect(); + + fovs.into_par_iter() + .filter_map(|fov| { + let vis: Vec<_> = fov + .check_statics(&pos) + .into_iter() + .filter_map(|pop| { + pop.map(|(p_vec, fov)| { + let p_vec = p_vec.into_iter().map(|p| Vector::new(p.into(), crate::frame::PyFrames::Ecliptic)).collect(); + (p_vec, fov.into()) + }) + }) + .collect(); + match vis.is_empty() { + true => None, + false => Some(vis), + } + }) + .flatten() + .collect() +} diff --git a/src/neospy/rust/fovs/collection.rs b/src/neospy/rust/fovs/collection.rs index 003b655..77627a6 100644 --- a/src/neospy/rust/fovs/collection.rs +++ b/src/neospy/rust/fovs/collection.rs @@ -16,6 +16,7 @@ pub enum FOVListLike { impl FOVListLike { /// Convert to a vector of neospy core fovs. + /// Frames will always be ecliptic pub fn into_sorted_vec_fov(self) -> Vec { let mut fovs = match self { FOVListLike::Vec(v) => v, diff --git a/src/neospy/rust/fovs/definitions.rs b/src/neospy/rust/fovs/definitions.rs index f2b2391..adef207 100644 --- a/src/neospy/rust/fovs/definitions.rs +++ b/src/neospy/rust/fovs/definitions.rs @@ -327,7 +327,7 @@ impl PyNeosCmos { subloop_id, exposure_id, cmos_id, - band, + band.into(), )) } @@ -406,36 +406,36 @@ impl PyNeosCmos { #[pymethods] #[allow(clippy::too_many_arguments)] impl PyNeosVisit { - #[new] - pub fn new( - pointing: VectorLike, - rotation: f64, - observer: PyState, - side_id: u16, - stack_id: u8, - quad_id: u8, - loop_id: u8, - subloop_id: u8, - exposure_id: u8, - cmos_id: u8, - band: u8, - ) -> Self { - let pointing = pointing.into_vector(crate::frame::PyFrames::Ecliptic); - let pointing = pointing.raw.into(); - PyNeosVisit(fov::NeosVisit::new( - pointing, - rotation.to_radians(), - observer.0, - side_id, - stack_id, - quad_id, - loop_id, - subloop_id, - exposure_id, - cmos_id, - band, - )) - } + // #[new] + // pub fn new( + // pointing: VectorLike, + // rotation: f64, + // observer: PyState, + // side_id: u16, + // stack_id: u8, + // quad_id: u8, + // loop_id: u8, + // subloop_id: u8, + // exposure_id: u8, + // cmos_id: u8, + // band: u8, + // ) -> Self { + // let pointing = pointing.into_vector(crate::frame::PyFrames::Ecliptic); + // let pointing = pointing.raw.into(); + // PyNeosVisit(fov::NeosVisit::from_pointing( + // pointing, + // rotation.to_radians(), + // observer.0, + // side_id, + // stack_id, + // quad_id, + // loop_id, + // subloop_id, + // exposure_id, + // cmos_id, + // band.into(), + // )) + // } #[getter] pub fn observer(&self) -> PyState { @@ -445,7 +445,7 @@ impl PyNeosVisit { #[getter] pub fn pointing(&self) -> Vector { Vector::new( - self.0.patch.pointing().into_inner().into(), + self.0.pointing().into(), self.0.observer().frame.into(), ) } diff --git a/src/neospy/rust/lib.rs b/src/neospy/rust/lib.rs index 1a19b42..d160cd9 100644 --- a/src/neospy/rust/lib.rs +++ b/src/neospy/rust/lib.rs @@ -57,6 +57,7 @@ fn _core(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(fovs::fov_checks_py, m)?)?; m.add_function(wrap_pyfunction!(fovs::fov_spk_checks_py, m)?)?; + m.add_function(wrap_pyfunction!(fovs::fov_static_checks_py, m)?)?; m.add_function(wrap_pyfunction!(flux::hg_apparent_flux_py, m)?)?; m.add_function(wrap_pyfunction!(flux::hg_apparent_mag_py, m)?)?; diff --git a/src/neospy_core/src/fov/fov_like.rs b/src/neospy_core/src/fov/fov_like.rs index dd75b86..2c62d95 100644 --- a/src/neospy_core/src/fov/fov_like.rs +++ b/src/neospy_core/src/fov/fov_like.rs @@ -30,6 +30,16 @@ pub trait FovLike: Sync + Sized { /// Change the target frame to the new frame. fn try_frame_change_mut(&mut self, new_frame: Frame) -> Result<(), NEOSpyError>; + + /// Check if a static source is visible + #[inline] + fn check_static(&self, pos: &Vector3 ) -> (usize, Contains) { + let obs = self.observer(); + let obs_pos: Vector3<_> = obs.pos.into(); + let rel_pos = pos - obs_pos; + self.contains(&rel_pos) + } + /// Assuming the object undergoes linear motion, check to see if it is within the /// field of view. #[inline] @@ -206,4 +216,34 @@ pub trait FovLike: Sync + Sized { }) .collect() } + + /// Given a collection of static positions, return all which are visible. + fn check_statics(&self, pos: &[Vector3]) -> Vec>, FOV)>> { + + let mut visible: Vec>> = vec![Vec::new(); self.n_patches()]; + + let vis_pos: Vec<_> = pos + .into_par_iter() + .filter_map(|p| { + match self.check_static(p) { + (idx, Contains::Inside) => Some((idx, p)), + _ => None, + } + }) + .collect(); + + vis_pos + .into_iter() + .for_each(|(patch_idx, p)| visible[patch_idx].push(*p)); + + visible. + into_iter() + .enumerate() + .map(|(idx, vis_patch)| { + if vis_patch.is_empty(){ + None}else{ Some((vis_patch, self.get_fov(idx))) + } + }) + .collect() + } } diff --git a/src/neospy_core/src/fov/generic.rs b/src/neospy_core/src/fov/generic.rs index e0f51f6..b5e7071 100644 --- a/src/neospy_core/src/fov/generic.rs +++ b/src/neospy_core/src/fov/generic.rs @@ -38,6 +38,12 @@ impl GenericRectangle { } } + /// Create a Field of view from a collection of corners. + pub fn from_corners(corners: [Vector3; 4], observer: State) -> Self { + let patch = OnSkyRectangle::from_corners(corners, observer.frame); + Self { patch, observer , rotation:f64::NAN} + } + /// Latitudinal width of the FOV. #[inline] pub fn lat_width(&self) -> f64 { diff --git a/src/neospy_core/src/fov/mod.rs b/src/neospy_core/src/fov/mod.rs index b41ef9e..98cc570 100644 --- a/src/neospy_core/src/fov/mod.rs +++ b/src/neospy_core/src/fov/mod.rs @@ -9,6 +9,7 @@ pub mod ztf; pub use fov_like::*; pub use generic::*; +use nalgebra::Vector3; pub use neos::*; pub use patches::*; pub use wise::*; @@ -84,6 +85,20 @@ impl FOV { } } + /// Check if static sources are visible in this FOV. + /// Position must be in the correct frame! + pub fn check_statics(&self, pos: &[Vector3]) -> Vec>, FOV)>> { + match self { + FOV::Wise(fov) => fov.check_statics(pos), + FOV::NeosCmos(fov) => fov.check_statics(pos), + FOV::ZtfCcdQuad(fov) => fov.check_statics(pos), + FOV::GenericCone(fov) => fov.check_statics(pos), + FOV::GenericRectangle(fov) => fov.check_statics(pos), + FOV::ZtfField(fov) => fov.check_statics(pos), + FOV::NeosVisit(fov) => fov.check_statics(pos), + } + } + /// Change the frame of this FOV pub fn try_frame_change_mut(&mut self, target_frame: Frame) -> Result<(), NEOSpyError> { match self { diff --git a/src/neospy_core/src/fov/neos.rs b/src/neospy_core/src/fov/neos.rs index 3540a30..05c61bb 100644 --- a/src/neospy_core/src/fov/neos.rs +++ b/src/neospy_core/src/fov/neos.rs @@ -6,6 +6,31 @@ use crate::prelude::*; use nalgebra::Vector3; use serde::{Deserialize, Serialize}; +/// NEOS bands +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)] +pub enum NeosBand{ + /// No Band defined. + Undefined, + + /// NEOS NC1 Band. + NC1, + + /// NEOS NC2 Band. + NC2, +} + +/// Convert a NEOS band from u8 +/// 1 is NC1 2 is NC2, everything else is Undefined. +impl From for NeosBand{ + fn from(value: u8) -> Self { + match value{ + 1=> NeosBand::NC1, + 2=> NeosBand::NC2, + _=> NeosBand::Undefined + } + } +} + /// NEOS frame data, a single detector on a single band #[derive(Debug, Clone, Deserialize, Serialize)] pub struct NeosCmos { @@ -36,8 +61,8 @@ pub struct NeosCmos { /// Exposure ID pub exposure_id: u8, - /// Wavelength band, either 1 or 2 for NC1 or NC2 - pub band: u8, + /// Wavelength band + pub band: NeosBand, /// CMOS ID /// ID number of the CMOS chip, 0, 1, 2, or 3 @@ -58,7 +83,7 @@ impl NeosCmos { subloop_id: u8, exposure_id: u8, cmos_id: u8, - band: u8, + band: NeosBand, ) -> Self { let patch = OnSkyRectangle::new(pointing, rotation, NEOS_WIDTH, NEOS_HEIGHT, observer.frame); @@ -111,7 +136,7 @@ impl FovLike for NeosCmos { } } -/// NEOS frame data, all 4 chips of a visit. +/// NEOS frame data, 4 chips of a visit. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct NeosVisit { /// Individual CMOS fields @@ -140,6 +165,9 @@ pub struct NeosVisit { /// Exposure ID pub exposure_id: u8, + + /// Wavelength band + pub band: NeosBand, } impl NeosVisit { @@ -163,6 +191,7 @@ impl NeosVisit { let subloop_id = first.subloop_id; let exposure_id = first.exposure_id; let rotation = first.rotation; + let band = first.band; for ccd in chips.iter() { if ccd.side_id != side_id @@ -173,6 +202,7 @@ impl NeosVisit { || ccd.exposure_id != exposure_id || ccd.rotation != rotation || ccd.observer().jd != observer.jd + || ccd.band != band { return Err(NEOSpyError::ValueError( "All NeosCmos must have matching values.".into(), @@ -189,6 +219,7 @@ impl NeosVisit { loop_id, subloop_id, exposure_id, + band, }) } @@ -207,7 +238,7 @@ impl NeosVisit { // subloop_id: u8, // exposure_id: u8, // cmos_id: u8, - // band: u8, + // band: NeosBand, // ) -> Self { // let chip_x_width = (1.0 - 3.0 * gap_fraction) * x_width / 4.0; @@ -216,7 +247,7 @@ impl NeosVisit { // // up vector. // let up_vec = rotate_around(&Vector3::new(0.0, 0.0, 1.0), pointing, -rotation); - // // construct the vector orthogonal to the pointing and rotate z axis vectors. + // // construct the vector orthogonal to the pointing and rotated z axis vectors. // // left = cross(up, pointing) // let left_vec = pointing.cross(&up_vec); @@ -247,6 +278,13 @@ impl NeosVisit { // frame, // } // } + + /// Return the central pointing vector. + pub fn pointing(&self) -> Vector3{ + let mut pointing = Vector3::::zeros(); + self.chips.iter().for_each(|chip| pointing += *chip.patch.pointing()); + pointing.normalize() + } } impl FovLike for NeosVisit { From 4bf430a9392878abf0acba0782cfa5f2c89dffcc Mon Sep 17 00:00:00 2001 From: dahlend Date: Fri, 24 May 2024 16:37:15 -0700 Subject: [PATCH 05/12] mostly working, missing the chip gap --- src/neospy/rust/fovs/definitions.rs | 84 +++++++++++------- src/neospy/rust/lib.rs | 1 + src/neospy_core/src/fov/fov_like.rs | 10 +-- src/neospy_core/src/fov/neos.rs | 131 ++++++++++++++++------------ src/neospy_core/src/fov/patches.rs | 18 ++++ 5 files changed, 149 insertions(+), 95 deletions(-) diff --git a/src/neospy/rust/fovs/definitions.rs b/src/neospy/rust/fovs/definitions.rs index adef207..a07e242 100644 --- a/src/neospy/rust/fovs/definitions.rs +++ b/src/neospy/rust/fovs/definitions.rs @@ -1,5 +1,5 @@ use nalgebra::Vector3; -use neospy_core::fov; +use neospy_core::fov::{self, NeosBand}; use neospy_core::fov::{FovLike, SkyPatch}; use pyo3::{exceptions, prelude::*}; @@ -382,6 +382,22 @@ impl PyNeosCmos { self.0.exposure_id } + /// Chip ID number + #[getter] + pub fn cmos_id(&self) -> u8 { + self.0.cmos_id + } + + /// Band Number + #[getter] + pub fn band(&self) -> u8 { + match self.0.band { + NeosBand::NC1 => 1, + NeosBand::NC2 => 2, + NeosBand::Undefined => 0, + } + } + /// Rotation angle of the FOV in degrees. #[getter] pub fn rotation(&self) -> f64 { @@ -390,7 +406,7 @@ impl PyNeosCmos { fn __repr__(&self) -> String { format!( - "NeosCmos(pointing={}, rotation={}, observer={}, side_id={}, stack_id={}, quad_id={}, loop_id={}, subloop_id={}, exposure_id={})", + "NeosCmos(pointing={}, rotation={}, observer={}, side_id={}, stack_id={}, quad_id={}, loop_id={}, subloop_id={}, exposure_id={}, cmos_id={}, band={})", self.pointing().__repr__(), self.rotation(), self.observer().__repr__(), @@ -399,43 +415,45 @@ impl PyNeosCmos { self.quad_id(), self.loop_id(), self.subloop_id(), - self.exposure_id() + self.exposure_id(), + self.cmos_id(), + self.band() ) } } #[pymethods] #[allow(clippy::too_many_arguments)] impl PyNeosVisit { - // #[new] - // pub fn new( - // pointing: VectorLike, - // rotation: f64, - // observer: PyState, - // side_id: u16, - // stack_id: u8, - // quad_id: u8, - // loop_id: u8, - // subloop_id: u8, - // exposure_id: u8, - // cmos_id: u8, - // band: u8, - // ) -> Self { - // let pointing = pointing.into_vector(crate::frame::PyFrames::Ecliptic); - // let pointing = pointing.raw.into(); - // PyNeosVisit(fov::NeosVisit::from_pointing( - // pointing, - // rotation.to_radians(), - // observer.0, - // side_id, - // stack_id, - // quad_id, - // loop_id, - // subloop_id, - // exposure_id, - // cmos_id, - // band.into(), - // )) - // } + #[new] + pub fn new( + x_width: f64, + y_width: f64, + pointing: VectorLike, + rotation: f64, + observer: PyState, + side_id: u16, + stack_id: u8, + quad_id: u8, + loop_id: u8, + subloop_id: u8, + exposure_id: u8, + band: u8, + ) -> Self { + let pointing = pointing.into_vector(crate::frame::PyFrames::Ecliptic); + let pointing = pointing.raw.into(); + PyNeosVisit(fov::NeosVisit::from_pointing(x_width.to_radians(), y_width.to_radians(), 0.0, + pointing, + rotation.to_radians(), + observer.0, + side_id, + stack_id, + quad_id, + loop_id, + subloop_id, + exposure_id, + band.into(), + )) + } #[getter] pub fn observer(&self) -> PyState { diff --git a/src/neospy/rust/lib.rs b/src/neospy/rust/lib.rs index d160cd9..65f8b55 100644 --- a/src/neospy/rust/lib.rs +++ b/src/neospy/rust/lib.rs @@ -34,6 +34,7 @@ fn _core(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/neospy_core/src/fov/fov_like.rs b/src/neospy_core/src/fov/fov_like.rs index 2c62d95..3350e75 100644 --- a/src/neospy_core/src/fov/fov_like.rs +++ b/src/neospy_core/src/fov/fov_like.rs @@ -31,13 +31,11 @@ pub trait FovLike: Sync + Sized { fn try_frame_change_mut(&mut self, new_frame: Frame) -> Result<(), NEOSpyError>; - /// Check if a static source is visible + /// Check if a static source is visible. This assumes the vector passed in is at an + /// infinite distance from the observer. #[inline] fn check_static(&self, pos: &Vector3 ) -> (usize, Contains) { - let obs = self.observer(); - let obs_pos: Vector3<_> = obs.pos.into(); - let rel_pos = pos - obs_pos; - self.contains(&rel_pos) + self.contains(pos) } /// Assuming the object undergoes linear motion, check to see if it is within the @@ -223,7 +221,7 @@ pub trait FovLike: Sync + Sized { let mut visible: Vec>> = vec![Vec::new(); self.n_patches()]; let vis_pos: Vec<_> = pos - .into_par_iter() + .iter() .filter_map(|p| { match self.check_static(p) { (idx, Contains::Inside) => Some((idx, p)), diff --git a/src/neospy_core/src/fov/neos.rs b/src/neospy_core/src/fov/neos.rs index 05c61bb..5f69228 100644 --- a/src/neospy_core/src/fov/neos.rs +++ b/src/neospy_core/src/fov/neos.rs @@ -1,7 +1,7 @@ //! # NEOS field of views use super::{closest_inside, Contains, FovLike, OnSkyRectangle, SkyPatch, FOV}; use crate::constants::{NEOS_HEIGHT, NEOS_WIDTH}; -// use crate::frames::rotate_around; +use crate::frames::rotate_around; use crate::prelude::*; use nalgebra::Vector3; use serde::{Deserialize, Serialize}; @@ -223,61 +223,80 @@ impl NeosVisit { }) } - // /// x_width is the longer dimension in radians - // pub fn from_pointing( - // x_width: f64, - // y_width: f64, - // gap_fraction: f64, - // pointing: Vector3, - // rotation: f64, - // observer: State, - // side_id: u16, - // stack_id: u8, - // quad_id: u8, - // loop_id: u8, - // subloop_id: u8, - // exposure_id: u8, - // cmos_id: u8, - // band: NeosBand, - // ) -> Self { - // let chip_x_width = (1.0 - 3.0 * gap_fraction) * x_width / 4.0; - - // // Rotate the Z axis to match the defined rotation angle, this vector is not - // // orthogonal to the pointing vector, but is in the correct plane of the final - // // up vector. - // let up_vec = rotate_around(&Vector3::new(0.0, 0.0, 1.0), pointing, -rotation); - - // // construct the vector orthogonal to the pointing and rotated z axis vectors. - // // left = cross(up, pointing) - // let left_vec = pointing.cross(&up_vec); - - // // Given the new left vector, and the existing orthogonal pointing vector, - // // construct a new up vector which is in the same plane as it was before, but now - // // orthogonal to the two existing vectors. - // // up = cross(pointing, left) - // let up_vec = pointing.cross(&left_vec); - - // // +------+-+------+-+------+-+------+ ^ - // // | 1 |g| 2 |g| 3 |g| 4 | | - // // | |a| |a| |a| | y - // // | |p| |p| |p| | | - // // +------+-+------+-+------+-+------+ _ - // // <-cf-> x -> - // // - // // pointing vector is in the middle of the 'a' in the central gap. - - // let outer: Vector3 = rotate_around(&left_vec, up_vec, -lon_width / 2.0); - // let n2: Vector3 = rotate_around(&(-left_vec), up_vec, lon_width / 2.0); - - // let long_top: Vector3 = rotate_around(&up_vec, left_vec, y_width / 2.0); - // let long_bottom: Vector3 = rotate_around(&(-up_vec), left_vec, -y_width / 2.0); - - // // construct the 4 normal vectors - // Self { - // edge_normals: [n1.into(), n2.into(), n3.into(), n4.into()], - // frame, - // } - // } + /// x_width is the longer dimension in radians + #[allow(clippy::too_many_arguments)] + pub fn from_pointing( + x_width: f64, + y_width: f64, + _gap_fraction: f64, + pointing: Vector3, + rotation: f64, + observer: State, + side_id: u16, + stack_id: u8, + quad_id: u8, + loop_id: u8, + subloop_id: u8, + exposure_id: u8, + band: NeosBand, + ) -> Self { + // let chip_x_width = (1.0 - 3.0 * gap_fraction) * x_width / 4.0; + + // Rotate the Z axis to match the defined rotation angle, this vector is not + // orthogonal to the pointing vector, but is in the correct plane of the final + // up vector. + let up_vec = rotate_around(&Vector3::new(0.0, 0.0, 1.0), pointing, -rotation); + + // construct the vector orthogonal to the pointing and rotated z axis vectors. + let left_vec = pointing.cross(&up_vec); + + // Given the new left vector, and the existing orthogonal pointing vector, + // construct a new up vector which is in the same plane as it was before, but now + // orthogonal to the two existing vectors. + let up_vec = pointing.cross(&left_vec); + + // +------+-+------+-+------+-+------+ ^ + // | 1 |g| 2 |g| 3 |g| 4 | | + // | |a| |a| |a| | y + // | |p| |p| |p| | | + // +------+-+------+-+------+-+------+ _ + // <-cf-> x -> + // + // pointing vector is in the middle of the 'a' in the central gap. + + // the Y direction is bounded by 2 planes, calculate them one time + let y_top: Vector3 = rotate_around(&up_vec, left_vec, y_width / 2.0); + let y_bottom: Vector3 = rotate_around(&(-up_vec), left_vec, -y_width / 2.0); + + + // for each chip calculate the x bounds + let chip_1_a: Vector3 = rotate_around(&left_vec, up_vec, -x_width/2.0); + let chip_1_b: Vector3 = -rotate_around(&left_vec, up_vec, -x_width/4.0); + let chip_2_a: Vector3 = rotate_around(&left_vec, up_vec, -x_width/4.0); + let chip_2_b: Vector3 = -rotate_around(&left_vec, up_vec, 0.0); + let chip_3_a: Vector3 = rotate_around(&left_vec, up_vec, 0.0); + let chip_3_b: Vector3 = -rotate_around(&left_vec, up_vec, x_width/4.0); + let chip_4_a: Vector3 = rotate_around(&left_vec, up_vec, x_width/4.0); + let chip_4_b: Vector3 = -rotate_around(&left_vec, up_vec, x_width/2.0); + + // make the patches for each chip + let chip_1_patch = OnSkyRectangle::from_normals([chip_1_a.into(), y_top.into(), chip_1_b.into(), y_bottom.into()], observer.frame); + let chip_2_patch = OnSkyRectangle::from_normals([chip_2_a.into(), y_top.into(), chip_2_b.into(), y_bottom.into()], observer.frame); + let chip_3_patch = OnSkyRectangle::from_normals([chip_3_a.into(), y_top.into(), chip_3_b.into(), y_bottom.into()], observer.frame); + let chip_4_patch = OnSkyRectangle::from_normals([chip_4_a.into(), y_top.into(), chip_4_b.into(), y_bottom.into()], observer.frame); + + // make the chips + let chip_1 = NeosCmos{observer:observer.clone(), patch:chip_1_patch, rotation, side_id, stack_id, quad_id, loop_id, subloop_id, exposure_id, band, cmos_id:0}; + let chip_2 = NeosCmos{observer:observer.clone(), patch:chip_2_patch, rotation, side_id, stack_id, quad_id, loop_id, subloop_id, exposure_id, band, cmos_id:1}; + let chip_3 = NeosCmos{observer:observer.clone(), patch:chip_3_patch, rotation, side_id, stack_id, quad_id, loop_id, subloop_id, exposure_id, band, cmos_id:2}; + let chip_4 = NeosCmos{observer:observer.clone(), patch:chip_4_patch, rotation, side_id, stack_id, quad_id, loop_id, subloop_id, exposure_id, band, cmos_id:3}; + + // Put all the chips in a box for safe-keeping, try not to eat them all at once. + let chips = Box::new([chip_1, chip_2, chip_3, chip_4]); + Self { + chips, observer, rotation, side_id, stack_id, quad_id, loop_id, subloop_id, exposure_id, band + } + } /// Return the central pointing vector. pub fn pointing(&self) -> Vector3{ diff --git a/src/neospy_core/src/fov/patches.rs b/src/neospy_core/src/fov/patches.rs index 75d424d..4534754 100644 --- a/src/neospy_core/src/fov/patches.rs +++ b/src/neospy_core/src/fov/patches.rs @@ -90,6 +90,24 @@ pub struct SphericalPolygon { pub type OnSkyRectangle = SphericalPolygon<4>; impl OnSkyRectangle { + /// Construct a rectangular spherical polygon. + /// + /// # Arguments + /// + /// * `edge_normals` - Normal vectors which define the boundary of a polygon. + /// * `frame` - Coordinate frame of the rectangle. + pub fn from_normals( + edge_normals: [[f64; 3]; 4], + frame: Frame, + ) -> Self { + + // construct the 4 normal vectors + Self { + edge_normals, + frame, + } + } + /// Construct a rectangular spherical polygon. /// /// This constructs a new SphericalPolygon made up of a rectangular shape on the unit From 93f1e2f354ff9de5aae0a013fb7291500c318314 Mon Sep 17 00:00:00 2001 From: dahlend Date: Fri, 24 May 2024 16:42:43 -0700 Subject: [PATCH 06/12] gap functioning --- src/neospy/rust/fovs/definitions.rs | 3 ++- src/neospy_core/src/fov/neos.rs | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/neospy/rust/fovs/definitions.rs b/src/neospy/rust/fovs/definitions.rs index a07e242..bee0760 100644 --- a/src/neospy/rust/fovs/definitions.rs +++ b/src/neospy/rust/fovs/definitions.rs @@ -428,6 +428,7 @@ impl PyNeosVisit { pub fn new( x_width: f64, y_width: f64, + gap_angle: f64, pointing: VectorLike, rotation: f64, observer: PyState, @@ -441,7 +442,7 @@ impl PyNeosVisit { ) -> Self { let pointing = pointing.into_vector(crate::frame::PyFrames::Ecliptic); let pointing = pointing.raw.into(); - PyNeosVisit(fov::NeosVisit::from_pointing(x_width.to_radians(), y_width.to_radians(), 0.0, + PyNeosVisit(fov::NeosVisit::from_pointing(x_width.to_radians(), y_width.to_radians(), gap_angle.to_radians(), pointing, rotation.to_radians(), observer.0, diff --git a/src/neospy_core/src/fov/neos.rs b/src/neospy_core/src/fov/neos.rs index 5f69228..ca97ee1 100644 --- a/src/neospy_core/src/fov/neos.rs +++ b/src/neospy_core/src/fov/neos.rs @@ -228,7 +228,7 @@ impl NeosVisit { pub fn from_pointing( x_width: f64, y_width: f64, - _gap_fraction: f64, + gap_angle: f64, pointing: Vector3, rotation: f64, observer: State, @@ -240,8 +240,6 @@ impl NeosVisit { exposure_id: u8, band: NeosBand, ) -> Self { - // let chip_x_width = (1.0 - 3.0 * gap_fraction) * x_width / 4.0; - // Rotate the Z axis to match the defined rotation angle, this vector is not // orthogonal to the pointing vector, but is in the correct plane of the final // up vector. @@ -268,15 +266,16 @@ impl NeosVisit { let y_top: Vector3 = rotate_around(&up_vec, left_vec, y_width / 2.0); let y_bottom: Vector3 = rotate_around(&(-up_vec), left_vec, -y_width / 2.0); + let half_gap = gap_angle / 2.0; // for each chip calculate the x bounds let chip_1_a: Vector3 = rotate_around(&left_vec, up_vec, -x_width/2.0); - let chip_1_b: Vector3 = -rotate_around(&left_vec, up_vec, -x_width/4.0); - let chip_2_a: Vector3 = rotate_around(&left_vec, up_vec, -x_width/4.0); - let chip_2_b: Vector3 = -rotate_around(&left_vec, up_vec, 0.0); - let chip_3_a: Vector3 = rotate_around(&left_vec, up_vec, 0.0); - let chip_3_b: Vector3 = -rotate_around(&left_vec, up_vec, x_width/4.0); - let chip_4_a: Vector3 = rotate_around(&left_vec, up_vec, x_width/4.0); + let chip_1_b: Vector3 = -rotate_around(&left_vec, up_vec, -x_width/4.0 - half_gap); + let chip_2_a: Vector3 = rotate_around(&left_vec, up_vec, -x_width/4.0 + half_gap); + let chip_2_b: Vector3 = -rotate_around(&left_vec, up_vec, -half_gap); + let chip_3_a: Vector3 = rotate_around(&left_vec, up_vec, half_gap); + let chip_3_b: Vector3 = -rotate_around(&left_vec, up_vec, x_width/4.0 - half_gap); + let chip_4_a: Vector3 = rotate_around(&left_vec, up_vec, x_width/4.0 + half_gap); let chip_4_b: Vector3 = -rotate_around(&left_vec, up_vec, x_width/2.0); // make the patches for each chip From 4f9d64924566e1fcc0492a514f183d930809b1bb Mon Sep 17 00:00:00 2001 From: dahlend Date: Tue, 28 May 2024 10:41:46 -0700 Subject: [PATCH 07/12] cleanup wise docs and imports --- src/neospy/rust/flux/common.rs | 20 ++++---- src/neospy/wise.py | 86 +++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/neospy/rust/flux/common.rs b/src/neospy/rust/flux/common.rs index 7089447..cef1ba3 100644 --- a/src/neospy/rust/flux/common.rs +++ b/src/neospy/rust/flux/common.rs @@ -472,28 +472,32 @@ pub fn wise_frm_flux_py( .collect() } +/// Calculate the W1 black body color correction for the given temperature in kelvin. #[pyfunction] #[pyo3(name = "w1_color_correction")] -pub fn w1_color_correction_py(temps: f64) -> f64 { - w1_color_correction(temps) +pub fn w1_color_correction_py(temp: f64) -> f64 { + w1_color_correction(temp) } +/// Calculate the W2 black body color correction for the given temperature in kelvin. #[pyfunction] #[pyo3(name = "w2_color_correction")] -pub fn w2_color_correction_py(temps: f64) -> f64 { - w2_color_correction(temps) +pub fn w2_color_correction_py(temp: f64) -> f64 { + w2_color_correction(temp) } +/// Calculate the W3 black body color correction for the given temperature in kelvin. #[pyfunction] #[pyo3(name = "w3_color_correction")] -pub fn w3_color_correction_py(temps: f64) -> f64 { - w3_color_correction(temps) +pub fn w3_color_correction_py(temp: f64) -> f64 { + w3_color_correction(temp) } +/// Calculate the W4 black body color correction for the given temperature in kelvin. #[pyfunction] #[pyo3(name = "w4_color_correction")] -pub fn w4_color_correction_py(temps: f64) -> f64 { - w4_color_correction(temps) +pub fn w4_color_correction_py(temp: f64) -> f64 { + w4_color_correction(temp) } #[pyfunction] diff --git a/src/neospy/wise.py b/src/neospy/wise.py index 801285e..b6807b6 100644 --- a/src/neospy/wise.py +++ b/src/neospy/wise.py @@ -25,7 +25,14 @@ from .irsa import IRSA_URL, query_irsa_tap # pylint: disable-next=import-error -from ._core import WiseCmos, FOVList # type: ignore +from ._core import ( # type: ignore + WiseCmos, + FOVList, + w1_color_correction, + w2_color_correction, + w3_color_correction, + w4_color_correction, +) __all__ = [ "MISSION_PHASES", @@ -34,11 +41,15 @@ "plot_frames", "fetch_WISE_frame", "MissionPhase", + "w1_color_correction", + "w2_color_correction", + "w3_color_correction", + "w4_color_correction", ] +# All constants below are indexed as follows W1 = [0], W2 = [1]. W3 = [2], W4 = [3] -# This table contains calibrated flux correction values for the 4 different WISE bands. -COLOR_CORR = np.array( +_COLOR_CORR = np.array( [ # Tbb K_W1 K_W2 K_W3 K_W4 [100, 17.2062, 3.9096, 2.6588, 1.0032], [110, 9.3213, 3.1120, 2.1424, 0.9955], @@ -73,54 +84,75 @@ [400, 1.1316, 1.0229, 0.8622, 0.9903], ] ) -# All lists below are indexed as follows W1 = [0], W2 = [1]. W3 = [2], W4 = [3] +""" +This table contains calibrated flux correction values for the 4 different WISE bands. + +This is provided as reference, however neospy has built in interpolation methods for +these black-body correction terms. See :func:`w1_color_correction` or similarly named +functions for other bands. +""" +# The data in the color correction table above is relatively well fit to a 1 / f(x) +# where f is a 4th order polynomial. It is the least accurate for the final band, but +# W4 is almost a constant. +# These fits perform much better than linear interpolation except near 100k. +# +# The values above were fit to polynomial equations as defined below, the results of +# which were hard-coded into the rust backend to allow for fast computation. +# These functions should not be referenced directly, as they also exist in the rust +# code and are only left here for reference to how the rust was constructed. +# +# from numpy.polynomial import Polynomial +# _COLOR_FITS = [ +# Polynomial.fit(_COLOR_CORR[:, 0], 1 / _COLOR_CORR[:, 1], 4), +# Polynomial.fit(_COLOR_CORR[:, 0], 1 / _COLOR_CORR[:, 2], 4), +# Polynomial.fit(_COLOR_CORR[:, 0], 1 / _COLOR_CORR[:, 3], 4), +# Polynomial.fit(_COLOR_CORR[:, 0], 1 / _COLOR_CORR[:, 4], 4), +# ] + -# Flux in the reflection model should be scaled by these values. SUN_COLOR_CORRECTION: list[float] = [1.0049, 1.0193, 1.0024, 1.0012] +""" +Flux in the reflected light model should be scaled by these values. +""" -# Non-color corrected values for zero mag corrections in Janskys. -# magnitude can then be computed via -2.5 log10(flux Jy / zero_point) ZERO_MAGS: list[float] = [306.681, 170.663, 29.0448, 8.2839] +""" +Non-color corrected values for zero mag corrections in Janskys. +Magnitude can then be computed via -2.5 log10(flux Jy / zero_point) +""" -# Color Corrected Zero Mags in units of Jy ZERO_MAGS_COLOR_CORRECTED: list[float] = [ ZERO_MAGS[0], ZERO_MAGS[1], 1.08 * ZERO_MAGS[2], 0.96 * ZERO_MAGS[3], ] +"""Color Corrected Zero Mags in units of Jy""" -# Non-color corrected values for the effective central wavelength of the bands (nm) BAND_WAVELENGTHS: list[float] = [3352.6, 4602.8, 11560.8, 22088.3] +"""Non-color corrected values for the effective central wavelength of the bands (nm)""" -# Color Corrected Band Wavelengths in units of nm BAND_WAVELENGTHS_COLOR_CORRECTED: list[float] = [ BAND_WAVELENGTHS[0], BAND_WAVELENGTHS[1], 0.96 * BAND_WAVELENGTHS[2], 1.025 * BAND_WAVELENGTHS[3], ] +"""Color Corrected Band Wavelengths in units of nm""" -# The data in the color correction table above is relatively well fit to a 1 / f(x) -# where f is a 4th order polynomial. It is the least accurate for the final band, but -# W4 is almost a constant. These fits perform much better than linear except near 100k. -# These functions should not be referenced directly, as they also exist in the rust -# code and are only left here for reference to how the rust was constructed. - -# from numpy.polynomial import Polynomial -# _COLOR_FITS = [ -# Polynomial.fit(WISE_COLOR_CORR[:, 0], 1 / WISE_COLOR_CORR[:, 1], 4), -# Polynomial.fit(WISE_COLOR_CORR[:, 0], 1 / WISE_COLOR_CORR[:, 2], 4), -# Polynomial.fit(WISE_COLOR_CORR[:, 0], 1 / WISE_COLOR_CORR[:, 3], 4), -# Polynomial.fit(WISE_COLOR_CORR[:, 0], 1 / WISE_COLOR_CORR[:, 4], 4), -# ] - -# WISE Field of view is ~47 arc-minutes square. FOV_WIDTH: float = 47 / 60 +""" +Approximate width of a WISE chip FOV, this slightly over-estimates the true FOV. +This is 47 arc-minutes. +""" -# Convert directly from DN to Jy using this Jy/DN conversion factor -# from https://wise2.ipac.caltech.edu/docs/release/prelim/expsup/sec2_3f.html DN_TO_JY = [1.9350e-06, 2.7048e-06, 2.9045e-06, 5.2269e-05] +""" +Convert directly from DN to Jy using this Jy/DN conversion factor. + +These values came from: +https://wise2.ipac.caltech.edu/docs/release/prelim/expsup/sec2_3f.html +""" MissionPhase = namedtuple( From a2d098791ce15faadbc08ebf4326c8097b244868 Mon Sep 17 00:00:00 2001 From: dahlend Date: Tue, 28 May 2024 10:42:43 -0700 Subject: [PATCH 08/12] documentation --- CHANGELOG.md | 10 +++++- src/neospy/__init__.py | 4 +++ src/neospy/fov.py | 11 ++++++- src/neospy/rust/fovs/definitions.rs | 51 +++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af969af..0ae6a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Added support for NEOS Visit FOVs, which are joint FOVs containing 4 rectangles. +- Added python interface to WISE Color Correction functions. + ## [0.2.2] - 2024 - 5 - 20 ### Added @@ -72,6 +79,7 @@ Initial Release Along with many helpful interfaces to web tools such as JPL Horizons or IPAC's IRSA. -[0.2.2]: https://github.com/IPAC-SW/neospy/tree/main +[Unreleased]: https://github.com/IPAC-SW/neospy/tree/main +[0.2.2]: https://github.com/IPAC-SW/neospy/releases/tag/v0.2.2 [0.2.1]: https://github.com/IPAC-SW/neospy/releases/tag/v0.2.1 [0.2.0]: https://github.com/IPAC-SW/neospy/releases/tag/v0.2.0 diff --git a/src/neospy/__init__.py b/src/neospy/__init__.py index db10f9f..dde376a 100644 --- a/src/neospy/__init__.py +++ b/src/neospy/__init__.py @@ -23,6 +23,8 @@ propagate_n_body, propagate_two_body, moid, + spice_visible, + state_visible, ) from .time import Time from .conversion import ( @@ -66,6 +68,8 @@ "compute_diameter", "compute_semi_major", "compute_aphelion", + "spice_visible", + "state_visible", "mag_to_flux", "flux_to_mag", "Vector", diff --git a/src/neospy/fov.py b/src/neospy/fov.py index 4e60b14..ab60f02 100644 --- a/src/neospy/fov.py +++ b/src/neospy/fov.py @@ -1,6 +1,7 @@ # pylint: disable-next=import-error from ._core import ( # type: ignore NeosCmos, + NeosVisit, WiseCmos, ZtfCcdQuad, ZtfField, @@ -9,4 +10,12 @@ ) -__all__ = ["NeosCmos", "WiseCmos", "ZtfCcdQuad", "ZtfField", "RectangleFOV", "FOVList"] +__all__ = [ + "NeosCmos", + "NeosVisit", + "WiseCmos", + "ZtfCcdQuad", + "ZtfField", + "RectangleFOV", + "FOVList", +] diff --git a/src/neospy/rust/fovs/definitions.rs b/src/neospy/rust/fovs/definitions.rs index bee0760..0f7265c 100644 --- a/src/neospy/rust/fovs/definitions.rs +++ b/src/neospy/rust/fovs/definitions.rs @@ -424,6 +424,48 @@ impl PyNeosCmos { #[pymethods] #[allow(clippy::too_many_arguments)] impl PyNeosVisit { + /// Construct a new NEOS Visit. + /// + /// This is a collection of 4 NeosCmos fields of view, representing the + /// 4 chips of each band. + /// + /// +------+-+------+-+------+-+------+ ^ + /// | 1 |g| 2 |g| 3 |g| 4 | | + /// | |a| |a| |a| | y + /// | |p| |p| |p| | | + /// +======+=+======+=+======+=+======+ _ + /// |- x -> + /// + /// Where the bottom is the sun shield. + /// + /// Parameters + /// ---------- + /// x_width : + /// Width of the long axis of the Visit in degrees. + /// y_width : + /// Width of the short axis of the Visit in degrees. + /// gap_angle : + /// Width of the gap between chips in degrees. + /// pointing : + /// Vector defining the center of the FOV. + /// rotation : + /// Rotation of the FOV in degrees. + /// observer : + /// State of the observer. + /// side_id : + /// Side ID indicating where we are in the survey. + /// stack_id : + /// Stack ID indicating where we are in the survey. + /// quad_id : + /// Quad ID indicating where we are in the survey. + /// loop_id : + /// Loop ID indicating where we are in the survey. + /// subloop_id : + /// Subloop ID indicating where we are in the survey. + /// exposure_id : + /// Exposure number indicating where we are in the survey. + /// band : + /// Band, can be either 1 or 2 to represent NC1/NC2. #[new] pub fn new( x_width: f64, @@ -456,11 +498,13 @@ impl PyNeosVisit { )) } + /// Observer State. #[getter] pub fn observer(&self) -> PyState { self.0.observer().clone().into() } + /// Direction that the observer is looking. #[getter] pub fn pointing(&self) -> Vector { Vector::new( @@ -469,36 +513,43 @@ impl PyNeosVisit { ) } + /// Metadata about where this FOV is in the Survey. #[getter] pub fn side_id(&self) -> u16 { self.0.side_id } + /// Metadata about where this FOV is in the Survey. #[getter] pub fn stack_id(&self) -> u8 { self.0.stack_id } + /// Metadata about where this FOV is in the Survey. #[getter] pub fn quad_id(&self) -> u8 { self.0.quad_id } + /// Metadata about where this FOV is in the Survey. #[getter] pub fn loop_id(&self) -> u8 { self.0.loop_id } + /// Metadata about where this FOV is in the Survey. #[getter] pub fn subloop_id(&self) -> u8 { self.0.subloop_id } + /// Metadata about where this FOV is in the Survey. #[getter] pub fn exposure_id(&self) -> u8 { self.0.exposure_id } + /// Rotation angle of the FOV in degrees. #[getter] pub fn rotation(&self) -> f64 { self.0.rotation From dcd521e4c1ee068a4041dfe324599803f909c7af Mon Sep 17 00:00:00 2001 From: dahlend Date: Tue, 28 May 2024 10:51:36 -0700 Subject: [PATCH 09/12] docs --- src/neospy/fov.py | 2 ++ src/neospy/rust/fovs/checks.rs | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/neospy/fov.py b/src/neospy/fov.py index ab60f02..7d5798c 100644 --- a/src/neospy/fov.py +++ b/src/neospy/fov.py @@ -7,6 +7,7 @@ ZtfField, RectangleFOV, FOVList, + fov_static_check, ) @@ -18,4 +19,5 @@ "ZtfField", "RectangleFOV", "FOVList", + "fov_static_check", ] diff --git a/src/neospy/rust/fovs/checks.rs b/src/neospy/rust/fovs/checks.rs index 9bb2f2a..c47415b 100644 --- a/src/neospy/rust/fovs/checks.rs +++ b/src/neospy/rust/fovs/checks.rs @@ -80,6 +80,20 @@ pub fn fov_spk_checks_py(obj_ids: Vec, fovs: FOVListLike) -> Vec, fovs: FOVListLike) -> Vec<(Vec, AllowedFOV)> { From 10422fef48e2bbf8cf151a41ed538cb1ed5c4e57 Mon Sep 17 00:00:00 2001 From: dahlend Date: Tue, 28 May 2024 10:52:34 -0700 Subject: [PATCH 10/12] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae6a6b..8a72e82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for NEOS Visit FOVs, which are joint FOVs containing 4 rectangles. - Added python interface to WISE Color Correction functions. +### Changed + +- Restructured the Rust FOVs to be organized by observatory. + ## [0.2.2] - 2024 - 5 - 20 ### Added From 34313bbc25ef0dd6719363158a63c7bf155e562a Mon Sep 17 00:00:00 2001 From: dahlend Date: Tue, 28 May 2024 10:52:38 -0700 Subject: [PATCH 11/12] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a72e82..ced0932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for NEOS Visit FOVs, which are joint FOVs containing 4 rectangles. - Added python interface to WISE Color Correction functions. +- Added support for querying static sky sources in FOVs. ### Changed From 59f02fd1aab8cac85361ce7904f6d3615c10d45e Mon Sep 17 00:00:00 2001 From: dahlend Date: Tue, 28 May 2024 10:57:16 -0700 Subject: [PATCH 12/12] import typo --- src/neospy/fov.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neospy/fov.py b/src/neospy/fov.py index 7d5798c..cec450b 100644 --- a/src/neospy/fov.py +++ b/src/neospy/fov.py @@ -7,7 +7,7 @@ ZtfField, RectangleFOV, FOVList, - fov_static_check, + fov_static_checks, ) @@ -19,5 +19,5 @@ "ZtfField", "RectangleFOV", "FOVList", - "fov_static_check", + "fov_static_checks", ]