1use std::path::{Path, PathBuf};
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::sync::mpsc::{self, Sender};
28use std::sync::Arc;
29use std::thread::JoinHandle;
30
31use readcon_core::helpers::atomic_number_to_symbol;
32use readcon_core::types::ConFrameBuilder;
33
34use crate::corpus::ConCorpus;
35use crate::error::{Error, Result};
36use crate::keys::FrameKey;
37
38struct Row {
39 positions: Vec<f64>,
40 forces: Vec<f64>,
41 energy: f64,
42}
43
44enum Msg {
45 Row(Row),
46 Flush(Sender<()>),
47}
48
49pub struct ObservationArchive {
51 corpus: Arc<ConCorpus>,
52 tx: Option<Sender<Msg>>,
53 worker: Option<JoinHandle<()>>,
54 committed: Arc<AtomicU64>,
55 dropped: Arc<AtomicU64>,
56 appended: AtomicU64,
60 natoms: usize,
61 dir: PathBuf,
62}
63
64impl ObservationArchive {
65 pub fn open(dir: impl AsRef<Path>, z: Vec<u32>, cell: [f64; 3]) -> Result<Self> {
70 let dir = dir.as_ref().to_path_buf();
71 std::fs::create_dir_all(&dir)?;
72 let corpus = Arc::new(ConCorpus::open(dir.join("observations.rdb"))?);
73 let existing = corpus.list_frame_keys()?.len() as u64;
74 let committed = Arc::new(AtomicU64::new(existing));
75 let dropped = Arc::new(AtomicU64::new(0));
76 let (tx, rx) = mpsc::channel::<Msg>();
77 let natoms = z.len();
78
79 let worker_corpus = Arc::clone(&corpus);
80 let worker_committed = Arc::clone(&committed);
81 let worker_dropped = Arc::clone(&dropped);
82 let worker = std::thread::Builder::new()
83 .name("rkrdb-archive".into())
84 .spawn(move || {
85 while let Ok(msg) = rx.recv() {
86 match msg {
87 Msg::Row(row) => {
88 let traj = worker_committed.load(Ordering::Relaxed);
89 match commit_row(&worker_corpus, traj, &z, cell, &row) {
90 Ok(()) => {
91 worker_committed.store(traj + 1, Ordering::Relaxed);
92 }
93 Err(_) => {
94 worker_dropped.fetch_add(1, Ordering::Relaxed);
95 }
96 }
97 }
98 Msg::Flush(ack) => {
99 let _ = ack.send(());
100 }
101 }
102 }
103 })
104 .map_err(|e| Error::Message(format!("archive worker spawn failed: {e}")))?;
105
106 Ok(Self {
107 corpus,
108 tx: Some(tx),
109 worker: Some(worker),
110 committed,
111 dropped,
112 appended: AtomicU64::new(existing),
113 natoms,
114 dir,
115 })
116 }
117
118 pub fn append(&self, positions: &[f64], forces: &[f64], energy: f64) -> bool {
123 if positions.len() != 3 * self.natoms || forces.len() != 3 * self.natoms {
124 return false;
125 }
126 let Some(tx) = self.tx.as_ref() else {
127 return false;
128 };
129 let sent = tx
130 .send(Msg::Row(Row {
131 positions: positions.to_vec(),
132 forces: forces.to_vec(),
133 energy,
134 }))
135 .is_ok();
136 if sent {
137 self.appended.fetch_add(1, Ordering::Relaxed);
138 }
139 sent
140 }
141
142 pub fn flush(&self) {
145 let Some(tx) = self.tx.as_ref() else { return };
146 let (ack_tx, ack_rx) = mpsc::channel();
147 if tx.send(Msg::Flush(ack_tx)).is_ok() {
148 let _ = ack_rx.recv();
149 }
150 }
151
152 pub fn committed(&self) -> u64 {
154 self.committed.load(Ordering::Relaxed)
155 }
156
157 pub fn appended(&self) -> u64 {
161 self.appended.load(Ordering::Relaxed)
162 }
163
164 pub fn dropped(&self) -> u64 {
166 self.dropped.load(Ordering::Relaxed)
167 }
168
169 pub fn natoms(&self) -> usize {
171 self.natoms
172 }
173
174 pub fn dir(&self) -> &Path {
176 &self.dir
177 }
178
179 pub fn fetch(&self, index: u64) -> Result<(Vec<f64>, Vec<f64>, f64)> {
183 let frame = self.corpus.get_frame(FrameKey {
184 traj_id: index,
185 frame_idx: 0,
186 })?;
187 let n = self.natoms;
188 if frame.atom_ids.len() != n {
189 return Err(Error::Message(format!(
190 "archive row {index}: expected {n} atoms, found {}",
191 frame.atom_ids.len()
192 )));
193 }
194 if frame.positions.nrows() != n || frame.forces.nrows() != n {
195 return Err(Error::Parse(format!(
196 "archive row {index}: expected {n} position and force rows, \
197 found {} and {}",
198 frame.positions.nrows(),
199 frame.forces.nrows()
200 )));
201 }
202 let mut positions = vec![0.0; 3 * n];
203 let mut forces = vec![0.0; 3 * n];
204 for (stored, &atom_id) in frame.atom_ids.iter().enumerate() {
205 let orig = atom_id as usize;
206 if orig >= n {
207 return Err(Error::Parse(format!(
208 "archive row {index}: atom id {orig} out of range"
209 )));
210 }
211 positions[3 * orig..3 * orig + 3]
212 .copy_from_slice(&frame.positions.as_f64_row(stored));
213 forces[3 * orig..3 * orig + 3]
214 .copy_from_slice(&frame.forces.as_f64_row(stored));
215 }
216 let energy = frame
217 .header
218 .energy()
219 .ok_or_else(|| Error::Message(format!("archive row {index} has no energy")))?;
220 Ok((positions, forces, energy))
221 }
222}
223
224impl Drop for ObservationArchive {
225 fn drop(&mut self) {
226 self.tx.take();
228 if let Some(worker) = self.worker.take() {
229 let _ = worker.join();
230 }
231 }
232}
233
234fn commit_row(
235 corpus: &ConCorpus,
236 traj: u64,
237 z: &[u32],
238 cell: [f64; 3],
239 row: &Row,
240) -> Result<()> {
241 let mut builder = ConFrameBuilder::new(cell, [90.0, 90.0, 90.0]);
242 builder.prebox_header("observation archive");
243 builder.set_energy(row.energy);
244 for (i, &zi) in z.iter().enumerate() {
245 builder.add_atom(
246 atomic_number_to_symbol(u64::from(zi)),
247 row.positions[3 * i],
248 row.positions[3 * i + 1],
249 row.positions[3 * i + 2],
250 [false; 3],
251 i as u64,
252 f64::from(zi),
253 );
254 }
255 builder
256 .set_forces_from_flat(&row.forces)
257 .map_err(|e| Error::Message(format!("archive forces: {e}")))?;
258 let frame = builder
259 .build()
260 .map_err(|e| Error::Message(format!("archive build: {e}")))?;
261 corpus.append_trajectory_frames_with_precision(traj, &[frame], "observation-archive", 17)?;
263 Ok(())
264}
265
266use std::ffi::CStr;
271use std::os::raw::{c_char, c_int};
272use std::sync::Mutex;
273
274use crate::ffi::{RKRDB_ERR, RKRDB_NOT_FOUND, RKRDB_NULL, RKRDB_OK};
275
276static ARCHIVES: Mutex<Vec<Option<Box<ObservationArchive>>>> = Mutex::new(Vec::new());
277
278fn push_archive(a: ObservationArchive) -> usize {
279 let mut g = ARCHIVES.lock().unwrap();
280 for (i, slot) in g.iter_mut().enumerate() {
281 if slot.is_none() {
282 *slot = Some(Box::new(a));
283 return i;
284 }
285 }
286 g.push(Some(Box::new(a)));
287 g.len() - 1
288}
289
290fn with_archive<F, T>(id: usize, f: F) -> std::result::Result<T, c_int>
291where
292 F: FnOnce(&ObservationArchive) -> std::result::Result<T, c_int>,
293{
294 let g = ARCHIVES.lock().unwrap();
295 let slot = g.get(id).ok_or(RKRDB_NULL)?;
296 let a = slot.as_ref().ok_or(RKRDB_NULL)?;
297 f(a)
298}
299
300#[no_mangle]
305pub unsafe extern "C" fn rkrdb_archive_open(
306 dir: *const c_char,
307 z: *const u32,
308 natoms: u32,
309 cell3: *const f64,
310 out_id: *mut usize,
311) -> c_int {
312 if dir.is_null() || z.is_null() || cell3.is_null() || out_id.is_null() || natoms == 0 {
313 return RKRDB_NULL;
314 }
315 let cdir = unsafe { CStr::from_ptr(dir) };
316 let Ok(dir) = cdir.to_str() else {
317 return RKRDB_ERR;
318 };
319 let z = unsafe { std::slice::from_raw_parts(z, natoms as usize) }.to_vec();
320 let cell = unsafe { [*cell3, *cell3.add(1), *cell3.add(2)] };
321 match ObservationArchive::open(dir, z, cell) {
322 Ok(archive) => {
323 unsafe { *out_id = push_archive(archive) };
324 RKRDB_OK
325 }
326 Err(_) => RKRDB_ERR,
327 }
328}
329
330#[no_mangle]
334pub unsafe extern "C" fn rkrdb_archive_append(
335 id: usize,
336 positions: *const f64,
337 forces: *const f64,
338 energy: f64,
339) -> c_int {
340 if positions.is_null() || forces.is_null() {
341 return RKRDB_NULL;
342 }
343 with_archive(id, |a| {
344 let n3 = 3 * a.natoms();
345 let pos = unsafe { std::slice::from_raw_parts(positions, n3) };
346 let frc = unsafe { std::slice::from_raw_parts(forces, n3) };
347 Ok(if a.append(pos, frc, energy) {
348 RKRDB_OK
349 } else {
350 RKRDB_ERR
351 })
352 })
353 .unwrap_or(RKRDB_NULL)
354}
355
356#[no_mangle]
358pub unsafe extern "C" fn rkrdb_archive_flush(id: usize) -> c_int {
359 with_archive(id, |a| {
360 a.flush();
361 Ok(RKRDB_OK)
362 })
363 .unwrap_or(RKRDB_NULL)
364}
365
366#[no_mangle]
368pub unsafe extern "C" fn rkrdb_archive_count(id: usize, out_count: *mut u64) -> c_int {
369 if out_count.is_null() {
370 return RKRDB_NULL;
371 }
372 with_archive(id, |a| {
373 unsafe { *out_count = a.committed() };
374 Ok(RKRDB_OK)
375 })
376 .unwrap_or(RKRDB_NULL)
377}
378
379#[no_mangle]
383pub unsafe extern "C" fn rkrdb_archive_appended(id: usize, out_count: *mut u64) -> c_int {
384 if out_count.is_null() {
385 return RKRDB_NULL;
386 }
387 with_archive(id, |a| {
388 unsafe { *out_count = a.appended() };
389 Ok(RKRDB_OK)
390 })
391 .unwrap_or(RKRDB_NULL)
392}
393
394#[no_mangle]
396pub unsafe extern "C" fn rkrdb_archive_dropped(id: usize, out_count: *mut u64) -> c_int {
397 if out_count.is_null() {
398 return RKRDB_NULL;
399 }
400 with_archive(id, |a| {
401 unsafe { *out_count = a.dropped() };
402 Ok(RKRDB_OK)
403 })
404 .unwrap_or(RKRDB_NULL)
405}
406
407#[no_mangle]
411pub unsafe extern "C" fn rkrdb_archive_fetch(
412 id: usize,
413 index: u64,
414 positions: *mut f64,
415 forces: *mut f64,
416 capacity_atoms: u32,
417 out_energy: *mut f64,
418) -> c_int {
419 if positions.is_null() || forces.is_null() || out_energy.is_null() {
420 return RKRDB_NULL;
421 }
422 with_archive(id, |a| {
423 if (capacity_atoms as usize) < a.natoms() {
424 return Ok(RKRDB_ERR);
425 }
426 match a.fetch(index) {
427 Ok((pos, frc, energy)) => {
428 unsafe {
429 std::ptr::copy_nonoverlapping(pos.as_ptr(), positions, pos.len());
430 std::ptr::copy_nonoverlapping(frc.as_ptr(), forces, frc.len());
431 *out_energy = energy;
432 }
433 Ok(RKRDB_OK)
434 }
435 Err(Error::MissingFrame(_)) => Ok(RKRDB_NOT_FOUND),
436 Err(_) => Ok(RKRDB_ERR),
437 }
438 })
439 .unwrap_or(RKRDB_NULL)
440}
441
442#[no_mangle]
444pub unsafe extern "C" fn rkrdb_archive_close(id: usize) -> c_int {
445 let mut g = ARCHIVES.lock().unwrap();
446 match g.get_mut(id) {
447 Some(slot) => {
448 *slot = None;
449 RKRDB_OK
450 }
451 None => RKRDB_NULL,
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn roundtrip_preserves_caller_order_and_energy() {
461 let dir = tempfile::tempdir().unwrap();
462 let z = vec![1_u32, 8, 1];
465 let cell = [25.0, 25.0, 25.0];
466 let archive = ObservationArchive::open(dir.path(), z.clone(), cell).unwrap();
467
468 let pos: Vec<f64> = (0..9).map(|i| i as f64 * 0.1).collect();
469 let frc: Vec<f64> = (0..9).map(|i| -(i as f64) * 0.01).collect();
470 assert!(archive.append(&pos, &frc, -76.4));
471 assert!(archive.append(&frc, &pos, -75.9));
472 assert_eq!(archive.appended(), 2);
474 archive.flush();
475 assert_eq!(archive.appended(), 2);
476 assert_eq!(archive.committed(), 2);
477 assert_eq!(archive.dropped(), 0);
478
479 let (p0, f0, e0) = archive.fetch(0).unwrap();
480 assert_eq!(p0, pos);
481 assert_eq!(f0, frc);
482 assert!((e0 - (-76.4)).abs() < 1e-12);
483 let (p1, _f1, e1) = archive.fetch(1).unwrap();
484 assert_eq!(p1, frc);
485 assert!((e1 - (-75.9)).abs() < 1e-12);
486 }
487
488 #[test]
489 fn restart_continues_trajectory_ids() {
490 let dir = tempfile::tempdir().unwrap();
491 let z = vec![6_u32, 1];
492 let cell = [10.0, 10.0, 10.0];
493 let pos = vec![0.0; 6];
494 let frc = vec![0.0; 6];
495 {
496 let archive = ObservationArchive::open(dir.path(), z.clone(), cell).unwrap();
497 assert!(archive.append(&pos, &frc, 1.0));
498 archive.flush();
499 assert_eq!(archive.committed(), 1);
500 }
501 {
502 let archive = ObservationArchive::open(dir.path(), z, cell).unwrap();
503 assert_eq!(archive.committed(), 1);
504 assert!(archive.append(&pos, &frc, 2.0));
505 archive.flush();
506 assert_eq!(archive.committed(), 2);
507 let (_, _, e) = archive.fetch(1).unwrap();
508 assert!((e - 2.0).abs() < 1e-12);
509 }
510 }
511
512 #[test]
513 fn append_rejects_wrong_length() {
514 let dir = tempfile::tempdir().unwrap();
515 let archive =
516 ObservationArchive::open(dir.path(), vec![1, 1], [5.0, 5.0, 5.0]).unwrap();
517 assert!(!archive.append(&[0.0; 3], &[0.0; 6], 0.0));
518 }
519}