use crate::io::InputOutputError;
use flate2::read::GzDecoder;
use hifitime::prelude::*;
use serde::{Deserialize, Serialize};
use serde_dhall::{SimpleType, StaticType};
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use std::str::FromStr;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all))]
pub enum StaticSpaceWeather {
SolarMinimum(),
SolarAverage(),
SolarMaximum(),
Custom { f107: f64, ap: f64, kp: f64 },
}
impl Default for StaticSpaceWeather {
fn default() -> Self {
Self::SolarAverage()
}
}
impl StaticSpaceWeather {
pub fn resolve_f107(&self, value: Option<f64>) -> f64 {
value.unwrap_or(match self {
Self::SolarMinimum() => 65.0,
Self::SolarAverage() => 130.0,
Self::SolarMaximum() => 200.0,
Self::Custom { f107, .. } => *f107,
})
}
pub fn resolve_ap(&self, value: Option<f64>) -> f64 {
value.unwrap_or(match self {
Self::SolarMinimum() => 4.0,
Self::SolarAverage() => 15.0,
Self::SolarMaximum() => 30.0,
Self::Custom { ap, .. } => *ap,
})
}
pub fn resolve_kp(&self, value: Option<f64>) -> f64 {
value.unwrap_or(match self {
Self::SolarMinimum() => 1.0,
Self::SolarAverage() => 3.0,
Self::SolarMaximum() => 4.3,
Self::Custom { kp, .. } => *kp,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, StaticType, PartialEq)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all))]
pub struct RawSpaceWeatherRow {
#[serde(rename = "DATE")]
pub date: String,
#[serde(rename = "BSRN")]
pub bsrn: u32,
#[serde(rename = "ND")]
pub nd: u32,
#[serde(rename = "KP1")]
pub kp1: Option<f64>,
#[serde(rename = "KP2")]
pub kp2: Option<f64>,
#[serde(rename = "KP3")]
pub kp3: Option<f64>,
#[serde(rename = "KP4")]
pub kp4: Option<f64>,
#[serde(rename = "KP5")]
pub kp5: Option<f64>,
#[serde(rename = "KP6")]
pub kp6: Option<f64>,
#[serde(rename = "KP7")]
pub kp7: Option<f64>,
#[serde(rename = "KP8")]
pub kp8: Option<f64>,
#[serde(rename = "KP_SUM")]
pub kp_sum: Option<f64>,
#[serde(rename = "AP1")]
pub ap1: Option<f64>,
#[serde(rename = "AP2")]
pub ap2: Option<f64>,
#[serde(rename = "AP3")]
pub ap3: Option<f64>,
#[serde(rename = "AP4")]
pub ap4: Option<f64>,
#[serde(rename = "AP5")]
pub ap5: Option<f64>,
#[serde(rename = "AP6")]
pub ap6: Option<f64>,
#[serde(rename = "AP7")]
pub ap7: Option<f64>,
#[serde(rename = "AP8")]
pub ap8: Option<f64>,
#[serde(rename = "AP_AVG")]
pub ap_avg: Option<f64>,
#[serde(rename = "CP")]
pub cp: Option<f64>,
#[serde(rename = "C9")]
pub c9: Option<u16>,
#[serde(rename = "ISN")]
pub isn: Option<u32>,
#[serde(rename = "F10.7_OBS")]
pub f107_obs: f64,
#[serde(rename = "F10.7_ADJ")]
pub f107_adj: f64,
#[serde(rename = "F10.7_DATA_TYPE")]
pub f107_data_type: String,
#[serde(rename = "F10.7_OBS_CENTER81")]
pub f107_obs_center81: Option<f64>,
#[serde(rename = "F10.7_OBS_LAST81")]
pub f107_obs_last81: Option<f64>,
#[serde(rename = "F10.7_ADJ_CENTER81")]
pub f107_adj_center81: Option<f64>,
#[serde(rename = "F10.7_ADJ_LAST81")]
pub f107_adj_last81: Option<f64>,
}
impl RawSpaceWeatherRow {
#[inline]
pub fn kp_bins(&self, fallback: StaticSpaceWeather) -> [f64; 8] {
let daily_mean_kp = self
.kp_sum
.map(|sum| sum / 80.0)
.unwrap_or_else(|| fallback.resolve_kp(None));
let resolve = |bin: Option<f64>| bin.map(|v| v / 10.0).unwrap_or(daily_mean_kp);
[
resolve(self.kp1),
resolve(self.kp2),
resolve(self.kp3),
resolve(self.kp4),
resolve(self.kp5),
resolve(self.kp6),
resolve(self.kp7),
resolve(self.kp8),
]
}
#[inline]
pub fn ap_bins(&self, fallback: StaticSpaceWeather) -> [f64; 8] {
let daily_mean_ap = self.ap_avg.unwrap_or_else(|| fallback.resolve_ap(None));
let resolve = |bin: Option<f64>| bin.unwrap_or(daily_mean_ap);
[
resolve(self.ap1),
resolve(self.ap2),
resolve(self.ap3),
resolve(self.ap4),
resolve(self.ap5),
resolve(self.ap6),
resolve(self.ap7),
resolve(self.ap8),
]
}
}
#[cfg_attr(feature = "python", pyclass(from_py_object))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct SpaceWeatherData {
#[serde(with = "as_vec")]
pub records: BTreeMap<Epoch, RawSpaceWeatherRow>,
pub fallback: StaticSpaceWeather,
}
impl SpaceWeatherData {
pub fn from_static_weather(weather: StaticSpaceWeather) -> Self {
Self {
records: BTreeMap::new(),
fallback: weather,
}
}
pub fn from_csv_file<P: AsRef<Path>>(
path: P,
fallback: StaticSpaceWeather,
) -> Result<Self, InputOutputError> {
let path_ref = path.as_ref();
let file = File::open(path_ref).map_err(|e| InputOutputError::StdIOError {
source: e,
action: "reading space weather file",
})?;
let mut buf_reader = BufReader::new(file);
let is_gzipped = match buf_reader.fill_buf() {
Ok(header) => header.len() >= 2 && header[0] == 0x1f && header[1] == 0x8b,
Err(source) => {
return Err(InputOutputError::StdIOError {
source,
action: "reading header of CSV file",
});
}
};
let stream: Box<dyn Read> = if is_gzipped {
Box::new(GzDecoder::new(buf_reader))
} else {
Box::new(buf_reader)
};
let mut rdr = csv::ReaderBuilder::new()
.trim(csv::Trim::All)
.from_reader(stream);
let mut records = BTreeMap::new();
for result in rdr.deserialize() {
let record: RawSpaceWeatherRow =
result.map_err(|source| InputOutputError::CsvData {
source,
action: "reading space weather",
})?;
if let Ok(epoch) = Epoch::from_str(&format!("{}T00:00:00 UTC", record.date)) {
records.insert(epoch, record);
}
}
Ok(Self { records, fallback })
}
pub fn raw_daily_record(&self, midnight_epoch: Epoch) -> Option<&RawSpaceWeatherRow> {
self.records.get(&midnight_epoch)
}
}
#[cfg_attr(feature = "python", pymethods)]
impl SpaceWeatherData {
pub fn msise_weather(&self, epoch: Epoch) -> Msise00DailyWeather {
let target_midnight = epoch.with_hms(0, 0, 0);
let current_day = self.records.get(&target_midnight);
let seconds_into_day = (epoch - target_midnight).to_seconds();
let bin_idx = ((seconds_into_day / (Unit::Hour * 3).to_seconds()).floor() as usize).min(7);
let ap_history = self.build_ap_history(target_midnight, bin_idx);
let f107_daily = self.fallback.resolve_f107(current_day.map(|r| r.f107_obs));
let f107_avg = current_day
.and_then(|r| r.f107_obs_center81.or(r.f107_adj_center81))
.unwrap_or(f107_daily);
let ap_daily = self.fallback.resolve_ap(current_day.and_then(|r| r.ap_avg));
Msise00DailyWeather {
f107_daily_sfu: f107_daily,
f107_avg_sfu: f107_avg,
ap_daily,
ap_3hour_history: ap_history,
}
}
fn build_ap_history(&self, midnight: Epoch, bin_idx: usize) -> [f64; 7] {
let one_day = Unit::Day * 1.0;
let get_ap_bins = |offset_days: f64| -> [f64; 8] {
let target_epoch = midnight - one_day * offset_days;
match self.records.get(&target_epoch) {
Some(row) => row.ap_bins(self.fallback),
None => [self.fallback.resolve_ap(None); 8],
}
};
let day_0_row = self.records.get(&midnight);
let daily_ap = self.fallback.resolve_ap(day_0_row.and_then(|r| r.ap_avg));
let day_0_bins = match day_0_row {
Some(row) => row.ap_bins(self.fallback),
None => [self.fallback.resolve_ap(None); 8],
};
let mut continuous_ap = [0.0; 32];
continuous_ap[0..8].copy_from_slice(&get_ap_bins(3.0));
continuous_ap[8..16].copy_from_slice(&get_ap_bins(2.0));
continuous_ap[16..24].copy_from_slice(&get_ap_bins(1.0));
continuous_ap[24..32].copy_from_slice(&day_0_bins);
let idx = 24 + bin_idx;
let avg_slice = |start: usize, end: usize| -> f64 {
let slice = &continuous_ap[start..=end];
slice.iter().sum::<f64>() / slice.len() as f64
};
[
daily_ap, continuous_ap[idx], continuous_ap[idx - 1], continuous_ap[idx - 2], continuous_ap[idx - 3], avg_slice(idx - 11, idx - 4), avg_slice(idx - 19, idx - 12), ]
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl SpaceWeatherData {
#[new]
fn py_new(
path: Option<PathBuf>,
fallback: Option<StaticSpaceWeather>,
) -> Result<Self, InputOutputError> {
if let Some(path) = path {
Self::from_csv_file(path, fallback.unwrap_or_default())
} else if let Some(weather) = fallback {
Ok(Self::from_static_weather(weather))
} else {
Err(InputOutputError::MissingData {
which:
"must provide at least either a path to a weather file or a fallback, or both"
.to_string(),
})
}
}
fn __str__(&self) -> String {
format!("{self}")
}
fn __repr__(&self) -> String {
format!("{self} @ {self:p}")
}
}
impl fmt::Display for SpaceWeatherData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.records.is_empty() {
write!(f, "empty SpaceWeatherData")
} else {
write!(
f,
"SpaceWeatherData from {} to {} ({:?})",
self.records.first_key_value().unwrap().0,
self.records.last_key_value().unwrap().0,
self.fallback
)
}
}
}
impl StaticType for SpaceWeatherData {
fn static_type() -> SimpleType {
let mut rcrd = HashMap::new();
rcrd.insert("epoch".to_string(), String::static_type());
rcrd.insert("raw_weather".to_string(), RawSpaceWeatherRow::static_type());
SimpleType::List(Box::new(SimpleType::Record(rcrd)))
}
}
mod as_vec {
use super::*;
use serde::{Deserializer, Serializer};
#[derive(Serialize, Deserialize)]
struct WeatherEntry {
epoch: Epoch,
raw_weather: RawSpaceWeatherRow,
}
pub fn serialize<S>(
map: &BTreeMap<Epoch, RawSpaceWeatherRow>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let vec: Vec<WeatherEntry> = map
.iter()
.map(|(epoch, raw_weather)| WeatherEntry {
epoch: *epoch,
raw_weather: raw_weather.clone(),
})
.collect();
vec.serialize(serializer)
}
pub fn deserialize<'de, D>(
deserializer: D,
) -> Result<BTreeMap<Epoch, RawSpaceWeatherRow>, D::Error>
where
D: Deserializer<'de>,
{
use serde::Deserialize;
let vec: Vec<WeatherEntry> = Vec::deserialize(deserializer)?;
let mut rcrd = BTreeMap::new();
for entry in vec {
rcrd.insert(entry.epoch, entry.raw_weather);
}
Ok(rcrd)
}
}
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub struct Msise00DailyWeather {
pub f107_daily_sfu: f64,
pub f107_avg_sfu: f64,
pub ap_daily: f64,
pub ap_3hour_history: [f64; 7],
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl Msise00DailyWeather {
fn __str__(&self) -> String {
format!("{self:?}")
}
fn __repr__(&self) -> String {
format!("{self:?} @ {self:p}")
}
}