diff --git a/Cargo.lock b/Cargo.lock index e72800c7e32..887dbe2af8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arbitrary" version = "1.4.1" @@ -123,6 +129,37 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "argmin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760a49d596b18b881d2fe6e9e6da4608fa64d4a7653ef5cd43bfaa4da018d596" +dependencies = [ + "anyhow", + "argmin-math", + "instant", + "num-traits", + "paste", + "rand 0.8.5", + "rand_xoshiro", + "thiserror 1.0.69", +] + +[[package]] +name = "argmin-math" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93a0d0269b60bd1cd674de70314e3f0da97406cf8c1936ce760d2a46e0f13fe" +dependencies = [ + "anyhow", + "cfg-if", + "num-complex", + "num-integer", + "num-traits", + "rand 0.8.5", + "thiserror 1.0.69", +] + [[package]] name = "arraystring" version = "0.3.0" @@ -253,7 +290,7 @@ version = "0.2.4" dependencies = [ "itertools 0.14.0", "partial-min-max", - "rand", + "rand 0.9.1", "rand_distr", "rand_pcg", "strum", @@ -371,7 +408,7 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -930,7 +967,7 @@ dependencies = [ "displaydoc", "getrandom 0.3.2", "icu_benchmark_macros", - "rand", + "rand 0.9.1", "rand_distr", "rand_pcg", "ryu", @@ -1194,6 +1231,7 @@ dependencies = [ name = "icu_calendar" version = "2.0.4" dependencies = [ + "argmin", "calendrical_calculations", "criterion", "databake", @@ -1387,7 +1425,7 @@ dependencies = [ "icu_locale_core", "icu_provider", "icu_provider_adapters", - "rand", + "rand 0.9.1", "rand_distr", "rand_pcg", "serde", @@ -1840,6 +1878,15 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -1981,7 +2028,7 @@ dependencies = [ "icu_benchmark_macros", "icu_locale_core", "postcard", - "rand", + "rand 0.9.1", "rkyv", "serde_core", "serde_json", @@ -2450,14 +2497,35 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -2467,7 +2535,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -2486,7 +2563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand", + "rand 0.9.1", ] [[package]] @@ -2495,7 +2572,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" dependencies = [ - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", ] [[package]] @@ -3042,13 +3128,33 @@ dependencies = [ "libc", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -3110,7 +3216,7 @@ dependencies = [ "databake", "displaydoc", "postcard", - "rand", + "rand 0.9.1", "serde_core", "serde_json", "zerovec", @@ -3829,7 +3935,7 @@ dependencies = [ "criterion", "either", "icu_benchmark_macros", - "rand", + "rand 0.9.1", ] [[package]] @@ -3922,7 +4028,7 @@ dependencies = [ "icu_locale_core", "litemap", "postcard", - "rand", + "rand 0.9.1", "rand_pcg", "rmp-serde", "serde", @@ -3946,7 +4052,7 @@ dependencies = [ "icu_benchmark_macros", "postcard", "potential_utf", - "rand", + "rand 0.9.1", "rand_distr", "rand_pcg", "rmp-serde", diff --git a/Cargo.toml b/Cargo.toml index f516f9bad27..6cd98d5c911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -273,6 +273,8 @@ serde-aux = { version = "4.0.0", default-features = false } toml = { version = "0.8.0", default-features = false, features = ["parse"] } ## External Deps Group 3: Dev and Datagen deps. Include default features. +argmin = "0.10" +argmin-math = "0.4" arraystring = "0.3.0" askama = "0.14" atoi = "2.0.0" diff --git a/components/calendar/Cargo.toml b/components/calendar/Cargo.toml index c25018a3adf..a21efe200ed 100644 --- a/components/calendar/Cargo.toml +++ b/components/calendar/Cargo.toml @@ -35,6 +35,7 @@ icu_calendar_data = { workspace = true, optional = true } icu_locale = { workspace = true, optional = true } [dev-dependencies] +argmin = { workspace = true } icu_provider = { path = "../../provider/core", features = ["logging"] } icu = { path = "../../components/icu", default-features = false } itertools = { workspace = true } diff --git a/components/calendar/src/cal/chinese/simple.rs b/components/calendar/src/cal/chinese/simple.rs index d500daa935d..4d120dbc612 100644 --- a/components/calendar/src/cal/chinese/simple.rs +++ b/components/calendar/src/cal/chinese/simple.rs @@ -34,6 +34,9 @@ const MEAN_GREGORIAN_SOLAR_TERM_LENGTH: Milliseconds = /// [Astronomical Almanac (1992)]: https://archive.org/details/131123ExplanatorySupplementAstronomicalAlmanac/page/n302/mode/1up const MEAN_SYNODIC_MONTH_LENGTH: Milliseconds = day_fraction_to_ms!(295305888531 / 10000000000i64); +#[cfg(test)] +const MEAN_SYNODIC_MONTH_LENGTH_F64: f64 = 29.5305888531; + /// Number of milliseconds in a day. const MILLISECONDS_IN_EPHEMERIS_DAY: i64 = 24 * 60 * 60 * 1000; @@ -160,3 +163,60 @@ impl super::LunarChineseYearData { LunarChineseYearData::new(related_iso, start_day, month_lengths, leap_month) } } + +#[cfg(test)] +mod test { + use argmin::core::{CostFunction, Error, Executor}; + use argmin::solver::brent::BrentOpt; + use calendrical_calculations::astronomy::Astronomical; + use calendrical_calculations::rata_die::Moment; + use calendrical_calculations::gregorian::fixed_from_gregorian; + use super::*; + + struct NewMoons { + new_moon_moments: Vec<(i32, Moment)>, + mean_synodic_length: f64, + center_moment: Moment, + new_moon_number: i32, + } + + impl CostFunction for NewMoons { + type Param = f64; + type Output = f64; + fn cost(&self, param: &f64) -> Result { + let candidate = self.center_moment + *param; + Ok(self.new_moon_moments.iter().map(|(i, x)| (candidate - *x) - self.mean_synodic_length * (self.new_moon_number - i) as f64).map(|v| v * v).sum::()) + } + } + + #[test] + fn calculate_new_moon_and_mean_synodic_month() { + let jan1900 = fixed_from_gregorian(1900, 1, 1); + let jan2000 = fixed_from_gregorian(2000, 1, 1); + let jan2100 = fixed_from_gregorian(2100, 1, 1); + let new_moon_0 = Astronomical::num_of_new_moon_at_or_after(Moment::try_from_rata_die(jan1900).unwrap()); + let new_moon_mid = Astronomical::num_of_new_moon_at_or_after(Moment::try_from_rata_die(jan2000).unwrap()); + let new_moon_n = Astronomical::num_of_new_moon_at_or_after(Moment::try_from_rata_die(jan2100).unwrap()); + let new_moon_moments: Vec<(i32, Moment)> = (new_moon_0..=new_moon_n).map(|i| (i, Astronomical::nth_new_moon(i))).collect(); + + let cost_fn = NewMoons { + new_moon_moments, + mean_synodic_length: MEAN_SYNODIC_MONTH_LENGTH_F64, + center_moment: Moment::try_from_rata_die(fixed_from_gregorian(2000, 1, 6)).unwrap(), + new_moon_number: new_moon_mid, + }; + + let solver = BrentOpt::new(-5.0, 5.0); + + let res = Executor::new(cost_fn, solver) + .configure(|state| state.max_iters(10)) + .run() + .unwrap(); + + println!("{res}"); + panic!(); + + // let mean_synodic_length = (new_moon_moments.last().unwrap().1 - new_moon_moments.first().unwrap().1) / (new_moon_moments.len() as f64); + // assert_eq!(0.0, mean_synodic_length); + } +} diff --git a/utils/calendrical_calculations/src/astronomy.rs b/utils/calendrical_calculations/src/astronomy.rs index ad07dfc4afb..536c9bbaa16 100644 --- a/utils/calendrical_calculations/src/astronomy.rs +++ b/utils/calendrical_calculations/src/astronomy.rs @@ -80,7 +80,7 @@ impl Location { /// Create a location; latitude is from -90 to 90, and longitude is from -180 to 180; /// attempting to create a location outside of these bounds will result in a LocationOutOfBoundsError. #[allow(dead_code)] // TODO: Remove dead_code tag after use - pub(crate) fn try_new( + pub fn try_new( latitude: f64, longitude: f64, elevation: f64, @@ -109,25 +109,25 @@ impl Location { /// Get the longitude of a Location #[allow(dead_code)] - pub(crate) fn longitude(&self) -> f64 { + pub fn longitude(&self) -> f64 { self.longitude } /// Get the latitude of a Location #[allow(dead_code)] - pub(crate) fn latitude(&self) -> f64 { + pub fn latitude(&self) -> f64 { self.latitude } /// Get the elevation of a Location #[allow(dead_code)] - pub(crate) fn elevation(&self) -> f64 { + pub fn elevation(&self) -> f64 { self.elevation } /// Get the utc-offset of a Location #[allow(dead_code)] - pub(crate) fn zone(&self) -> f64 { + pub fn zone(&self) -> f64 { self.utc_offset } @@ -135,7 +135,7 @@ impl Location { /// this yields the difference in Moment given a longitude /// e.g. a longitude of 90 degrees is 0.25 (90 / 360) days ahead /// of a location with a longitude of 0 degrees. - pub(crate) fn zone_from_longitude(longitude: f64) -> f64 { + pub fn zone_from_longitude(longitude: f64) -> f64 { longitude / (360.0) } @@ -144,7 +144,7 @@ impl Location { /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz. /// Reference lisp code: #[allow(dead_code)] - pub(crate) fn standard_from_local(standard_time: Moment, location: Location) -> Moment { + pub fn standard_from_local(standard_time: Moment, location: Location) -> Moment { Self::standard_from_universal( Self::universal_from_local(standard_time, location), location, @@ -155,7 +155,7 @@ impl Location { /// /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz. /// Reference lisp code: - pub(crate) fn universal_from_local(local_time: Moment, location: Location) -> Moment { + pub fn universal_from_local(local_time: Moment, location: Location) -> Moment { local_time - Self::zone_from_longitude(location.longitude) } @@ -164,7 +164,7 @@ impl Location { /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz. /// Reference lisp code: #[allow(dead_code)] // TODO: Remove dead_code tag after use - pub(crate) fn local_from_universal(universal_time: Moment, location: Location) -> Moment { + pub fn local_from_universal(universal_time: Moment, location: Location) -> Moment { universal_time + Self::zone_from_longitude(location.longitude) } @@ -175,7 +175,7 @@ impl Location { /// /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz. /// Reference lisp code: - pub(crate) fn universal_from_standard(standard_moment: Moment, location: Location) -> Moment { + pub fn universal_from_standard(standard_moment: Moment, location: Location) -> Moment { debug_assert!(location.utc_offset > MIN_UTC_OFFSET && location.utc_offset < MAX_UTC_OFFSET, "UTC offset {0} was not within the possible range of offsets (see astronomy::MIN_UTC_OFFSET and astronomy::MAX_UTC_OFFSET)", location.utc_offset); standard_moment - location.utc_offset } @@ -187,7 +187,7 @@ impl Location { /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz. /// Reference lisp code: #[allow(dead_code)] - pub(crate) fn standard_from_universal(standard_time: Moment, location: Location) -> Moment { + pub fn standard_from_universal(standard_time: Moment, location: Location) -> Moment { debug_assert!(location.utc_offset > MIN_UTC_OFFSET && location.utc_offset < MAX_UTC_OFFSET, "UTC offset {0} was not within the possible range of offsets (see astronomy::MIN_UTC_OFFSET and astronomy::MAX_UTC_OFFSET)", location.utc_offset); standard_time + location.utc_offset } diff --git a/utils/calendrical_calculations/src/lib.rs b/utils/calendrical_calculations/src/lib.rs index f1d2f9adb37..77690fcae75 100644 --- a/utils/calendrical_calculations/src/lib.rs +++ b/utils/calendrical_calculations/src/lib.rs @@ -35,7 +35,8 @@ )] #![warn(missing_docs)] -mod astronomy; +/// Lower-level astronomical simulation functions +pub mod astronomy; /// Chinese-like lunar calendars (Chinese, Dangi) pub mod chinese_based; /// The Coptic calendar diff --git a/utils/calendrical_calculations/src/rata_die.rs b/utils/calendrical_calculations/src/rata_die.rs index a02cebd5970..00746562281 100644 --- a/utils/calendrical_calculations/src/rata_die.rs +++ b/utils/calendrical_calculations/src/rata_die.rs @@ -160,7 +160,7 @@ impl Sub for RataDie { /// NOTE: This should not cause overflow errors for most cases, but consider /// alternative implementations if necessary. #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] -pub(crate) struct Moment(f64); +pub struct Moment(f64); /// Add a number of days to a Moment impl Add for Moment { @@ -209,7 +209,19 @@ impl Moment { self.0 } - /// Get the RataDie of a Moment + /// Get a Moment for a RataDie. + /// Returns None if the RataDie is out of range of Moment. + pub fn try_from_rata_die(rata_die: RataDie) -> Option { + // Why is there no impl TryFrom for f64 ??? + let value = rata_die.to_i64_date() as f64; + if value as i64 != rata_die.to_i64_date() { + return None; + } + Some(Self(value)) + } + + /// Get the RataDie of a Moment, + /// truncating to midnight pub fn as_rata_die(self) -> RataDie { RataDie::new(self.0.floor() as i64) }