1#![allow(clippy::needless_range_loop)]
2use std::collections::HashMap;
12
13#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct SegyTextHeader {
19 pub bytes: Vec<u8>,
21}
22
23impl SegyTextHeader {
24 pub fn new() -> Self {
26 Self {
27 bytes: vec![0x40u8; 3200],
28 }
29 }
30
31 pub fn set_line(&mut self, line_idx: usize, text: &str) {
35 if line_idx >= 40 {
36 return;
37 }
38 let start = line_idx * 80;
39 let ascii_bytes = text.as_bytes();
40 for i in 0..80 {
41 let ch = if i < ascii_bytes.len() {
42 ascii_bytes[i]
43 } else {
44 b' '
45 };
46 self.bytes[start + i] = ascii_to_ebcdic(ch);
47 }
48 }
49
50 pub fn get_line(&self, line_idx: usize) -> String {
52 if line_idx >= 40 {
53 return String::new();
54 }
55 let start = line_idx * 80;
56 let chars: Vec<u8> = self.bytes[start..start + 80]
57 .iter()
58 .map(|&b| ebcdic_to_ascii(b))
59 .collect();
60 String::from_utf8_lossy(&chars).trim_end().to_string()
61 }
62}
63
64impl Default for SegyTextHeader {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70#[allow(dead_code)]
72fn ascii_to_ebcdic(ch: u8) -> u8 {
73 match ch {
74 b' ' => 0x40,
75 b'0'..=b'9' => ch - b'0' + 0xF0,
76 b'A'..=b'I' => ch - b'A' + 0xC1,
77 b'J'..=b'R' => ch - b'J' + 0xD1,
78 b'S'..=b'Z' => ch - b'S' + 0xE2,
79 b'a'..=b'i' => ch - b'a' + 0x81,
80 b'j'..=b'r' => ch - b'j' + 0x91,
81 b's'..=b'z' => ch - b's' + 0xA2,
82 b'.' => 0x4B,
83 b',' => 0x6B,
84 b':' => 0x7A,
85 _ => 0x40,
86 }
87}
88
89#[allow(dead_code)]
91fn ebcdic_to_ascii(b: u8) -> u8 {
92 match b {
93 0x40 => b' ',
94 0xF0..=0xF9 => b - 0xF0 + b'0',
95 0xC1..=0xC9 => b - 0xC1 + b'A',
96 0xD1..=0xD9 => b - 0xD1 + b'J',
97 0xE2..=0xE9 => b - 0xE2 + b'S',
98 0x81..=0x89 => b - 0x81 + b'a',
99 0x91..=0x99 => b - 0x91 + b'j',
100 0xA2..=0xA9 => b - 0xA2 + b's',
101 0x4B => b'.',
102 0x6B => b',',
103 0x7A => b':',
104 _ => b'?',
105 }
106}
107
108#[allow(dead_code)]
113#[derive(Debug, Clone)]
114pub struct SegyBinaryHeader {
115 pub job_id: i32,
117 pub line_number: i32,
119 pub reel_number: i32,
121 pub traces_per_record: i16,
123 pub aux_traces_per_record: i16,
125 pub sample_interval_us: i16,
127 pub samples_per_trace: i16,
129 pub data_format_code: i16,
131 pub segy_revision: i16,
133 pub fixed_length_flag: i16,
135}
136
137impl Default for SegyBinaryHeader {
138 fn default() -> Self {
139 Self {
140 job_id: 1,
141 line_number: 1,
142 reel_number: 1,
143 traces_per_record: 1,
144 aux_traces_per_record: 0,
145 sample_interval_us: 2000, samples_per_trace: 1000,
147 data_format_code: 5, segy_revision: 256, fixed_length_flag: 1,
150 }
151 }
152}
153
154#[allow(dead_code)]
158#[derive(Debug, Clone)]
159pub struct SegyTraceHeader {
160 pub trace_seq_file: i32,
162 pub field_record_number: i32,
164 pub trace_number_in_field: i32,
166 pub source_x: i32,
168 pub source_y: i32,
170 pub group_x: i32,
172 pub group_y: i32,
174 pub coordinate_scalar: i16,
176 pub delay_recording_time_ms: i16,
178 pub num_samples: i16,
180 pub sample_interval_us: i16,
182 pub year: i16,
184 pub day_of_year: i16,
186 pub hour: i16,
188 pub minute: i16,
190 pub second: i16,
192}
193
194impl Default for SegyTraceHeader {
195 fn default() -> Self {
196 Self {
197 trace_seq_file: 1,
198 field_record_number: 1,
199 trace_number_in_field: 1,
200 source_x: 0,
201 source_y: 0,
202 group_x: 0,
203 group_y: 0,
204 coordinate_scalar: -100,
205 delay_recording_time_ms: 0,
206 num_samples: 1000,
207 sample_interval_us: 2000,
208 year: 2026,
209 day_of_year: 1,
210 hour: 0,
211 minute: 0,
212 second: 0,
213 }
214 }
215}
216
217#[allow(dead_code)]
219#[derive(Debug, Clone)]
220pub struct SegyTrace {
221 pub header: SegyTraceHeader,
223 pub data: Vec<f32>,
225}
226
227impl SegyTrace {
228 pub fn new(data: Vec<f32>) -> Self {
230 let num_samples = data.len() as i16;
231 let header = SegyTraceHeader {
232 num_samples,
233 ..Default::default()
234 };
235 Self { header, data }
236 }
237
238 pub fn num_samples(&self) -> usize {
240 self.data.len()
241 }
242
243 pub fn sample_interval_s(&self) -> f64 {
245 self.header.sample_interval_us as f64 * 1e-6
246 }
247}
248
249#[allow(dead_code)]
251#[derive(Debug, Clone)]
252pub struct SegyFile {
253 pub text_header: SegyTextHeader,
255 pub binary_header: SegyBinaryHeader,
257 pub traces: Vec<SegyTrace>,
259}
260
261impl SegyFile {
262 pub fn new() -> Self {
264 Self {
265 text_header: SegyTextHeader::new(),
266 binary_header: SegyBinaryHeader::default(),
267 traces: Vec::new(),
268 }
269 }
270
271 pub fn add_trace(&mut self, mut trace: SegyTrace) {
273 let idx = self.traces.len() as i32 + 1;
274 trace.header.trace_seq_file = idx;
275 self.traces.push(trace);
276 }
277
278 pub fn num_traces(&self) -> usize {
280 self.traces.len()
281 }
282
283 pub fn serialize_binary_header(&self) -> Vec<u8> {
285 let h = &self.binary_header;
286 let mut buf = vec![0u8; 400];
287 write_i32_be(&mut buf, 0, h.job_id);
288 write_i32_be(&mut buf, 4, h.line_number);
289 write_i32_be(&mut buf, 8, h.reel_number);
290 write_i16_be(&mut buf, 12, h.traces_per_record);
291 write_i16_be(&mut buf, 14, h.aux_traces_per_record);
292 write_i16_be(&mut buf, 16, h.sample_interval_us);
293 write_i16_be(&mut buf, 20, h.samples_per_trace);
294 write_i16_be(&mut buf, 24, h.data_format_code);
295 write_i16_be(&mut buf, 300, h.segy_revision);
296 write_i16_be(&mut buf, 302, h.fixed_length_flag);
297 buf
298 }
299
300 pub fn deserialize_binary_header(buf: &[u8]) -> SegyBinaryHeader {
302 SegyBinaryHeader {
303 job_id: read_i32_be(buf, 0),
304 line_number: read_i32_be(buf, 4),
305 reel_number: read_i32_be(buf, 8),
306 traces_per_record: read_i16_be(buf, 12),
307 aux_traces_per_record: read_i16_be(buf, 14),
308 sample_interval_us: read_i16_be(buf, 16),
309 samples_per_trace: read_i16_be(buf, 20),
310 data_format_code: read_i16_be(buf, 24),
311 segy_revision: read_i16_be(buf, 300),
312 fixed_length_flag: read_i16_be(buf, 302),
313 }
314 }
315}
316
317impl Default for SegyFile {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323#[allow(dead_code)]
330#[derive(Debug, Clone)]
331pub struct SacHeader {
332 pub delta: f32,
334 pub depmin: f32,
336 pub depmax: f32,
338 pub b: f32,
340 pub e: f32,
342 pub o: f32,
344 pub a: f32,
346 pub dist: f32,
348 pub az: f32,
350 pub baz: f32,
352 pub cmpaz: f32,
354 pub cmpinc: f32,
356 pub stla: f32,
358 pub stlo: f32,
360 pub stel: f32,
362 pub evla: f32,
364 pub evlo: f32,
366 pub evdp: f32,
368 pub mag: f32,
370 pub npts: i32,
372 pub nzyear: i32,
374 pub nzjday: i32,
376 pub nzhour: i32,
378 pub nzmin: i32,
380 pub nzsec: i32,
382 pub nzmsec: i32,
384 pub nvhdr: i32,
386 pub kstnm: String,
388 pub kcmpnm: String,
390 pub knetwk: String,
392}
393
394impl Default for SacHeader {
395 fn default() -> Self {
396 Self {
397 delta: 0.01,
398 depmin: -1e20,
399 depmax: 1e20,
400 b: 0.0,
401 e: 0.0,
402 o: -12345.0,
403 a: -12345.0,
404 dist: -12345.0,
405 az: -12345.0,
406 baz: -12345.0,
407 cmpaz: 0.0,
408 cmpinc: 0.0,
409 stla: -12345.0,
410 stlo: -12345.0,
411 stel: -12345.0,
412 evla: -12345.0,
413 evlo: -12345.0,
414 evdp: -12345.0,
415 mag: -12345.0,
416 npts: 0,
417 nzyear: 2026,
418 nzjday: 1,
419 nzhour: 0,
420 nzmin: 0,
421 nzsec: 0,
422 nzmsec: 0,
423 nvhdr: 6,
424 kstnm: String::from("-12345 "),
425 kcmpnm: String::from("-12345 "),
426 knetwk: String::from("-12345 "),
427 }
428 }
429}
430
431#[allow(dead_code)]
433#[derive(Debug, Clone)]
434pub struct SacFile {
435 pub header: SacHeader,
437 pub data: Vec<f32>,
439}
440
441impl SacFile {
442 pub fn from_data(data: Vec<f32>, delta: f32) -> Self {
444 let npts = data.len() as i32;
445 let e = if npts > 0 {
446 (npts - 1) as f32 * delta
447 } else {
448 0.0
449 };
450 let depmin = data.iter().cloned().fold(f32::MAX, f32::min);
451 let depmax = data.iter().cloned().fold(f32::MIN, f32::max);
452 let header = SacHeader {
453 delta,
454 npts,
455 b: 0.0,
456 e,
457 depmin,
458 depmax,
459 ..Default::default()
460 };
461 Self { header, data }
462 }
463
464 pub fn end_time(&self) -> f64 {
466 self.header.b as f64 + (self.header.npts - 1) as f64 * self.header.delta as f64
467 }
468
469 pub fn serialize_header(&self) -> Vec<u8> {
471 let mut buf = vec![0u8; 632];
472 write_f32_le(&mut buf, 0, self.header.delta);
473 write_f32_le(&mut buf, 4, self.header.depmin);
474 write_f32_le(&mut buf, 8, self.header.depmax);
475 write_f32_le(&mut buf, 20, self.header.b);
476 write_f32_le(&mut buf, 24, self.header.e);
477 write_i32_le(&mut buf, 316, self.header.npts);
478 write_i32_le(&mut buf, 284, self.header.nvhdr);
479 buf
480 }
481
482 pub fn npts_from_header(buf: &[u8]) -> i32 {
484 read_i32_le(buf, 316)
485 }
486
487 pub fn num_samples(&self) -> usize {
489 self.data.len()
490 }
491}
492
493#[allow(dead_code)]
499#[derive(Debug, Clone)]
500pub struct MiniSeedHeader {
501 pub sequence_number: u32,
503 pub quality_indicator: u8,
505 pub station: String,
507 pub location: String,
509 pub channel: String,
511 pub network: String,
513 pub start_year: u16,
515 pub start_day: u16,
517 pub start_hour: u8,
519 pub start_min: u8,
521 pub start_sec: u8,
523 pub start_frac: u16,
525 pub num_samples: u16,
527 pub sample_rate_factor: i16,
529 pub sample_rate_multiplier: i16,
531 pub activity_flags: u8,
533 pub io_flags: u8,
535 pub data_quality_flags: u8,
537 pub num_blockettes: u8,
539 pub time_correction: i32,
541 pub data_offset: u16,
543 pub blockette_offset: u16,
545}
546
547impl Default for MiniSeedHeader {
548 fn default() -> Self {
549 Self {
550 sequence_number: 1,
551 quality_indicator: b'D',
552 station: String::from("XXXXX"),
553 location: String::from(" "),
554 channel: String::from("BHZ"),
555 network: String::from("XX"),
556 start_year: 2026,
557 start_day: 1,
558 start_hour: 0,
559 start_min: 0,
560 start_sec: 0,
561 start_frac: 0,
562 num_samples: 0,
563 sample_rate_factor: 100,
564 sample_rate_multiplier: 1,
565 activity_flags: 0,
566 io_flags: 0,
567 data_quality_flags: 0,
568 num_blockettes: 0,
569 time_correction: 0,
570 data_offset: 64,
571 blockette_offset: 0,
572 }
573 }
574}
575
576impl MiniSeedHeader {
577 pub fn sample_rate(&self) -> f64 {
579 let factor = self.sample_rate_factor as f64;
580 let mult = self.sample_rate_multiplier as f64;
581 if factor > 0.0 && mult > 0.0 {
582 factor * mult
583 } else if factor > 0.0 && mult < 0.0 {
584 -factor / mult
585 } else if factor < 0.0 && mult > 0.0 {
586 -mult / factor
587 } else {
588 1.0 / (factor * mult)
589 }
590 }
591}
592
593#[allow(dead_code)]
597#[derive(Debug, Clone)]
598pub struct Steim1Frame {
599 pub words: [u32; 16],
602}
603
604impl Steim1Frame {
605 pub fn encode_diffs(diffs: &[i32]) -> Vec<Self> {
613 let mut frames = Vec::new();
614 let mut pos = 0;
615 while pos < diffs.len() {
616 let mut frame = Steim1Frame { words: [0u32; 16] };
617 let mut word_idx = 1usize;
618 while word_idx < 16 && pos < diffs.len() {
619 let d = diffs[pos];
620 if (-128..=127).contains(&d) {
621 frame.words[0] |= 3 << (30 - 2 * word_idx);
623 frame.words[word_idx] = (d as i8 as u8) as u32;
624 } else if (-32768..=32767).contains(&d) {
625 frame.words[0] |= 2 << (30 - 2 * word_idx);
627 frame.words[word_idx] = (d as i16 as u16) as u32;
628 } else {
629 frame.words[0] |= 1 << (30 - 2 * word_idx);
631 frame.words[word_idx] = d as u32;
632 }
633 word_idx += 1;
634 pos += 1;
635 }
636 frames.push(frame);
637 }
638 frames
639 }
640}
641
642#[allow(dead_code)]
644#[derive(Debug, Clone)]
645pub struct MiniSeedRecord {
646 pub header: MiniSeedHeader,
648 pub samples: Vec<i32>,
650 pub steim1_frames: Vec<Steim1Frame>,
652}
653
654impl MiniSeedRecord {
655 pub fn from_samples(samples: Vec<i32>, station: &str, channel: &str, network: &str) -> Self {
657 let header = MiniSeedHeader {
658 station: station.to_string(),
659 channel: channel.to_string(),
660 network: network.to_string(),
661 num_samples: samples.len() as u16,
662 ..Default::default()
663 };
664 let mut diffs = vec![samples.first().copied().unwrap_or(0)];
666 for i in 1..samples.len() {
667 diffs.push(samples[i] - samples[i - 1]);
668 }
669 let steim1_frames = Steim1Frame::encode_diffs(&diffs);
670 Self {
671 header,
672 samples,
673 steim1_frames,
674 }
675 }
676
677 pub fn next_sequence(&mut self) {
679 self.header.sequence_number = (self.header.sequence_number % 999999) + 1;
680 }
681}
682
683#[allow(dead_code)]
687#[derive(Debug, Clone)]
688pub struct SeismicTrace {
689 pub network: String,
691 pub station: String,
693 pub location: String,
695 pub channel: String,
697 pub sampling_rate: f64,
699 pub start_time: f64,
701 pub data: Vec<f64>,
703}
704
705impl SeismicTrace {
706 pub fn new(
708 network: &str,
709 station: &str,
710 channel: &str,
711 sampling_rate: f64,
712 start_time: f64,
713 data: Vec<f64>,
714 ) -> Self {
715 Self {
716 network: network.to_string(),
717 station: station.to_string(),
718 location: String::from(" "),
719 channel: channel.to_string(),
720 sampling_rate,
721 start_time,
722 data,
723 }
724 }
725
726 pub fn end_time(&self) -> f64 {
728 if self.data.is_empty() {
729 self.start_time
730 } else {
731 self.start_time + (self.data.len() - 1) as f64 / self.sampling_rate
732 }
733 }
734
735 pub fn duration(&self) -> f64 {
737 self.end_time() - self.start_time
738 }
739
740 pub fn seed_id(&self) -> String {
742 format!(
743 "{}.{}.{}.{}",
744 self.network, self.station, self.location, self.channel
745 )
746 }
747
748 pub fn rms(&self) -> f64 {
750 if self.data.is_empty() {
751 return 0.0;
752 }
753 let sum2: f64 = self.data.iter().map(|&x| x * x).sum();
754 (sum2 / self.data.len() as f64).sqrt()
755 }
756}
757
758#[allow(dead_code)]
762#[derive(Debug, Clone)]
763pub struct SeismicStation {
764 pub network: String,
766 pub station: String,
768 pub latitude: f64,
770 pub longitude: f64,
772 pub elevation: f64,
774 pub channels: Vec<String>,
776}
777
778impl SeismicStation {
779 pub fn new(network: &str, station: &str, lat: f64, lon: f64, elevation: f64) -> Self {
781 Self {
782 network: network.to_string(),
783 station: station.to_string(),
784 latitude: lat,
785 longitude: lon,
786 elevation,
787 channels: Vec::new(),
788 }
789 }
790
791 pub fn distance_km(&self, other: &SeismicStation) -> f64 {
795 haversine_km(
796 self.latitude,
797 self.longitude,
798 other.latitude,
799 other.longitude,
800 )
801 }
802
803 pub fn epicentral_distance_km(&self, event_lat: f64, event_lon: f64) -> f64 {
805 haversine_km(self.latitude, self.longitude, event_lat, event_lon)
806 }
807}
808
809#[allow(dead_code)]
813pub fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
814 const R_KM: f64 = 6371.0;
815 let d_lat = (lat2 - lat1).to_radians();
816 let d_lon = (lon2 - lon1).to_radians();
817 let a = (d_lat / 2.0).sin().powi(2)
818 + lat1.to_radians().cos() * lat2.to_radians().cos() * (d_lon / 2.0).sin().powi(2);
819 let c = 2.0 * a.sqrt().asin();
820 R_KM * c
821}
822
823#[allow(dead_code)]
831#[derive(Debug, Clone)]
832pub struct ArrivalPicker {
833 pub sta_samples: usize,
835 pub lta_samples: usize,
837 pub trigger_threshold: f64,
839 pub detrigger_threshold: f64,
841}
842
843impl ArrivalPicker {
844 pub fn new(sta_samples: usize, lta_samples: usize, trigger: f64, detrigger: f64) -> Self {
846 Self {
847 sta_samples,
848 lta_samples,
849 trigger_threshold: trigger,
850 detrigger_threshold: detrigger,
851 }
852 }
853
854 pub fn characteristic_function(&self, data: &[f64]) -> Vec<f64> {
859 let n = data.len();
860 let mut cf = vec![0.0f64; n];
861 let sq: Vec<f64> = data.iter().map(|&x| x * x).collect();
862
863 let mut cum = vec![0.0f64; n + 1];
865 for i in 0..n {
866 cum[i + 1] = cum[i] + sq[i];
867 }
868
869 for i in self.lta_samples..n {
870 let lta_start = i.saturating_sub(self.lta_samples);
871 let sta_start = i.saturating_sub(self.sta_samples);
872 let lta = (cum[i] - cum[lta_start]) / self.lta_samples as f64;
873 let sta = (cum[i] - cum[sta_start]) / self.sta_samples as f64;
874 if lta > 1e-30 {
875 cf[i] = sta / lta;
876 }
877 }
878 cf
879 }
880
881 pub fn pick_arrivals(&self, data: &[f64]) -> Vec<usize> {
885 let cf = self.characteristic_function(data);
886 let mut picks = Vec::new();
887 let mut triggered = false;
888 for (i, &c) in cf.iter().enumerate() {
889 if !triggered && c >= self.trigger_threshold {
890 picks.push(i);
891 triggered = true;
892 } else if triggered && c < self.detrigger_threshold {
893 triggered = false;
894 }
895 }
896 picks
897 }
898}
899
900#[allow(dead_code)]
907#[derive(Debug, Clone)]
908pub struct FrequencyFilter {
909 pub freqmin: f64,
911 pub freqmax: f64,
913 pub order: usize,
915 pub sampling_rate: f64,
917}
918
919impl FrequencyFilter {
920 pub fn bandpass(freqmin: f64, freqmax: f64, order: usize, sampling_rate: f64) -> Self {
922 Self {
923 freqmin,
924 freqmax,
925 order,
926 sampling_rate,
927 }
928 }
929
930 pub fn in_passband(&self, freq_hz: f64) -> bool {
932 freq_hz >= self.freqmin && freq_hz <= self.freqmax
933 }
934
935 pub fn ideal_gain(&self, freq_hz: f64) -> f64 {
940 if self.in_passband(freq_hz) { 1.0 } else { 0.0 }
941 }
942
943 pub fn apply(&self, data: &[f64]) -> Vec<f64> {
948 let n = data.len();
952 if n == 0 {
953 return Vec::new();
954 }
955 let lp_samples = (self.sampling_rate / (2.0 * self.freqmax)).round().max(1.0) as usize;
957 let hp_samples = (self.sampling_rate / (2.0 * self.freqmin)).round().max(1.0) as usize;
959
960 let lp = moving_average(data, lp_samples);
961 let hp_lp = moving_average(data, hp_samples);
962 let mut out = vec![0.0f64; n];
964 for i in 0..n {
965 out[i] = lp[i] - hp_lp[i];
966 }
967 out
968 }
969}
970
971#[allow(dead_code)]
973fn moving_average(data: &[f64], window: usize) -> Vec<f64> {
974 let n = data.len();
975 let w = window.max(1);
976 let mut out = vec![0.0f64; n];
977 let mut sum = 0.0f64;
978 let mut count = 0usize;
979 for i in 0..n {
980 sum += data[i];
981 count += 1;
982 if i >= w {
983 sum -= data[i - w];
984 count -= 1;
985 }
986 out[i] = sum / count as f64;
987 }
988 out
989}
990
991#[allow(dead_code)]
995#[derive(Debug, Clone)]
996pub struct SeismicEvent {
997 pub event_id: String,
999 pub origin_time: f64,
1001 pub latitude: f64,
1003 pub longitude: f64,
1005 pub depth_km: f64,
1007 pub magnitude: f64,
1009 pub magnitude_type: String,
1011 pub num_phases: u32,
1013 pub rms_residual: f64,
1015}
1016
1017impl SeismicEvent {
1018 #[allow(clippy::too_many_arguments)]
1020 pub fn new(
1021 event_id: &str,
1022 origin_time: f64,
1023 lat: f64,
1024 lon: f64,
1025 depth_km: f64,
1026 magnitude: f64,
1027 magnitude_type: &str,
1028 ) -> Self {
1029 Self {
1030 event_id: event_id.to_string(),
1031 origin_time,
1032 latitude: lat,
1033 longitude: lon,
1034 depth_km,
1035 magnitude,
1036 magnitude_type: magnitude_type.to_string(),
1037 num_phases: 0,
1038 rms_residual: 0.0,
1039 }
1040 }
1041
1042 pub fn epicentral_distance_km(&self, station: &SeismicStation) -> f64 {
1044 haversine_km(
1045 self.latitude,
1046 self.longitude,
1047 station.latitude,
1048 station.longitude,
1049 )
1050 }
1051
1052 pub fn estimate_p_travel_time(&self, station: &SeismicStation) -> f64 {
1056 let dist = self.epicentral_distance_km(station);
1057 let hypo_dist = (dist * dist + self.depth_km * self.depth_km).sqrt();
1058 hypo_dist / 8.0 }
1060
1061 pub fn estimate_s_travel_time(&self, station: &SeismicStation) -> f64 {
1063 let dist = self.epicentral_distance_km(station);
1064 let hypo_dist = (dist * dist + self.depth_km * self.depth_km).sqrt();
1065 hypo_dist / 4.6 }
1067}
1068
1069#[allow(dead_code)]
1071#[derive(Debug, Clone)]
1072pub struct SeismicCatalog {
1073 pub events: Vec<SeismicEvent>,
1075 pub name: String,
1077}
1078
1079impl SeismicCatalog {
1080 pub fn new(name: &str) -> Self {
1082 Self {
1083 events: Vec::new(),
1084 name: name.to_string(),
1085 }
1086 }
1087
1088 pub fn add_event(&mut self, event: SeismicEvent) {
1090 self.events.push(event);
1091 }
1092
1093 pub fn len(&self) -> usize {
1095 self.events.len()
1096 }
1097
1098 pub fn is_empty(&self) -> bool {
1100 self.events.is_empty()
1101 }
1102
1103 pub fn filter_by_magnitude(&self, mag_min: f64, mag_max: f64) -> Vec<&SeismicEvent> {
1105 self.events
1106 .iter()
1107 .filter(|e| e.magnitude >= mag_min && e.magnitude <= mag_max)
1108 .collect()
1109 }
1110
1111 pub fn sort_by_time(&mut self) {
1113 self.events.sort_by(|a, b| {
1114 a.origin_time
1115 .partial_cmp(&b.origin_time)
1116 .unwrap_or(std::cmp::Ordering::Equal)
1117 });
1118 }
1119
1120 pub fn max_magnitude_event(&self) -> Option<&SeismicEvent> {
1122 self.events.iter().max_by(|a, b| {
1123 a.magnitude
1124 .partial_cmp(&b.magnitude)
1125 .unwrap_or(std::cmp::Ordering::Equal)
1126 })
1127 }
1128
1129 pub fn build_id_index(&self) -> HashMap<String, usize> {
1131 self.events
1132 .iter()
1133 .enumerate()
1134 .map(|(i, e)| (e.event_id.clone(), i))
1135 .collect()
1136 }
1137
1138 pub fn events_in_window(&self, t_start: f64, t_end: f64) -> Vec<&SeismicEvent> {
1140 self.events
1141 .iter()
1142 .filter(|e| e.origin_time >= t_start && e.origin_time <= t_end)
1143 .collect()
1144 }
1145}
1146
1147#[allow(dead_code)]
1151#[derive(Debug, Clone)]
1152pub struct PhaseArrival {
1153 pub station_id: String,
1155 pub phase: String,
1157 pub arrival_time: f64,
1159 pub residual: f64,
1161}
1162
1163impl PhaseArrival {
1164 pub fn new(station_id: &str, phase: &str, arrival_time: f64, residual: f64) -> Self {
1166 Self {
1167 station_id: station_id.to_string(),
1168 phase: phase.to_string(),
1169 arrival_time,
1170 residual,
1171 }
1172 }
1173}
1174
1175#[allow(dead_code)]
1179fn write_i32_be(buf: &mut [u8], offset: usize, val: i32) {
1180 let bytes = val.to_be_bytes();
1181 buf[offset..offset + 4].copy_from_slice(&bytes);
1182}
1183
1184#[allow(dead_code)]
1186fn write_i16_be(buf: &mut [u8], offset: usize, val: i16) {
1187 let bytes = val.to_be_bytes();
1188 buf[offset..offset + 2].copy_from_slice(&bytes);
1189}
1190
1191#[allow(dead_code)]
1193fn read_i32_be(buf: &[u8], offset: usize) -> i32 {
1194 let bytes: [u8; 4] = buf[offset..offset + 4].try_into().unwrap_or([0; 4]);
1195 i32::from_be_bytes(bytes)
1196}
1197
1198#[allow(dead_code)]
1200fn read_i16_be(buf: &[u8], offset: usize) -> i16 {
1201 let bytes: [u8; 2] = buf[offset..offset + 2].try_into().unwrap_or([0; 2]);
1202 i16::from_be_bytes(bytes)
1203}
1204
1205#[allow(dead_code)]
1207fn write_f32_le(buf: &mut [u8], offset: usize, val: f32) {
1208 let bytes = val.to_le_bytes();
1209 buf[offset..offset + 4].copy_from_slice(&bytes);
1210}
1211
1212#[allow(dead_code)]
1214fn write_i32_le(buf: &mut [u8], offset: usize, val: i32) {
1215 let bytes = val.to_le_bytes();
1216 buf[offset..offset + 4].copy_from_slice(&bytes);
1217}
1218
1219#[allow(dead_code)]
1221fn read_i32_le(buf: &[u8], offset: usize) -> i32 {
1222 let bytes: [u8; 4] = buf[offset..offset + 4].try_into().unwrap_or([0; 4]);
1223 i32::from_le_bytes(bytes)
1224}
1225
1226#[cfg(test)]
1229mod tests {
1230 use super::*;
1231
1232 #[test]
1235 fn test_segy_text_header_roundtrip() {
1236 let mut hdr = SegyTextHeader::new();
1237 hdr.set_line(0, "CREATED BY OXIPHYSICS");
1238 let line = hdr.get_line(0);
1239 assert!(line.contains("CREATED"), "line={line}");
1240 }
1241
1242 #[test]
1243 fn test_segy_text_header_empty_line() {
1244 let hdr = SegyTextHeader::new();
1245 let line = hdr.get_line(0);
1246 assert!(line.is_empty(), "line={line:?}");
1248 }
1249
1250 #[test]
1251 fn test_segy_text_header_out_of_range() {
1252 let mut hdr = SegyTextHeader::new();
1253 hdr.set_line(40, "SHOULD NOT WRITE");
1254 assert_eq!(hdr.get_line(40), "");
1256 }
1257
1258 #[test]
1259 fn test_segy_binary_header_roundtrip() {
1260 let segy = SegyFile::new();
1261 let buf = segy.serialize_binary_header();
1262 assert_eq!(buf.len(), 400);
1263 let h2 = SegyFile::deserialize_binary_header(&buf);
1264 assert_eq!(h2.job_id, segy.binary_header.job_id);
1265 assert_eq!(h2.sample_interval_us, segy.binary_header.sample_interval_us);
1266 assert_eq!(h2.samples_per_trace, segy.binary_header.samples_per_trace);
1267 assert_eq!(h2.data_format_code, segy.binary_header.data_format_code);
1268 }
1269
1270 #[test]
1271 fn test_segy_binary_header_segy_revision() {
1272 let segy = SegyFile::new();
1273 let buf = segy.serialize_binary_header();
1274 let h2 = SegyFile::deserialize_binary_header(&buf);
1275 assert_eq!(h2.segy_revision, 256, "Expected rev 1 (256)");
1276 }
1277
1278 #[test]
1279 fn test_segy_add_trace_sequence_number() {
1280 let mut segy = SegyFile::new();
1281 let t1 = SegyTrace::new(vec![1.0, 2.0, 3.0]);
1282 let t2 = SegyTrace::new(vec![4.0, 5.0, 6.0]);
1283 segy.add_trace(t1);
1284 segy.add_trace(t2);
1285 assert_eq!(segy.traces[0].header.trace_seq_file, 1);
1286 assert_eq!(segy.traces[1].header.trace_seq_file, 2);
1287 }
1288
1289 #[test]
1290 fn test_segy_trace_num_samples() {
1291 let data: Vec<f32> = (0..500).map(|x| x as f32 * 0.001).collect();
1292 let trace = SegyTrace::new(data.clone());
1293 assert_eq!(trace.num_samples(), 500);
1294 assert_eq!(trace.header.num_samples, 500);
1295 }
1296
1297 #[test]
1298 fn test_segy_trace_sample_interval() {
1299 let mut trace = SegyTrace::new(vec![0.0f32; 10]);
1300 trace.header.sample_interval_us = 4000; let dt = trace.sample_interval_s();
1302 assert!((dt - 0.004).abs() < 1e-9, "dt={dt:.6}");
1303 }
1304
1305 #[test]
1306 fn test_segy_num_traces() {
1307 let mut segy = SegyFile::new();
1308 assert_eq!(segy.num_traces(), 0);
1309 for i in 0..10 {
1310 segy.add_trace(SegyTrace::new(vec![i as f32; 100]));
1311 }
1312 assert_eq!(segy.num_traces(), 10);
1313 }
1314
1315 #[test]
1318 fn test_sac_npts_matches_data_length() {
1319 let data: Vec<f32> = (0..1024).map(|x| (x as f32).sin()).collect();
1320 let sac = SacFile::from_data(data.clone(), 0.01);
1321 assert_eq!(sac.header.npts as usize, data.len());
1322 assert_eq!(sac.num_samples(), data.len());
1323 }
1324
1325 #[test]
1326 fn test_sac_end_time_correct() {
1327 let data = vec![0.0f32; 1001];
1328 let sac = SacFile::from_data(data, 0.01);
1329 assert!((sac.header.e - 10.0).abs() < 1e-5, "e={:.6}", sac.header.e);
1331 }
1332
1333 #[test]
1334 fn test_sac_depmin_depmax() {
1335 let data = vec![-3.0f32, 0.0, 5.0, 2.0];
1336 let sac = SacFile::from_data(data, 0.01);
1337 assert!((sac.header.depmin - (-3.0)).abs() < 1e-5);
1338 assert!((sac.header.depmax - 5.0).abs() < 1e-5);
1339 }
1340
1341 #[test]
1342 fn test_sac_header_serialize_npts() {
1343 let data = vec![1.0f32; 256];
1344 let sac = SacFile::from_data(data, 0.01);
1345 let buf = sac.serialize_header();
1346 assert_eq!(buf.len(), 632);
1347 let npts = SacFile::npts_from_header(&buf);
1348 assert_eq!(npts, 256);
1349 }
1350
1351 #[test]
1352 fn test_sac_delta_roundtrip() {
1353 let data = vec![0.0f32; 100];
1354 let sac = SacFile::from_data(data, 0.025);
1355 let buf = sac.serialize_header();
1356 let delta_bytes: [u8; 4] = buf[0..4].try_into().unwrap();
1358 let delta = f32::from_le_bytes(delta_bytes);
1359 assert!((delta - 0.025).abs() < 1e-6, "delta={delta:.6}");
1360 }
1361
1362 #[test]
1365 fn test_miniseed_sequence_number_increment() {
1366 let mut rec = MiniSeedRecord::from_samples(vec![0; 10], "ANMO", "BHZ", "IU");
1367 assert_eq!(rec.header.sequence_number, 1);
1368 rec.next_sequence();
1369 assert_eq!(rec.header.sequence_number, 2);
1370 rec.next_sequence();
1371 assert_eq!(rec.header.sequence_number, 3);
1372 }
1373
1374 #[test]
1375 fn test_miniseed_sequence_number_wraps() {
1376 let mut rec = MiniSeedRecord::from_samples(vec![0; 10], "ANMO", "BHZ", "IU");
1377 rec.header.sequence_number = 999999;
1378 rec.next_sequence();
1379 assert_eq!(rec.header.sequence_number, 1);
1380 }
1381
1382 #[test]
1383 fn test_miniseed_num_samples() {
1384 let samples = vec![1, 2, 3, 4, 5];
1385 let rec = MiniSeedRecord::from_samples(samples.clone(), "ANMO", "BHZ", "IU");
1386 assert_eq!(rec.header.num_samples as usize, samples.len());
1387 }
1388
1389 #[test]
1390 fn test_miniseed_channel_assignment() {
1391 let rec = MiniSeedRecord::from_samples(vec![0; 5], "COLA", "HHN", "AK");
1392 assert_eq!(rec.header.channel, "HHN");
1393 assert_eq!(rec.header.network, "AK");
1394 assert_eq!(rec.header.station, "COLA");
1395 }
1396
1397 #[test]
1398 fn test_miniseed_sample_rate_positive() {
1399 let hdr = MiniSeedHeader::default();
1400 let sr = hdr.sample_rate();
1402 assert!((sr - 100.0).abs() < 1e-9, "sr={sr:.6}");
1403 }
1404
1405 #[test]
1406 fn test_miniseed_sample_rate_negative_factor() {
1407 let hdr = MiniSeedHeader {
1408 sample_rate_factor: -10, sample_rate_multiplier: 1,
1410 ..Default::default()
1411 };
1412 let sr = hdr.sample_rate();
1414 assert!((sr - 0.1).abs() < 1e-9, "sr={sr:.6}");
1415 }
1416
1417 #[test]
1418 fn test_steim1_encode_small_diffs() {
1419 let diffs = vec![0i32, 1, -1, 127, -128];
1420 let frames = Steim1Frame::encode_diffs(&diffs);
1421 assert!(!frames.is_empty());
1422 }
1424
1425 #[test]
1426 fn test_steim1_encode_large_diffs() {
1427 let diffs = vec![1_000_000i32, -1_000_000];
1428 let frames = Steim1Frame::encode_diffs(&diffs);
1429 assert!(!frames.is_empty());
1430 }
1431
1432 #[test]
1435 fn test_seismic_trace_end_time() {
1436 let data = vec![0.0; 101];
1437 let trace = SeismicTrace::new("IU", "ANMO", "BHZ", 100.0, 0.0, data);
1438 assert!(
1440 (trace.end_time() - 1.0).abs() < 1e-9,
1441 "end={:.6}",
1442 trace.end_time()
1443 );
1444 }
1445
1446 #[test]
1447 fn test_seismic_trace_seed_id() {
1448 let trace = SeismicTrace::new("IU", "ANMO", "BHZ", 100.0, 0.0, vec![]);
1449 assert_eq!(trace.seed_id(), "IU.ANMO. .BHZ");
1450 }
1451
1452 #[test]
1453 fn test_seismic_trace_rms() {
1454 let data = vec![1.0, -1.0, 1.0, -1.0];
1455 let trace = SeismicTrace::new("IU", "ANMO", "BHZ", 1.0, 0.0, data);
1456 assert!((trace.rms() - 1.0).abs() < 1e-9, "rms={:.6}", trace.rms());
1457 }
1458
1459 #[test]
1460 fn test_seismic_trace_duration() {
1461 let data = vec![0.0; 1001];
1462 let trace = SeismicTrace::new("IU", "ANMO", "BHZ", 100.0, 10.0, data);
1463 assert!((trace.duration() - 10.0).abs() < 1e-9);
1465 }
1466
1467 #[test]
1470 fn test_station_distance_same_location() {
1471 let sta1 = SeismicStation::new("IU", "ANMO", 34.9459, -106.4572, 1820.0);
1472 let sta2 = SeismicStation::new("IU", "ANMO", 34.9459, -106.4572, 1820.0);
1473 let d = sta1.distance_km(&sta2);
1474 assert!(d < 0.001, "d={d:.6}");
1475 }
1476
1477 #[test]
1478 fn test_haversine_known_distance() {
1479 let d = haversine_km(51.5074, -0.1278, 48.8566, 2.3522);
1481 assert!((d - 341.0).abs() < 5.0, "d={d:.1} km");
1482 }
1483
1484 #[test]
1485 fn test_haversine_equator() {
1486 let d = haversine_km(0.0, 0.0, 0.0, 1.0);
1488 assert!((d - 111.32).abs() < 0.5, "d={d:.3} km");
1489 }
1490
1491 #[test]
1492 fn test_station_epicentral_distance() {
1493 let sta = SeismicStation::new("IU", "ANMO", 34.9459, -106.4572, 1820.0);
1494 let d = sta.epicentral_distance_km(34.9459, -106.4572);
1496 assert!(d < 0.001, "d={d:.6}");
1497 }
1498
1499 #[test]
1502 fn test_stalta_characteristic_function_size() {
1503 let data: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.1).sin()).collect();
1504 let picker = ArrivalPicker::new(10, 100, 3.0, 1.5);
1505 let cf = picker.characteristic_function(&data);
1506 assert_eq!(cf.len(), data.len());
1507 }
1508
1509 #[test]
1510 fn test_stalta_picks_before_direct_wave() {
1511 let mut data = vec![0.01f64; 1000];
1513 for i in 500..510 {
1514 data[i] = 100.0; }
1516 for (i, x) in data.iter_mut().enumerate() {
1518 if i < 500 {
1519 *x += 0.001 * (i as f64).sin();
1520 }
1521 }
1522 let picker = ArrivalPicker::new(10, 100, 3.0, 1.5);
1523 let picks = picker.pick_arrivals(&data);
1524 assert!(!picks.is_empty(), "Expected at least one pick");
1526 let first_pick = picks[0];
1527 assert!(first_pick >= 100, "Pick too early: {first_pick}");
1528 assert!(first_pick <= 520, "Pick too late: {first_pick}");
1529 }
1530
1531 #[test]
1532 fn test_stalta_no_picks_quiet_signal() {
1533 let data = vec![0.001f64; 1000];
1534 let picker = ArrivalPicker::new(10, 100, 10.0, 5.0);
1535 let picks = picker.pick_arrivals(&data);
1536 assert!(picks.is_empty(), "Expected no picks for quiet signal");
1537 }
1538
1539 #[test]
1542 fn test_bandpass_in_passband() {
1543 let filt = FrequencyFilter::bandpass(1.0, 10.0, 4, 100.0);
1544 assert!(filt.in_passband(5.0));
1545 assert!(!filt.in_passband(0.5));
1546 assert!(!filt.in_passband(15.0));
1547 }
1548
1549 #[test]
1550 fn test_bandpass_ideal_gain_passband() {
1551 let filt = FrequencyFilter::bandpass(1.0, 10.0, 4, 100.0);
1552 assert!((filt.ideal_gain(5.0) - 1.0).abs() < 1e-10);
1553 assert!((filt.ideal_gain(0.1)).abs() < 1e-10);
1554 }
1555
1556 #[test]
1557 fn test_bandpass_apply_preserves_length() {
1558 let data: Vec<f64> = (0..256).map(|i| (i as f64 * 0.1).sin()).collect();
1559 let filt = FrequencyFilter::bandpass(1.0, 10.0, 4, 100.0);
1560 let out = filt.apply(&data);
1561 assert_eq!(out.len(), data.len());
1562 }
1563
1564 #[test]
1565 fn test_bandpass_apply_empty() {
1566 let filt = FrequencyFilter::bandpass(1.0, 10.0, 4, 100.0);
1567 let out = filt.apply(&[]);
1568 assert!(out.is_empty());
1569 }
1570
1571 #[test]
1574 fn test_catalog_add_and_len() {
1575 let mut cat = SeismicCatalog::new("Test");
1576 assert_eq!(cat.len(), 0);
1577 cat.add_event(SeismicEvent::new(
1578 "ev001", 1000.0, 35.0, -120.0, 10.0, 5.5, "Mw",
1579 ));
1580 cat.add_event(SeismicEvent::new(
1581 "ev002", 2000.0, 36.0, -121.0, 15.0, 4.2, "Mb",
1582 ));
1583 assert_eq!(cat.len(), 2);
1584 }
1585
1586 #[test]
1587 fn test_catalog_sort_by_time() {
1588 let mut cat = SeismicCatalog::new("Test");
1589 cat.add_event(SeismicEvent::new(
1590 "ev002", 2000.0, 35.0, -120.0, 10.0, 5.5, "Mw",
1591 ));
1592 cat.add_event(SeismicEvent::new(
1593 "ev001", 1000.0, 35.0, -120.0, 10.0, 4.0, "Mb",
1594 ));
1595 cat.sort_by_time();
1596 assert!(cat.events[0].origin_time < cat.events[1].origin_time);
1597 }
1598
1599 #[test]
1600 fn test_catalog_filter_by_magnitude() {
1601 let mut cat = SeismicCatalog::new("Test");
1602 cat.add_event(SeismicEvent::new(
1603 "ev001", 1000.0, 35.0, -120.0, 10.0, 5.5, "Mw",
1604 ));
1605 cat.add_event(SeismicEvent::new(
1606 "ev002", 2000.0, 36.0, -121.0, 15.0, 3.0, "Mb",
1607 ));
1608 cat.add_event(SeismicEvent::new(
1609 "ev003", 3000.0, 37.0, -122.0, 20.0, 7.0, "Mw",
1610 ));
1611 let big = cat.filter_by_magnitude(5.0, 8.0);
1612 assert_eq!(big.len(), 2);
1613 }
1614
1615 #[test]
1616 fn test_catalog_max_magnitude() {
1617 let mut cat = SeismicCatalog::new("Test");
1618 cat.add_event(SeismicEvent::new(
1619 "ev001", 1000.0, 35.0, -120.0, 10.0, 5.5, "Mw",
1620 ));
1621 cat.add_event(SeismicEvent::new(
1622 "ev002", 2000.0, 36.0, -121.0, 15.0, 7.1, "Mw",
1623 ));
1624 let max_ev = cat.max_magnitude_event().unwrap();
1625 assert!((max_ev.magnitude - 7.1).abs() < 1e-9);
1626 }
1627
1628 #[test]
1629 fn test_catalog_events_in_window() {
1630 let mut cat = SeismicCatalog::new("Test");
1631 cat.add_event(SeismicEvent::new(
1632 "ev001", 100.0, 35.0, -120.0, 10.0, 5.5, "Mw",
1633 ));
1634 cat.add_event(SeismicEvent::new(
1635 "ev002", 500.0, 36.0, -121.0, 15.0, 3.0, "Mb",
1636 ));
1637 cat.add_event(SeismicEvent::new(
1638 "ev003", 900.0, 37.0, -122.0, 20.0, 7.0, "Mw",
1639 ));
1640 let evts = cat.events_in_window(200.0, 800.0);
1641 assert_eq!(evts.len(), 1);
1642 assert_eq!(evts[0].event_id, "ev002");
1643 }
1644
1645 #[test]
1646 fn test_event_p_travel_time_increases_with_distance() {
1647 let event = SeismicEvent::new("ev001", 0.0, 0.0, 0.0, 10.0, 6.0, "Mw");
1648 let sta_near = SeismicStation::new("XX", "NEAR", 0.1, 0.0, 0.0);
1649 let sta_far = SeismicStation::new("XX", "FAR", 5.0, 0.0, 0.0);
1650 let t_near = event.estimate_p_travel_time(&sta_near);
1651 let t_far = event.estimate_p_travel_time(&sta_far);
1652 assert!(
1653 t_far > t_near,
1654 "t_far={t_far:.3} should be > t_near={t_near:.3}"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_event_s_travel_time_greater_than_p() {
1660 let event = SeismicEvent::new("ev001", 0.0, 35.0, -120.0, 10.0, 6.0, "Mw");
1661 let sta = SeismicStation::new("IU", "ANMO", 34.9459, -106.4572, 1820.0);
1662 let t_p = event.estimate_p_travel_time(&sta);
1663 let t_s = event.estimate_s_travel_time(&sta);
1664 assert!(t_s > t_p, "S ({t_s:.3}s) should arrive after P ({t_p:.3}s)");
1665 }
1666
1667 #[test]
1668 fn test_phase_arrival_creation() {
1669 let arr = PhaseArrival::new("IU.ANMO. .BHZ", "P", 1000.5, 0.03);
1670 assert_eq!(arr.phase, "P");
1671 assert!((arr.residual - 0.03).abs() < 1e-9);
1672 }
1673}