1#![warn(missing_docs)]
7mod hit;
20pub mod ordering;
21mod packet;
22pub mod section;
23
24pub use hit::{calculate_tof, correct_timestamp_rollover};
25pub use packet::Tpx3Packet;
26
27use serde::{Deserialize, Serialize};
28use std::fs::File;
29use std::io::BufReader;
30use std::path::Path;
31
32#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct ChipTransform {
39 pub a: i32,
41 pub b: i32,
43 pub c: i32,
45 pub d: i32,
47 pub tx: i32,
49 pub ty: i32,
51}
52
53impl ChipTransform {
54 #[must_use]
56 pub fn identity() -> Self {
57 Self {
58 a: 1,
59 b: 0,
60 c: 0,
61 d: 1,
62 tx: 0,
63 ty: 0,
64 }
65 }
66
67 #[inline]
73 #[must_use]
74 pub fn apply(&self, x: u16, y: u16) -> (u16, u16) {
75 let x = i32::from(x);
76 let y = i32::from(y);
77
78 let gx = self.a * x + self.b * y + self.tx;
79 let gy = self.c * x + self.d * y + self.ty;
80
81 debug_assert!(
82 u16::try_from(gx).is_ok(),
83 "ChipTransform: X out of bounds: {gx}"
84 );
85 debug_assert!(
86 u16::try_from(gy).is_ok(),
87 "ChipTransform: Y out of bounds: {gy}"
88 );
89
90 (
92 u16::try_from(gx).unwrap_or(u16::MAX),
93 u16::try_from(gy).unwrap_or(u16::MAX),
94 )
95 }
96
97 pub fn validate_bounds(&self, chip_size_x: u16, chip_size_y: u16) -> Result<(), String> {
105 let max_x = i32::from(chip_size_x.saturating_sub(1));
106 let max_y = i32::from(chip_size_y.saturating_sub(1));
107
108 let corners = [(0, 0), (max_x, 0), (0, max_y), (max_x, max_y)];
110
111 for (x, y) in corners {
112 let gx = self.a * x + self.b * y + self.tx;
113 let gy = self.c * x + self.d * y + self.ty;
114
115 if gx < 0 || gx > i32::from(u16::MAX) {
116 return Err(format!(
117 "Transform produces out-of-bounds x={gx} for input ({x}, {y}). \
118 Valid range is [0, 65535].",
119 ));
120 }
121 if gy < 0 || gy > i32::from(u16::MAX) {
122 return Err(format!(
123 "Transform produces out-of-bounds y={gy} for input ({x}, {y}). \
124 Valid range is [0, 65535].",
125 ));
126 }
127 }
128
129 Ok(())
130 }
131}
132
133#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct DetectorConfig {
136 pub tdc_frequency_hz: f64,
138 pub enable_missing_tdc_correction: bool,
140 pub chip_size_x: u16,
142 pub chip_size_y: u16,
144 pub chip_transforms: Vec<ChipTransform>,
146}
147
148impl Default for DetectorConfig {
149 fn default() -> Self {
150 Self::venus_defaults()
151 }
152}
153
154#[derive(Deserialize, Serialize)]
156struct JsonConfig {
157 detector: JsonDetector,
158}
159
160#[derive(Deserialize, Serialize, Default)]
161#[serde(default)]
162struct JsonDetector {
163 timing: JsonTiming,
164 chip_layout: JsonChipLayout,
165 chip_transformations: Option<Vec<JsonChipTransform>>,
166}
167
168#[derive(Deserialize, Serialize)]
169#[serde(default)]
170struct JsonTiming {
171 tdc_frequency_hz: f64,
172 enable_missing_tdc_correction: bool,
173}
174
175impl Default for JsonTiming {
176 fn default() -> Self {
177 Self {
178 tdc_frequency_hz: 60.0,
179 enable_missing_tdc_correction: true,
180 }
181 }
182}
183
184#[derive(Deserialize, Serialize)]
185#[serde(default)]
186struct JsonChipLayout {
187 chip_size_x: u16,
188 chip_size_y: u16,
189}
190
191impl Default for JsonChipLayout {
192 fn default() -> Self {
193 Self {
194 chip_size_x: 256,
195 chip_size_y: 256,
196 }
197 }
198}
199
200#[derive(Deserialize, Serialize)]
201struct JsonChipTransform {
202 chip_id: u8,
203 matrix: [[i32; 3]; 2],
204}
205
206impl DetectorConfig {
207 #[must_use]
215 pub fn venus_defaults() -> Self {
216 let transforms = vec![
217 ChipTransform {
219 a: 1,
220 b: 0,
221 c: 0,
222 d: 1,
223 tx: 258,
224 ty: 0,
225 },
226 ChipTransform {
228 a: -1,
229 b: 0,
230 c: 0,
231 d: -1,
232 tx: 513,
233 ty: 513,
234 },
235 ChipTransform {
237 a: -1,
238 b: 0,
239 c: 0,
240 d: -1,
241 tx: 255,
242 ty: 513,
243 },
244 ChipTransform {
246 a: 1,
247 b: 0,
248 c: 0,
249 d: 1,
250 tx: 0,
251 ty: 0,
252 },
253 ];
254
255 Self {
256 tdc_frequency_hz: 60.0,
257 enable_missing_tdc_correction: true,
258 chip_size_x: 256,
259 chip_size_y: 256,
260 chip_transforms: transforms,
261 }
262 }
263
264 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
271 let file = File::open(path)?;
272 let reader = BufReader::new(file);
273 let json_config: JsonConfig = serde_json::from_reader(reader)?;
274 Self::from_json_config(json_config)
275 }
276
277 pub fn from_json(json: &str) -> Result<Self, Box<dyn std::error::Error>> {
284 let json_config: JsonConfig = serde_json::from_str(json)?;
285 Self::from_json_config(json_config)
286 }
287
288 pub fn to_json_string(&self) -> Result<String, Box<dyn std::error::Error>> {
293 let transforms = {
294 let transforms = self
295 .chip_transforms
296 .iter()
297 .enumerate()
298 .map(|(chip_id, transform)| {
299 let chip_id = u8::try_from(chip_id).map_err(|_| {
300 std::io::Error::new(
301 std::io::ErrorKind::InvalidInput,
302 format!("chip_id {chip_id} exceeds u8"),
303 )
304 })?;
305 Ok(JsonChipTransform {
306 chip_id,
307 matrix: [
308 [transform.a, transform.b, transform.tx],
309 [transform.c, transform.d, transform.ty],
310 ],
311 })
312 })
313 .collect::<Result<Vec<_>, std::io::Error>>()?;
314 Some(transforms)
315 };
316
317 let json_config = JsonConfig {
318 detector: JsonDetector {
319 timing: JsonTiming {
320 tdc_frequency_hz: self.tdc_frequency_hz,
321 enable_missing_tdc_correction: self.enable_missing_tdc_correction,
322 },
323 chip_layout: JsonChipLayout {
324 chip_size_x: self.chip_size_x,
325 chip_size_y: self.chip_size_y,
326 },
327 chip_transformations: transforms,
328 },
329 };
330
331 Ok(serde_json::to_string_pretty(&json_config)?)
332 }
333
334 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
339 let json = self.to_json_string()?;
340 std::fs::write(path, json)?;
341 Ok(())
342 }
343
344 fn from_json_config(config: JsonConfig) -> Result<Self, Box<dyn std::error::Error>> {
345 let detector = config.detector;
346
347 let chip_size_x = detector.chip_layout.chip_size_x;
348 let chip_size_y = detector.chip_layout.chip_size_y;
349
350 let transforms = match detector.chip_transformations {
353 Some(transforms) => {
354 if transforms.is_empty() {
355 Vec::new()
356 } else {
357 let max_chip_id = transforms.iter().map(|t| t.chip_id).max().unwrap_or(0);
359
360 let mut t_vec = vec![ChipTransform::identity(); (max_chip_id + 1) as usize];
361
362 for t in transforms {
363 let matrix = t.matrix;
364 t_vec[t.chip_id as usize] = ChipTransform {
366 a: matrix[0][0],
367 b: matrix[0][1],
368 tx: matrix[0][2],
369 c: matrix[1][0],
370 d: matrix[1][1],
371 ty: matrix[1][2],
372 };
373 }
374 t_vec
375 }
376 }
377 None => {
378 Self::venus_defaults().chip_transforms
380 }
381 };
382
383 let config = Self {
384 tdc_frequency_hz: detector.timing.tdc_frequency_hz,
385 enable_missing_tdc_correction: detector.timing.enable_missing_tdc_correction,
386 chip_size_x,
387 chip_size_y,
388 chip_transforms: transforms,
389 };
390
391 config.validate_transforms()?;
393
394 Ok(config)
395 }
396
397 pub fn validate_transforms(&self) -> Result<(), Box<dyn std::error::Error>> {
405 for (i, transform) in self.chip_transforms.iter().enumerate() {
406 transform
407 .validate_bounds(self.chip_size_x, self.chip_size_y)
408 .map_err(|e| format!("Chip {i} transform invalid: {e}"))?;
409 }
410 Ok(())
411 }
412
413 #[must_use]
415 pub fn tdc_period_seconds(&self) -> f64 {
416 1.0 / self.tdc_frequency_hz
417 }
418
419 #[must_use]
421 pub fn tdc_correction_25ns(&self) -> u32 {
422 let correction = (self.tdc_period_seconds() / 25e-9).round();
423 if correction <= 0.0 {
424 return 0;
425 }
426 if correction >= f64::from(u32::MAX) {
427 return u32::MAX;
428 }
429 format!("{correction:.0}")
430 .parse::<u32>()
431 .unwrap_or(u32::MAX)
432 }
433
434 #[must_use]
439 pub fn map_chip_to_global(&self, chip_id: u8, x: u16, y: u16) -> (u16, u16) {
440 if let Some(transform) = self.chip_transforms.get(chip_id as usize) {
441 transform.apply(x, y)
442 } else {
443 (x, y)
444 }
445 }
446
447 #[must_use]
452 pub fn detector_dimensions(&self) -> (usize, usize) {
453 if self.chip_transforms.is_empty() {
454 return (usize::from(self.chip_size_x), usize::from(self.chip_size_y));
455 }
456
457 let max_x = i32::from(self.chip_size_x.saturating_sub(1));
458 let max_y = i32::from(self.chip_size_y.saturating_sub(1));
459 let corners = [(0, 0), (max_x, 0), (0, max_y), (max_x, max_y)];
460
461 let mut max_global_x = 0i32;
462 let mut max_global_y = 0i32;
463
464 for transform in &self.chip_transforms {
465 for (x, y) in corners {
466 let gx = transform.a * x + transform.b * y + transform.tx;
467 let gy = transform.c * x + transform.d * y + transform.ty;
468 if gx > max_global_x {
469 max_global_x = gx;
470 }
471 if gy > max_global_y {
472 max_global_y = gy;
473 }
474 }
475 }
476
477 let max_x = usize::try_from(max_global_x.max(0)).unwrap_or(0);
478 let max_y = usize::try_from(max_global_y.max(0)).unwrap_or(0);
479 (max_x.saturating_add(1), max_y.saturating_add(1))
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use serde_json::Value;
487
488 fn assert_f64_eq(actual: f64, expected: f64) {
489 assert!(
490 (actual - expected).abs() <= f64::EPSILON,
491 "expected {expected}, got {actual}"
492 );
493 }
494
495 #[test]
496 fn test_venus_defaults() {
497 let config = DetectorConfig::venus_defaults();
498 assert_f64_eq(config.tdc_frequency_hz, 60.0);
499 assert!(config.enable_missing_tdc_correction);
500 assert_eq!(config.chip_transforms.len(), 4);
501 }
502
503 #[test]
504 fn test_venus_detector_dimensions() {
505 let config = DetectorConfig::venus_defaults();
506 let (width, height) = config.detector_dimensions();
507 assert_eq!((width, height), (514, 514));
508 }
509
510 #[test]
511 fn test_tdc_correction() {
512 let config = DetectorConfig::venus_defaults();
513 let correction = config.tdc_correction_25ns();
515 assert!(correction > 600_000 && correction < 700_000);
516 }
517
518 #[test]
519 fn test_venus_chip_mappings() {
520 let config = DetectorConfig::venus_defaults();
521
522 let (gx, gy) = config.map_chip_to_global(0, 100, 100);
526 assert_eq!((gx, gy), (358, 100));
527
528 let (gx, gy) = config.map_chip_to_global(1, 100, 100);
532 assert_eq!((gx, gy), (413, 413));
533
534 let (gx, gy) = config.map_chip_to_global(2, 100, 100);
538 assert_eq!((gx, gy), (155, 413));
539
540 let (gx, gy) = config.map_chip_to_global(3, 100, 100);
544 assert_eq!((gx, gy), (100, 100));
545 }
546 #[test]
547 fn test_json_loading() {
548 let json = r#"{
549 "detector": {
550 "timing": {
551 "tdc_frequency_hz": 14.0,
552 "enable_missing_tdc_correction": false
553 },
554 "chip_layout": {
555 "chip_size_x": 256,
556 "chip_size_y": 256
557 },
558 "chip_transformations": [
559 {
560 "chip_id": 0,
561 "matrix": [[1, 0, 100], [0, 1, 200]]
562 },
563 {
564 "chip_id": 1,
565 "matrix": [[-1, 0, 300], [0, -1, 400]]
566 }
567 ]
568 }
569 }"#;
570
571 let config = DetectorConfig::from_json(json).expect("Failed to parse JSON");
572
573 assert_f64_eq(config.tdc_frequency_hz, 14.0);
574 assert!(!config.enable_missing_tdc_correction);
575 assert_eq!(config.chip_size_x, 256);
576 assert_eq!(config.chip_size_y, 256);
577 assert_eq!(config.chip_transforms.len(), 2);
578
579 let (gx, gy) = config.map_chip_to_global(0, 10, 20);
581 assert_eq!((gx, gy), (110, 220));
584
585 let (gx, gy) = config.map_chip_to_global(1, 10, 20);
587 assert_eq!((gx, gy), (290, 380));
590 }
591 #[test]
592 fn test_json_partial_config_frequency_only() {
593 let json = r#"{
595 "detector": {
596 "timing": {
597 "tdc_frequency_hz": 14.0
598 }
599 }
600 }"#;
601
602 let config = DetectorConfig::from_json(json).expect("Should parse partial config");
603
604 assert_f64_eq(config.tdc_frequency_hz, 14.0); assert!(config.enable_missing_tdc_correction); assert_eq!(config.chip_size_x, 256); assert_eq!(config.chip_size_y, 256); assert_eq!(config.chip_transforms.len(), 4); }
610
611 #[test]
612 fn test_json_empty_detector() {
613 let json = r#"{ "detector": {} }"#;
615
616 let config = DetectorConfig::from_json(json).expect("Should parse minimal config");
617
618 assert_f64_eq(config.tdc_frequency_hz, 60.0); assert_eq!(config.chip_transforms.len(), 4); }
621
622 #[test]
623 fn test_json_custom_transforms_only() {
624 let json = r#"{
626 "detector": {
627 "chip_transformations": [
628 {"chip_id": 0, "matrix": [[1, 0, 260], [0, 1, 0]]}
629 ]
630 }
631 }"#;
632
633 let config = DetectorConfig::from_json(json).expect("Should parse");
634
635 assert_f64_eq(config.tdc_frequency_hz, 60.0); assert_eq!(config.chip_transforms[0].tx, 260); }
638
639 fn assert_transform_eq(actual: &ChipTransform, expected: &ChipTransform) {
640 assert_eq!(actual.a, expected.a);
641 assert_eq!(actual.b, expected.b);
642 assert_eq!(actual.c, expected.c);
643 assert_eq!(actual.d, expected.d);
644 assert_eq!(actual.tx, expected.tx);
645 assert_eq!(actual.ty, expected.ty);
646 }
647
648 #[test]
649 fn test_json_roundtrip_serialization() {
650 let config = DetectorConfig {
651 tdc_frequency_hz: 14.0,
652 enable_missing_tdc_correction: false,
653 chip_size_x: 128,
654 chip_size_y: 64,
655 chip_transforms: vec![
656 ChipTransform {
657 a: 1,
658 b: 0,
659 c: 0,
660 d: 1,
661 tx: 10,
662 ty: 20,
663 },
664 ChipTransform {
665 a: -1,
666 b: 0,
667 c: 0,
668 d: -1,
669 tx: 127,
670 ty: 63,
671 },
672 ],
673 };
674
675 let json = config.to_json_string().expect("serialize config");
676 let decoded = DetectorConfig::from_json(&json).expect("roundtrip decode");
677
678 assert_f64_eq(decoded.tdc_frequency_hz, config.tdc_frequency_hz);
679 assert_eq!(
680 decoded.enable_missing_tdc_correction,
681 config.enable_missing_tdc_correction
682 );
683 assert_eq!(decoded.chip_size_x, config.chip_size_x);
684 assert_eq!(decoded.chip_size_y, config.chip_size_y);
685 assert_eq!(decoded.chip_transforms.len(), config.chip_transforms.len());
686 for (actual, expected) in decoded
687 .chip_transforms
688 .iter()
689 .zip(config.chip_transforms.iter())
690 {
691 assert_transform_eq(actual, expected);
692 }
693 }
694
695 #[test]
696 fn test_json_serialization_schema() {
697 let config = DetectorConfig::venus_defaults();
698 let json = config.to_json_string().expect("serialize config");
699 let value: Value = serde_json::from_str(&json).expect("parse json");
700
701 let detector = value
702 .get("detector")
703 .and_then(|v| v.as_object())
704 .expect("detector object");
705 let timing = detector
706 .get("timing")
707 .and_then(|v| v.as_object())
708 .expect("timing object");
709 let layout = detector
710 .get("chip_layout")
711 .and_then(|v| v.as_object())
712 .expect("chip_layout object");
713
714 assert!(timing.contains_key("tdc_frequency_hz"));
715 assert!(timing.contains_key("enable_missing_tdc_correction"));
716 assert!(layout.contains_key("chip_size_x"));
717 assert!(layout.contains_key("chip_size_y"));
718
719 let transforms = detector
720 .get("chip_transformations")
721 .and_then(|v| v.as_array())
722 .expect("chip_transformations array");
723 assert_eq!(transforms.len(), config.chip_transforms.len());
724 let first = transforms[0].as_object().expect("transform object");
725 assert!(first.contains_key("chip_id"));
726 let matrix = first
727 .get("matrix")
728 .and_then(|v| v.as_array())
729 .expect("matrix array");
730 assert_eq!(matrix.len(), 2);
731 assert_eq!(matrix[0].as_array().expect("matrix row").len(), 3);
732 assert_eq!(matrix[1].as_array().expect("matrix row").len(), 3);
733 }
734
735 #[test]
736 fn test_json_empty_transforms_serialization() {
737 let config = DetectorConfig {
738 tdc_frequency_hz: 42.0,
739 enable_missing_tdc_correction: true,
740 chip_size_x: 256,
741 chip_size_y: 256,
742 chip_transforms: Vec::new(),
743 };
744
745 let json = config.to_json_string().expect("serialize config");
746 let value: Value = serde_json::from_str(&json).expect("parse json");
747 let detector = value
748 .get("detector")
749 .and_then(|v| v.as_object())
750 .expect("detector object");
751
752 let transforms = detector
753 .get("chip_transformations")
754 .and_then(|v| v.as_array())
755 .expect("chip_transformations array");
756 assert!(transforms.is_empty());
757
758 let decoded = DetectorConfig::from_json(&json).expect("decode");
759 assert_eq!(decoded.chip_size_x, 256);
760 assert_eq!(decoded.chip_size_y, 256);
761 assert!(decoded.chip_transforms.is_empty());
762 }
763
764 #[test]
765 fn test_venus_transforms_valid() {
766 let config = DetectorConfig::venus_defaults();
768 assert!(config.validate_transforms().is_ok());
769 }
770
771 #[test]
772 fn test_invalid_transform_negative_output() {
773 let json = r#"{
776 "detector": {
777 "chip_transformations": [
778 {"chip_id": 0, "matrix": [[-1, 0, 50], [0, 1, 0]]}
779 ]
780 }
781 }"#;
782
783 let result = DetectorConfig::from_json(json);
784 assert!(result.is_err());
785 let err = result.unwrap_err().to_string();
786 assert!(
787 err.contains("out-of-bounds"),
788 "Error should mention out-of-bounds: {err}"
789 );
790 }
791
792 #[test]
793 fn test_transform_validate_bounds_directly() {
794 let identity = ChipTransform::identity();
796 assert!(identity.validate_bounds(256, 256).is_ok());
797
798 let chip1 = ChipTransform {
800 a: -1,
801 b: 0,
802 c: 0,
803 d: -1,
804 tx: 513,
805 ty: 513,
806 };
807 assert!(chip1.validate_bounds(256, 256).is_ok());
808
809 let invalid = ChipTransform {
812 a: -1,
813 b: 0,
814 c: 0,
815 d: 1,
816 tx: 100,
817 ty: 0,
818 };
819 assert!(invalid.validate_bounds(256, 256).is_err());
820 }
821 #[test]
822 fn test_json_accepts_non_square_chips() {
823 let json = r#"{
824 "detector": {
825 "chip_layout": {
826 "chip_size_x": 256,
827 "chip_size_y": 128
828 }
829 }
830 }"#;
831
832 let config = DetectorConfig::from_json(json).expect("Should accept non-square chips");
833 assert_eq!(config.chip_size_x, 256);
834 assert_eq!(config.chip_size_y, 128);
835 }
836}