1use serde::{Deserialize, Serialize};
9
10use crate::CalcError;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct HzToPpmResult {
15 pub variation_hz: f64,
17 pub ppm: f64,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct PpmToHzResult {
24 pub variation_hz: f64,
26 pub max_hz: f64,
28 pub min_hz: f64,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct XtalLoadResult {
35 pub c_load_calc_f: f64,
37 pub c_load_rule_of_thumb_f: f64,
39}
40
41pub fn hz_to_ppm(center_hz: f64, max_hz: f64) -> Result<HzToPpmResult, CalcError> {
50 if center_hz <= 0.0 {
51 return Err(CalcError::OutOfRange {
52 name: "center_hz",
53 value: center_hz,
54 expected: "> 0",
55 });
56 }
57 if max_hz <= center_hz {
58 return Err(CalcError::OutOfRange {
59 name: "max_hz",
60 value: max_hz,
61 expected: "> center_hz",
62 });
63 }
64
65 let variation_hz = max_hz - center_hz;
66 let ppm = (variation_hz / center_hz) * 1_000_000.0;
67
68 Ok(HzToPpmResult { variation_hz, ppm })
69}
70
71pub fn ppm_to_hz(center_hz: f64, ppm: f64) -> Result<PpmToHzResult, CalcError> {
80 if center_hz <= 0.0 {
81 return Err(CalcError::OutOfRange {
82 name: "center_hz",
83 value: center_hz,
84 expected: "> 0",
85 });
86 }
87 if ppm <= 0.0 {
88 return Err(CalcError::OutOfRange {
89 name: "ppm",
90 value: ppm,
91 expected: "> 0",
92 });
93 }
94
95 let variation_hz = center_hz * ppm / 1_000_000.0;
96
97 Ok(PpmToHzResult {
98 variation_hz,
99 max_hz: center_hz + variation_hz,
100 min_hz: center_hz - variation_hz,
101 })
102}
103
104pub fn xtal_load(
115 c_stray_f: f64,
116 c1_f: f64,
117 c2_f: f64,
118) -> Result<XtalLoadResult, CalcError> {
119 if c_stray_f < 0.0 {
120 return Err(CalcError::OutOfRange {
121 name: "c_stray_f",
122 value: c_stray_f,
123 expected: ">= 0",
124 });
125 }
126 if c1_f <= 0.0 {
127 return Err(CalcError::OutOfRange {
128 name: "c1_f",
129 value: c1_f,
130 expected: "> 0",
131 });
132 }
133 if c2_f <= 0.0 {
134 return Err(CalcError::OutOfRange {
135 name: "c2_f",
136 value: c2_f,
137 expected: "> 0",
138 });
139 }
140
141 let c_series = (c1_f * c2_f) / (c1_f + c2_f);
142 let c_load_calc_f = c_series + c_stray_f;
143 let c_load_rule_of_thumb_f = (c1_f + c2_f) / 2.0;
144
145 Ok(XtalLoadResult {
146 c_load_calc_f,
147 c_load_rule_of_thumb_f,
148 })
149}
150
151#[cfg(test)]
152mod tests {
153 use approx::assert_relative_eq;
154
155 use super::*;
156
157 #[test]
160 fn saturn_hz_to_ppm() {
161 let result = hz_to_ppm(32000.0, 32001.0).unwrap();
162 assert_relative_eq!(result.variation_hz, 1.0, epsilon = 1e-10);
163 assert_relative_eq!(result.ppm, 31.25, epsilon = 1e-6);
164 }
165
166 #[test]
169 fn saturn_ppm_to_hz() {
170 let result = ppm_to_hz(50e6, 25.0).unwrap();
171 assert_relative_eq!(result.variation_hz, 1250.0, epsilon = 1e-6);
172 assert_relative_eq!(result.max_hz, 50_001_250.0, epsilon = 1e-4);
173 assert_relative_eq!(result.min_hz, 49_998_750.0, epsilon = 1e-4);
174 }
175
176 #[test]
179 fn saturn_xtal_load() {
180 let result = xtal_load(3e-12, 14e-12, 14e-12).unwrap();
181 assert_relative_eq!(result.c_load_calc_f, 10e-12, epsilon = 1e-14);
182 assert_relative_eq!(result.c_load_rule_of_thumb_f, 14e-12, epsilon = 1e-14);
183 }
184
185 #[test]
186 fn error_on_zero_center_freq() {
187 assert!(hz_to_ppm(0.0, 100.0).is_err());
188 assert!(ppm_to_hz(0.0, 10.0).is_err());
189 }
190
191 #[test]
192 fn error_on_max_not_greater_than_center() {
193 assert!(hz_to_ppm(1000.0, 999.0).is_err());
194 assert!(hz_to_ppm(1000.0, 1000.0).is_err());
195 }
196}