use crate::time::{ModifiedJulianDate, Period, MJD};
use qtty::*;
use super::extrema::{self, ExtremumKind};
type Mjd = ModifiedJulianDate;
#[inline]
fn opposite_sign<V: Unit>(a: Quantity<V>, b: Quantity<V>) -> bool {
a.signum() * b.signum() < 0.0
}
pub fn fixed_step_brackets<V, F>(
period: Period<MJD>,
step: Days,
f: &F,
threshold: Quantity<V>,
) -> Vec<Period<MJD>>
where
V: Unit,
F: Fn(ModifiedJulianDate) -> Quantity<V>,
{
let g = |t: Mjd| -> Quantity<V> { f(t) - threshold };
let mut brackets = Vec::new();
let mut t = period.start;
let mut prev = g(t);
while t < period.end {
let next_t = (t + step).min(period.end);
let next_v = g(next_t);
if opposite_sign(prev, next_v) {
brackets.push(Period::new(t, next_t));
}
t = next_t;
prev = next_v;
}
brackets
}
pub fn adaptive_step_brackets<V, F>(
period: Period<MJD>,
initial_step: Days,
min_step: Days,
f: &F,
threshold: Quantity<V>,
) -> Vec<Period<MJD>>
where
V: Unit,
F: Fn(ModifiedJulianDate) -> Quantity<V>,
{
let g = |t: Mjd| -> Quantity<V> { f(t) - threshold };
let mut brackets = Vec::new();
struct Frame<V: Unit> {
period: Period<MJD>,
g_lo: Quantity<V>,
g_hi: Quantity<V>,
}
let mut stack: Vec<Frame<V>> = Vec::new();
let mut t = period.start;
let mut prev = g(t);
while t < period.end {
let next_t = (t + initial_step).min(period.end);
let next_v = g(next_t);
stack.push(Frame {
period: Period::new(t, next_t),
g_lo: prev,
g_hi: next_v,
});
t = next_t;
prev = next_v;
}
while let Some(frame) = stack.pop() {
if opposite_sign(frame.g_lo, frame.g_hi) {
let width = frame.period.end - frame.period.start;
if width <= min_step + min_step {
brackets.push(frame.period);
} else {
let mid = frame.period.start + width * 0.5;
let g_mid = g(mid);
stack.push(Frame {
period: Period::new(frame.period.start, mid),
g_lo: frame.g_lo,
g_hi: g_mid,
});
stack.push(Frame {
period: Period::new(mid, frame.period.end),
g_lo: g_mid,
g_hi: frame.g_hi,
});
}
}
}
brackets.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
brackets
}
pub fn extrema_based_brackets<V, F>(
period: Period<MJD>,
extrema_step: Days,
f: &F,
threshold: Quantity<V>,
) -> Vec<Period<MJD>>
where
V: Unit,
F: Fn(ModifiedJulianDate) -> Quantity<V>,
{
let extrema = extrema::find_extrema(period, extrema_step, f);
let mut brackets = Vec::new();
for ext in &extrema {
if ext.kind == ExtremumKind::Maximum && ext.value > threshold {
let rise_bracket =
search_crossing_backward(Period::new(period.start, ext.t), f, threshold);
if let Some(br) = rise_bracket {
brackets.push(br);
}
let set_bracket = search_crossing_forward(Period::new(ext.t, period.end), f, threshold);
if let Some(br) = set_bracket {
brackets.push(br);
}
}
}
brackets.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
brackets.dedup_by(|a, b| {
const TOL: Days = Days::new(1e-8); let dt_start = (a.start - b.start).abs();
let dt_end = (a.end - b.end).abs();
dt_start < TOL && dt_end < TOL
});
brackets
}
fn search_crossing_backward<V, F>(
search_period: Period<MJD>,
f: &F,
threshold: Quantity<V>,
) -> Option<Period<MJD>>
where
V: Unit,
F: Fn(ModifiedJulianDate) -> Quantity<V>,
{
let g = |t: Mjd| f(t) - threshold;
let range = search_period.duration();
let mut bracket = Period::new(search_period.end, search_period.end);
let mut g_hi = g(bracket.end);
let mut step = range * 0.1;
if step < 1e-10 {
step = range * 0.5;
}
bracket.start = (bracket.end - step).max(search_period.start);
let mut g_lo = g(bracket.start);
while bracket.start > search_period.start {
if opposite_sign(g_lo, g_hi) {
return Some(bracket);
}
bracket.end = bracket.start;
g_hi = g_lo;
bracket.start = (bracket.start - step).max(search_period.start);
g_lo = g(bracket.start);
}
if opposite_sign(g_lo, g_hi) {
Some(bracket)
} else {
None
}
}
fn search_crossing_forward<V, F>(
search_period: Period<MJD>,
f: &F,
threshold: Quantity<V>,
) -> Option<Period<MJD>>
where
V: Unit,
F: Fn(ModifiedJulianDate) -> Quantity<V>,
{
let g = |t: Mjd| f(t) - threshold;
let range = search_period.duration();
let mut bracket = Period::new(search_period.start, search_period.start);
let mut g_lo = g(bracket.start);
let mut step = range * 0.1;
if step < 1e-10 {
step = range * 0.5;
}
bracket.end = (bracket.start + step).min(search_period.end);
let mut g_hi = g(bracket.end);
while bracket.end < search_period.end {
if opposite_sign(g_lo, g_hi) {
return Some(bracket);
}
bracket.start = bracket.end;
g_lo = g_hi;
bracket.end = (bracket.end + step).min(search_period.end);
g_hi = g(bracket.end);
}
if opposite_sign(g_lo, g_hi) {
Some(bracket)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use qtty::Radian;
type Radians = Quantity<Radian>;
fn mjd(v: f64) -> Mjd {
Mjd::new(v)
}
fn period(a: f64, b: f64) -> Period<MJD> {
Period::new(mjd(a), mjd(b))
}
fn mjd_f64(t: Mjd) -> f64 {
t.quantity().value()
}
#[test]
fn fixed_step_finds_sine_crossings() {
let f = |t: Mjd| Radians::new((2.0 * std::f64::consts::PI * (mjd_f64(t) + 0.03)).sin());
let brackets =
fixed_step_brackets(period(0.0, 1.0), Days::new(0.05), &f, Radians::new(0.0));
assert!(
brackets.len() >= 2,
"got {} brackets: {:?}",
brackets.len(),
brackets
);
}
#[test]
fn adaptive_step_finds_crossings() {
let f = |t: Mjd| Radians::new((2.0 * std::f64::consts::PI * (mjd_f64(t) + 0.03)).sin());
let brackets = adaptive_step_brackets(
period(0.0, 1.0),
Days::new(0.2),
Days::new(0.01),
&f,
Radians::new(0.0),
);
assert!(brackets.len() >= 2, "got {} brackets", brackets.len());
}
#[test]
fn extrema_based_finds_satellite_pass() {
let f = |t: Mjd| Radians::new(-2.0 * (mjd_f64(t) - 5.0).powi(2) + 30.0);
let brackets =
extrema_based_brackets(period(0.0, 10.0), Days::new(0.5), &f, Radians::new(10.0));
assert!(brackets.len() >= 2, "got {} brackets", brackets.len());
for br in &brackets {
let g_lo = f(br.start) - Radians::new(10.0);
let g_hi = f(br.end) - Radians::new(10.0);
assert!(
g_lo.signum() * g_hi.signum() <= 0.0,
"bracket [{}, {}] doesn't contain crossing: g_lo={}, g_hi={}",
br.start,
br.end,
g_lo,
g_hi
);
}
}
#[test]
fn extrema_based_no_pass_above_threshold() {
let f = |t: Mjd| Radians::new(-2.0 * (mjd_f64(t) - 5.0).powi(2) + 5.0);
let brackets =
extrema_based_brackets(period(0.0, 10.0), Days::new(0.5), &f, Radians::new(10.0));
assert!(brackets.is_empty(), "no passes above threshold");
}
#[test]
fn fixed_step_no_crossings() {
let brackets = fixed_step_brackets(
period(0.0, 10.0),
Days::new(1.0),
&|_: Mjd| Radians::new(5.0),
Radians::new(0.0),
);
assert!(brackets.is_empty());
}
}