Skip to main content

testioc/
image.rs

1//! A simulated detector image: diagonal fringes drifting across the frame,
2//! three gaussian spots on Lissajous paths and a little noise, as 8-bit
3//! pixels. Built from precomputed tables so a 1024x1024 frame costs about a
4//! millisecond, into a small pool of reused buffers.
5
6use std::f32::consts::TAU;
7use std::sync::Arc;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use epics_pva_rs::nt::nd_array::{
11    NdAlarm, NdArrayBuffer, NdCodec, NdDimension, NdTimeStamp, NtNdArray, nt_nd_array_desc,
12    nt_nd_array_value,
13};
14use epics_pva_rs::server_native::SharedPV;
15
16pub const WIDTH: usize = 1024;
17pub const HEIGHT: usize = 1024;
18
19const FRINGE_PERIOD: usize = 128;
20const NOISE_LEN: usize = 1 << 16;
21const SPOT_RADIUS: usize = 120;
22const POOL: usize = 3;
23
24struct Spot {
25    sigma: f32,
26    amp: f32,
27    // Lissajous frequencies (radians per frame) and phases.
28    fx: f32,
29    fy: f32,
30    px: f32,
31    py: f32,
32}
33
34const SPOTS: [Spot; 3] = [
35    Spot {
36        sigma: 40.0,
37        amp: 170.0,
38        fx: 0.031,
39        fy: 0.047,
40        px: 0.0,
41        py: 1.0,
42    },
43    Spot {
44        sigma: 22.0,
45        amp: 150.0,
46        fx: 0.053,
47        fy: 0.029,
48        px: 2.0,
49        py: 0.5,
50    },
51    Spot {
52        sigma: 36.0,
53        amp: 130.0,
54        fx: 0.019,
55        fy: 0.037,
56        px: 4.0,
57        py: 3.0,
58    },
59];
60
61pub struct ImageSim {
62    fringe: Vec<u8>,
63    noise: Vec<u8>,
64    // Per spot, a unit gaussian profile over -SPOT_RADIUS..=SPOT_RADIUS
65    // scaled to 0..=255 and the same scaled by the spot amplitude.
66    profiles: Vec<(Vec<u16>, Vec<u16>)>,
67    pool: Vec<Arc<[i8]>>,
68    rng: u32,
69}
70
71impl Default for ImageSim {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl ImageSim {
78    pub fn new() -> Self {
79        let fringe = (0..WIDTH + FRINGE_PERIOD)
80            .map(|i| {
81                let s = (TAU * i as f32 / FRINGE_PERIOD as f32).sin();
82                (50.0 + 35.0 * s) as u8
83            })
84            .collect();
85        let mut rng = 0x2545_f491;
86        let noise = (0..NOISE_LEN + WIDTH)
87            .map(|_| (xorshift(&mut rng) % 25) as u8)
88            .collect();
89        let profiles = SPOTS
90            .iter()
91            .map(|s| {
92                let g = |d: usize| {
93                    let d = d as f32 - SPOT_RADIUS as f32;
94                    (-d * d / (2.0 * s.sigma * s.sigma)).exp()
95                };
96                let unit = (0..=2 * SPOT_RADIUS)
97                    .map(|d| (255.0 * g(d)) as u16)
98                    .collect();
99                let scaled = (0..=2 * SPOT_RADIUS)
100                    .map(|d| (s.amp * g(d)) as u16)
101                    .collect();
102                (unit, scaled)
103            })
104            .collect();
105        ImageSim {
106            fringe,
107            noise,
108            profiles,
109            pool: Vec::new(),
110            rng,
111        }
112    }
113
114    /// Render frame number `n` into a pooled buffer. Pixels are 0..=255
115    /// stored as i8 bit patterns, as an Int8 simDetector produces.
116    pub fn frame(&mut self, n: u32) -> Arc<[i8]> {
117        let mut buf = match self.pool.iter().position(|b| Arc::strong_count(b) == 1) {
118            Some(i) => self.pool.swap_remove(i),
119            None => Arc::from(vec![0i8; WIDTH * HEIGHT]),
120        };
121        let px = Arc::get_mut(&mut buf).expect("unshared pool buffer");
122        let t = n as f32;
123        let shift = (n as usize * 3) % FRINGE_PERIOD;
124
125        for (y, row) in px.as_chunks_mut::<WIDTH>().0.iter_mut().enumerate() {
126            let f0 = (y + shift) % FRINGE_PERIOD;
127            let n0 = xorshift(&mut self.rng) as usize % NOISE_LEN;
128            let fringe = &self.fringe[f0..f0 + WIDTH];
129            let noise = &self.noise[n0..n0 + WIDTH];
130            for ((p, &f), &z) in row.iter_mut().zip(fringe).zip(noise) {
131                *p = (f + z) as i8;
132            }
133        }
134
135        for (s, (unit, scaled)) in SPOTS.iter().zip(&self.profiles) {
136            let margin = SPOT_RADIUS as f32 + 1.0;
137            let cx = WIDTH as f32 / 2.0 + (WIDTH as f32 / 2.0 - margin) * (s.fx * t + s.px).sin();
138            let cy = HEIGHT as f32 / 2.0 + (HEIGHT as f32 / 2.0 - margin) * (s.fy * t + s.py).sin();
139            let (x0, y0) = (cx as usize - SPOT_RADIUS, cy as usize - SPOT_RADIUS);
140            let xs = &unit[..];
141            for (dy, &gy) in scaled.iter().enumerate() {
142                let start = (y0 + dy) * WIDTH + x0;
143                let row = &mut px[start..start + xs.len()];
144                for (p, &gx) in row.iter_mut().zip(xs) {
145                    let add = ((gx * gy) >> 8) as u8;
146                    *p = (*p as u8).saturating_add(add) as i8;
147                }
148            }
149        }
150
151        self.pool.push(buf.clone());
152        if self.pool.len() > POOL {
153            self.pool.remove(0);
154        }
155        buf
156    }
157}
158
159fn xorshift(s: &mut u32) -> u32 {
160    *s ^= *s << 13;
161    *s ^= *s >> 17;
162    *s ^= *s << 5;
163    *s
164}
165
166fn now() -> NdTimeStamp {
167    let d = SystemTime::now()
168        .duration_since(UNIX_EPOCH)
169        .unwrap_or_default();
170    NdTimeStamp {
171        seconds_past_epoch: d.as_secs() as i64,
172        nanoseconds: d.subsec_nanos() as i32,
173        user_tag: 0,
174    }
175}
176
177fn nt(pixels: Arc<[i8]>, dims: &[usize], unique_id: i32) -> NtNdArray {
178    let ts = now();
179    let size = pixels.len() as i64;
180    NtNdArray {
181        value: NdArrayBuffer::Byte(pixels.into()),
182        codec: NdCodec::default(),
183        compressed_size: size,
184        uncompressed_size: size,
185        unique_id,
186        data_time_stamp: ts.clone(),
187        alarm: NdAlarm::default(),
188        time_stamp: ts,
189        dimension: dims
190            .iter()
191            .map(|&d| NdDimension {
192                size: d as i32,
193                full_size: d as i32,
194                binning: 1,
195                ..NdDimension::default()
196            })
197            .collect(),
198        attribute: Vec::new(),
199    }
200}
201
202/// The NTNDArray PV, opened with an empty array until the first frame.
203pub fn open_pv() -> SharedPV {
204    let pv = SharedPV::build_readonly();
205    let empty = nt(Arc::from(Vec::new()), &[], 0);
206    pv.open(nt_nd_array_desc(), nt_nd_array_value(&empty))
207        .expect("NTNDArray value fits its descriptor");
208    pv
209}
210
211/// Post a frame as NDPluginPVA would: dimension [x, y], uncompressed.
212pub fn post(pv: &SharedPV, pixels: Arc<[i8]>, unique_id: i32) {
213    pv.try_post(nt_nd_array_value(&nt(pixels, &[WIDTH, HEIGHT], unique_id)));
214}