1use std::fmt;
2use std::fs::{remove_file, File, OpenOptions};
3use std::io;
4use std::io::prelude::*;
5use std::io::{BufReader, BufWriter};
6use std::path::Path;
7
8use byteorder::{ByteOrder, LittleEndian};
9use float_cmp::ApproxEqUlps;
10use rand::{RngExt, SeedableRng, rngs::StdRng, seq::SliceRandom};
11
12const HEADER_LENGTH: usize = 25;
13const MAX_RECORD_LENGTH: usize = 32;
14const BUFFER_CAPACITY: usize = 1024 * 1024;
15const MODE_LENGTH: usize = 5;
16const BATCHES: usize = 128; #[derive(Debug, Copy, Clone)]
19pub struct Header {
20 pub mode: [u8; 5],
21 pub total_particles: i32,
22 pub total_photons: i32,
23 pub min_energy: f32,
24 pub max_energy: f32,
25 pub total_particles_in_source: f32,
26 pub record_size: u64,
27 pub using_zlast: bool,
28}
29
30#[derive(Debug, Copy, Clone)]
31pub struct Record {
32 pub latch: u32,
33 total_energy: f32,
34 pub x_cm: f32,
35 pub y_cm: f32,
36 pub x_cos: f32, pub y_cos: f32,
38 pub weight: f32, pub zlast: Option<f32>,
40}
41
42#[derive(Debug)]
43pub struct Transform;
44
45#[derive(Debug)]
46pub enum EGSError {
47 Io(io::Error),
48 BadMode,
49 BadLength,
50 ModeMismatch,
51 HeaderMismatch,
52 RecordMismatch,
53}
54
55pub type EGSResult<T> = Result<T, EGSError>;
56
57impl From<io::Error> for EGSError {
58 fn from(err: io::Error) -> EGSError {
59 EGSError::Io(err)
60 }
61}
62
63impl fmt::Display for EGSError {
64 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 match *self {
66 EGSError::Io(ref err) => err.fmt(f),
67 EGSError::BadMode => {
68 write!(
69 f,
70 "First 5 bytes of file are invalid, must be MODE0 or MODE2"
71 )
72 }
73 EGSError::BadLength => {
74 write!(
75 f,
76 "Number of total particles does notmatch byte length of file"
77 )
78 }
79 EGSError::ModeMismatch => write!(f, "Input file MODE0/MODE2 do not match"),
80 EGSError::HeaderMismatch => write!(f, "Headers are different"),
81 EGSError::RecordMismatch => write!(f, "Records are different"),
82 }
83 }
84}
85
86pub struct PHSPReader {
87 reader: BufReader<File>,
88 pub header: Header,
89 next_record: u64,
90}
91
92pub struct PHSPWriter {
93 writer: BufWriter<File>,
94 pub header: Header,
95}
96
97impl PHSPReader {
98 pub fn from(file: File) -> EGSResult<PHSPReader> {
99 let actual_size = (file.metadata()?).len();
100 let mut reader = BufReader::with_capacity(BUFFER_CAPACITY, file);
101 let mut buffer = [0; HEADER_LENGTH];
102 reader.read_exact(&mut buffer)?;
103 let mut mode = [0; MODE_LENGTH];
104 mode.clone_from_slice(&buffer[0..5]);
105 let header = Header {
106 mode,
107 total_particles: LittleEndian::read_i32(&buffer[5..9]),
108 total_photons: LittleEndian::read_i32(&buffer[9..13]),
109 max_energy: LittleEndian::read_f32(&buffer[13..17]),
110 min_energy: LittleEndian::read_f32(&buffer[17..21]),
111 total_particles_in_source: LittleEndian::read_f32(&buffer[21..25]),
112 using_zlast: &mode == b"MODE2",
113 record_size: if &mode == b"MODE0" {
114 28
115 } else if &mode == b"MODE2" {
116 32
117 } else {
118 return Err(EGSError::BadMode);
119 },
120 };
121 if actual_size != header.expected_size() as u64 {
122 writeln!(
123 &mut std::io::stderr(),
124 "Expected {} bytes in file, not {}",
125 header.expected_size(),
126 actual_size
127 )
128 .unwrap();
129 }
131 reader.consume(header.record_size as usize - HEADER_LENGTH);
132 Ok(PHSPReader {
133 reader,
134 header,
135 next_record: 0,
136 })
137 }
138 fn exhausted(&self) -> bool {
139 self.next_record >= self.header.total_particles as u64
140 }
141}
142
143impl Iterator for PHSPReader {
144 type Item = EGSResult<Record>;
145 fn next(&mut self) -> Option<EGSResult<Record>> {
146 if self.next_record >= self.header.total_particles as u64 {
147 return None;
148 }
149 let mut buffer = [0; MAX_RECORD_LENGTH];
150 match self
151 .reader
152 .read_exact(&mut buffer[..self.header.record_size as usize])
153 {
154 Ok(()) => (),
155 Err(err) => return Some(Err(EGSError::Io(err))),
156 };
157 self.next_record += 1;
158 Some(Ok(Record {
159 latch: LittleEndian::read_u32(&buffer[0..4]),
160 total_energy: LittleEndian::read_f32(&buffer[4..8]),
161 x_cm: LittleEndian::read_f32(&buffer[8..12]),
162 y_cm: LittleEndian::read_f32(&buffer[12..16]),
163 x_cos: LittleEndian::read_f32(&buffer[16..20]),
164 y_cos: LittleEndian::read_f32(&buffer[20..24]),
165 weight: LittleEndian::read_f32(&buffer[24..28]),
166 zlast: if self.header.using_zlast {
167 Some(LittleEndian::read_f32(&buffer[28..32]))
168 } else {
169 None
170 },
171 }))
172 }
173}
174
175impl PHSPWriter {
176 pub fn from(file: File, header: &Header) -> EGSResult<PHSPWriter> {
177 let mut writer = BufWriter::with_capacity(BUFFER_CAPACITY, file);
178 let mut buffer = [0; MAX_RECORD_LENGTH];
179 buffer[0..5].clone_from_slice(&header.mode);
180 LittleEndian::write_i32(&mut buffer[5..9], header.total_particles);
181 LittleEndian::write_i32(&mut buffer[9..13], header.total_photons);
182 LittleEndian::write_f32(&mut buffer[13..17], header.max_energy);
183 LittleEndian::write_f32(&mut buffer[17..21], header.min_energy);
184 LittleEndian::write_f32(&mut buffer[21..25], header.total_particles_in_source);
185 writer.write_all(&buffer[..header.record_size as usize])?;
186 Ok(PHSPWriter {
187 header: *header,
188 writer,
189 })
190 }
191
192 pub fn write(&mut self, record: &Record) -> EGSResult<()> {
193 let mut buffer = [0; 32];
194 LittleEndian::write_u32(&mut buffer[0..4], record.latch);
195 LittleEndian::write_f32(&mut buffer[4..8], record.total_energy);
196 LittleEndian::write_f32(&mut buffer[8..12], record.x_cm);
197 LittleEndian::write_f32(&mut buffer[12..16], record.y_cm);
198 LittleEndian::write_f32(&mut buffer[16..20], record.x_cos);
199 LittleEndian::write_f32(&mut buffer[20..24], record.y_cos);
200 LittleEndian::write_f32(&mut buffer[24..28], record.weight);
201 if self.header.using_zlast {
202 LittleEndian::write_f32(&mut buffer[28..32], record.weight);
203 }
204 self.writer
205 .write_all(&buffer[..self.header.record_size as usize])?;
206 Ok(())
207 }
208}
209
210impl Header {
211 fn expected_size(&self) -> usize {
212 (self.total_particles as usize + 1) * self.record_size as usize
213 }
214 pub fn similar_to(&self, other: &Header) -> bool {
215 self.mode == other.mode
216 && self.total_particles == other.total_particles
217 && self.total_photons == other.total_photons
218 && self.max_energy.approx_eq_ulps(&other.max_energy, 10)
219 && self.min_energy.approx_eq_ulps(&other.min_energy, 10)
220 && self
221 .total_particles_in_source
222 .approx_eq_ulps(&other.total_particles_in_source, 2)
223 }
224 fn merge(&mut self, other: &Header) {
225 assert!(self.mode == other.mode, "Merge mode mismatch");
226 self.total_particles = self
227 .total_particles
228 .checked_add(other.total_particles)
229 .expect("Too many particles, i32 overflow");
230 self.total_photons += other.total_photons;
231 self.min_energy = self.min_energy.min(other.min_energy);
232 self.max_energy = self.max_energy.max(other.max_energy);
233 self.total_particles_in_source += other.total_particles_in_source;
234 }
235}
236
237impl Record {
238 pub fn similar_to(&self, other: &Record) -> bool {
239 self.latch == other.latch
240 && self.total_energy() - other.total_energy() < 0.01
241 && self.x_cm - other.x_cm < 0.01
242 && self.y_cm - other.y_cm < 0.01
243 && self.x_cos - other.x_cos < 0.01
244 && self.y_cos - other.y_cos < 0.01
245 && self.weight - other.weight < 0.01
246 && self.zlast == other.zlast
247 }
248 pub fn bremsstrahlung_or_annihilation(&self) -> bool {
249 self.latch & 1 != 0
250 }
251 pub fn bit_region(&self) -> u32 {
252 self.latch & 0xfffffe
253 }
254 pub fn region_number(&self) -> u32 {
255 self.latch & 0xf000000
256 }
257 pub fn b29(&self) -> bool {
258 self.latch & (1 << 29) != 0
259 }
260 pub fn charged(&self) -> bool {
261 self.latch & (1 << 30) != 0
262 }
263 pub fn crossed_multiple(&self) -> bool {
264 self.latch & (1 << 30) != 0
265 }
266 pub fn get_weight(&self) -> f32 {
267 self.weight.abs()
268 }
269 pub fn set_weight(&mut self, new_weight: f32) {
270 self.weight = new_weight * self.weight.signum();
271 }
272 pub fn total_energy(&self) -> f32 {
273 self.total_energy.abs()
274 }
275 pub fn z_positive(&self) -> bool {
276 self.weight.is_sign_positive()
277 }
278 pub fn z_cos(&self) -> f32 {
279 (1.0 - (self.x_cos * self.x_cos + self.y_cos * self.y_cos)).sqrt()
280 }
281 pub fn first_scored_by_primary_history(&self) -> bool {
282 self.total_energy.is_sign_negative()
283 }
284
285 fn translate(&mut self, x: f32, y: f32) {
286 self.x_cm += x;
287 self.y_cm += y;
288 }
289
290 fn transform(&mut self, matrix: &[[f32; 3]; 3]) {
291 let x_cm = self.x_cm;
292 let y_cm = self.y_cm;
293 self.x_cm = matrix[0][0] * x_cm + matrix[0][1] * y_cm + matrix[0][2] * 1.0;
294 self.y_cm = matrix[1][0] * x_cm + matrix[1][1] * y_cm + matrix[1][2] * 1.0;
295 let x_cos = self.x_cos;
296 let y_cos = self.y_cos;
297 self.x_cos = matrix[0][0] * x_cos + matrix[0][1] * y_cos + matrix[0][2] * self.z_cos();
298 self.y_cos = matrix[1][0] * x_cos + matrix[1][1] * y_cos + matrix[1][2] * self.z_cos();
299 }
300}
301
302impl Transform {
303 pub fn reflection(matrix: &mut [[f32; 3]; 3], x_raw: f32, y_raw: f32) {
304 let norm = (x_raw * x_raw + y_raw * y_raw).sqrt();
305 let x = x_raw / norm;
306 let y = y_raw / norm;
307 *matrix = [
308 [x * x - y * y, 2.0 * x * y, 0.0],
309 [2.0 * x * y, y * y - x * x, 0.0],
310 [0.0, 0.0, 1.0],
311 ];
312 }
313 pub fn rotation(matrix: &mut [[f32; 3]; 3], theta: f32) {
314 *matrix = [
315 [theta.cos(), -theta.sin(), 0.0],
316 [theta.sin(), theta.cos(), 0.0],
317 [0.0, 0.0, 1.0],
318 ];
319 }
320}
321
322pub fn randomize(path: &Path, seed: u64) -> EGSResult<()> {
323 let mut rng = StdRng::seed_from_u64(seed);
324 let ifile = File::open(path)?;
325 let mut reader = PHSPReader::from(ifile)?;
326 let header = reader.header;
327 let max_per_batch = reader.header.total_particles as usize / BATCHES + 1;
328 let mut batch_paths = Vec::with_capacity(BATCHES);
329 for i in 0..BATCHES {
330 let mut batch_path = path.to_path_buf();
331 batch_path.set_extension(format!("rand{}", i));
332 batch_paths.push(batch_path);
333 }
334 let mut records = Vec::with_capacity(max_per_batch);
335 for path in batch_paths.iter() {
336 for _ in 0..max_per_batch {
337 if let Some(record) = reader.next() { records.push(record.unwrap()) }
338 }
339 records.shuffle(&mut rng);
342
343 let header = Header {
344 mode: reader.header.mode,
345 total_particles: records.len() as i32,
346 total_photons: 0,
347 max_energy: 0.0,
348 min_energy: 0.0,
349 total_particles_in_source: 0.0,
350 using_zlast: &reader.header.mode == b"MODE2",
351 record_size: reader.header.record_size,
352 };
353 let ofile = File::create(path)?;
354 let mut writer = PHSPWriter::from(ofile, &header)?;
355 for record in records.iter() {
356 writer.write(record)?;
357 }
358 records.clear();
359 }
360 drop(records);
361 let mut readers = Vec::with_capacity(BATCHES);
362 for path in batch_paths.iter() {
363 let ifile = File::open(path)?;
364 readers.push(PHSPReader::from(ifile)?);
365 }
366
367 let ofile = File::create(path)?;
368 let mut writer = PHSPWriter::from(ofile, &header)?;
369 while !readers.is_empty() {
370 readers.shuffle(&mut rng);
371 for reader in readers.iter_mut() {
372 if let Some(record) = reader.next() { writer.write(&record.unwrap())? }
373 }
374 readers.retain(|r| !r.exhausted());
375 }
376 for path in batch_paths.iter() {
377 remove_file(path)?;
378 }
379 Ok(())
380}
381
382pub fn combine(input_paths: &[&Path], output_path: &Path, delete: bool) -> EGSResult<()> {
383 assert!(!input_paths.is_empty(), "Cannot combine zero files");
384 let reader = PHSPReader::from(File::open(input_paths[0])?)?;
385 let mut final_header = reader.header;
386 for path in input_paths[1..].iter() {
387 let reader = PHSPReader::from(File::open(path)?)?;
388 final_header.merge(&reader.header);
389 }
390 println!("Final header: {:?}", final_header);
391 let ofile = File::create(output_path)?;
392 let mut writer = PHSPWriter::from(ofile, &final_header)?;
393 for path in input_paths.iter() {
394 let reader = PHSPReader::from(File::open(path)?)?;
395 for record in reader {
396 writer.write(&record.unwrap())?
397 }
398 if delete {
399 remove_file(path)?;
400 }
401 }
402 Ok(())
403}
404
405pub fn compare(path1: &Path, path2: &Path) -> EGSResult<()> {
406 let ifile1 = File::open(path1)?;
407 let ifile2 = File::open(path2)?;
408 let reader1 = PHSPReader::from(ifile1)?;
409 let reader2 = PHSPReader::from(ifile2)?;
410 println!(" First\t\tSecond");
411 println!(
412 "Total particles: {0: <10}\t\t{1:}",
413 reader1.header.total_particles, reader2.header.total_particles
414 );
415 println!(
416 "Total photons: {0: <10}\t\t{1}",
417 reader1.header.total_photons, reader2.header.total_photons
418 );
419 println!(
420 "Minimum energy: {0: <10}\t\t{1}",
421 reader1.header.min_energy, reader2.header.min_energy
422 );
423 println!(
424 "Maximum energy: {0: <10}\t\t{1}",
425 reader1.header.max_energy, reader2.header.max_energy
426 );
427 println!(
428 "Source particles: {0: <10}\t\t{1}",
429 reader1.header.total_particles_in_source, reader2.header.total_particles_in_source
430 );
431 if !reader1.header.similar_to(&reader2.header) {
432 println!("Headers different");
433 return Err(EGSError::HeaderMismatch);
434 } else {
435 for (record1, record2) in reader1.zip(reader2) {
436 let r1 = record1.unwrap();
437 let r2 = record2.unwrap();
438 if !r1.similar_to(&r2) {
439 println!("{:?} != {:?}", r1, r2);
440 return Err(EGSError::RecordMismatch);
441 }
442 }
443 }
444 Ok(())
445}
446
447pub fn sample_combine(ipaths: &[&Path], opath: &Path, rate: f64, seed: u64) -> EGSResult<()> {
448 assert!(!ipaths.is_empty(), "Cannot combine zero files");
449 let mut rng = StdRng::seed_from_u64(seed);
450 let mut header = Header {
451 mode: *b"MODE0",
452 record_size: 28,
453 using_zlast: false,
454 total_particles: 0,
455 total_photons: 0,
456 min_energy: 1000.0,
457 max_energy: 0.0,
458 total_particles_in_source: 0.0,
459 };
460 let mut writer = PHSPWriter::from(File::create(opath)?, &header)?;
461 for path in ipaths.iter() {
462 let reader = PHSPReader::from(File::open(path)?)?;
463 assert!(!reader.header.using_zlast);
464 println!("Found {} particles", reader.header.total_particles);
465 header.total_particles_in_source += reader.header.total_particles_in_source;
466 let records = reader.filter(|_| rng.random_bool(rate));
467 for record in records.map(|r| r.unwrap()) {
468 header.total_particles = header
469 .total_particles
470 .checked_add(1)
471 .expect("Total particles overflow");
472 if !record.charged() {
473 header.total_photons += 1;
474 }
475 if record.total_energy > 0.0 {
476 header.min_energy = header.min_energy.min(record.total_energy);
477 header.max_energy = header.max_energy.max(record.total_energy);
478 }
479 writer.write(&record)?;
480 }
481 println!("Now have {} particles", header.total_particles);
482 }
483 header.total_particles_in_source *= rate as f32;
484 drop(writer);
485 let ofile = OpenOptions::new()
487 .write(true)
488 .create(true)
489 .truncate(false)
490 .open(opath)?;
491 PHSPWriter::from(ofile, &header)?;
492 Ok(())
493}
494
495pub fn translate(input_path: &Path, output_path: &Path, x: f32, y: f32) -> EGSResult<()> {
496 let ifile = File::open(input_path)?;
497 let reader = PHSPReader::from(ifile)?;
498 let ofile = if input_path == output_path {
499 println!(
500 "Translating {} in place by ({}, {})",
501 input_path.display(),
502 x,
503 y
504 );
505 OpenOptions::new()
506 .write(true)
507 .create(true)
508 .truncate(false)
509 .open(output_path)?
510 } else {
511 println!(
512 "Translating {} by ({}, {}) and saving to {}",
513 input_path.display(),
514 x,
515 y,
516 output_path.display()
517 );
518 File::create(output_path)?
519 };
520 let mut writer = PHSPWriter::from(ofile, &reader.header)?;
521 let n_particles = reader.header.total_particles;
522 let mut records_translated = 0;
523 for mut record in reader.map(|r| r.unwrap()) {
524 record.translate(x, y);
525 writer.write(&record)?;
526 records_translated += 1;
527 }
528 println!(
529 "Translated {} records, expected {}",
530 records_translated, n_particles
531 );
532 Ok(())
533}
534
535pub fn transform(input_path: &Path, output_path: &Path, matrix: &[[f32; 3]; 3]) -> EGSResult<()> {
536 let ifile = File::open(input_path)?;
537 let reader = PHSPReader::from(ifile)?;
538 let ofile = if input_path == output_path {
539 println!("Transforming {} in place", input_path.display());
540 OpenOptions::new()
541 .write(true)
542 .create(true)
543 .truncate(false)
544 .open(output_path)?
545 } else {
546 println!(
547 "Transforming {} and saving to {}",
548 input_path.display(),
549 output_path.display()
550 );
551 File::create(output_path)?
552 };
553 let mut writer = PHSPWriter::from(ofile, &reader.header)?;
554 let n_particles = reader.header.total_particles;
555 let mut records_transformed = 0;
556 for mut record in reader.map(|r| r.unwrap()) {
557 record.transform(matrix);
558 writer.write(&record)?;
559 records_transformed += 1;
560 }
561 println!(
562 "Transformed {} records, expected {}",
563 records_transformed, n_particles
564 );
565 Ok(())
566}
567
568pub fn reweight(
569 input_path: &Path,
570 output_path: &Path,
571 f: &dyn Fn(f32) -> f32,
572 number_bins: usize,
573 max_radius: f32,
574) -> EGSResult<()> {
575 let input_file = File::open(input_path)?;
576 let output_file = if input_path == output_path {
577 println!("Reweighting in-place");
578 OpenOptions::new()
579 .write(true)
580 .create(true)
581 .truncate(false)
582 .open(output_path)?
583 } else {
584 println!("Rewighting and saving to {}", output_path.display());
585 File::create(output_path)?
586 };
587 let reader1 = PHSPReader::from(input_file)?;
588 let mut writer1 = PHSPWriter::from(output_file, &reader1.header)?;
589 let bin_size = max_radius / number_bins as f32;
590 println!("Bin size is {}", bin_size);
591 let mut sum_old_weight = 0.0;
592 let mut sum_new_weight = 0.0;
593 for mut record in reader1.map(|r| r.unwrap()) {
594 sum_old_weight += record.weight;
595 let r = (record.x_cm * record.x_cm + record.y_cm * record.y_cm).sqrt();
596 record.weight *= f(r);
597 sum_new_weight += record.weight;
598 writer1.write(&record)?;
599 }
600 drop(writer1);
601 let ifile2 = File::open(input_path)?;
602 let ofile2 = if input_path == output_path {
603 OpenOptions::new()
604 .write(true)
605 .create(true)
606 .truncate(false)
607 .open(output_path)?
608 } else {
609 File::create(output_path)?
610 };
611 let reader2 = PHSPReader::from(ifile2)?;
612 let mut writer2 = PHSPWriter::from(ofile2, &reader2.header)?;
613 let factor = sum_old_weight / sum_new_weight;
614 for mut record in reader2.map(|r| r.unwrap()) {
615 record.weight *= factor;
616 writer2.write(&record)?;
617 }
618 Ok(())
619}