1use std::collections::BTreeSet;
2use std::fs;
3use std::io::Cursor;
4use std::path::{Path, PathBuf};
5
6use heed::types::{Bytes, Str, Unit};
7use heed::{Database, Env, EnvOpenOptions, RwTxn};
8use readcon_core::iterators::ConFrameIterator;
9use readcon_core::types::ConFrame;
10use readcon_core::writer::ConFrameWriter;
11use serde::{Deserialize, Serialize};
12
13use crate::error::{Error, Result};
14use crate::export_xyz::write_frame_extxyz;
15use crate::frame_scalars::{
16 frame_cell_volume, frame_charge, frame_frame_index, frame_magmom, frame_neb_band, frame_neb_bead,
17 frame_pbc_mask, frame_time, frame_timestep, frame_total_mass,
18};
19use crate::keys::{
20 composition_formula, elem_count_key, elem_count_symbol_prefix, energy_bin_key, flag_key,
21 fmax_bin_key, formula_key, formula_prefix, hash_frame_bytes, mass_bin_key, meta_scalar_key,
22 natoms_key, ordered_f64_bits, parse_elem_count_key, pbc_key, pbc_mask_from_bools,
23 species_counts_from_symbols, symbol_key, symbol_prefix, volume_bin_key, ContentHash, FrameKey,
24 TrajId, FLAG_HAS_ENERGY, FLAG_HAS_FORCES, FLAG_HAS_VELOCITIES, META_CHARGE, META_FRAME_INDEX,
25 META_MAGMOM, META_NEB_BAND, META_NEB_BEAD, META_TIME, META_TIMESTEP,
26};
27use crate::select::Select;
28
29const MAP_SIZE: usize = 2 * 1024 * 1024 * 1024;
31const MAX_DBS: u32 = 48;
32const MAX_READERS: u32 = 512;
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct TrajMeta {
39 pub n_frames: u32,
40 pub source: String,
41}
42
43struct PreparedIndexPuts {
45 fk: FrameKey,
46 blob: String,
47 hash: [u8; 16],
48 natoms_k: [u8; 16],
49 symbol_keys: Vec<Vec<u8>>,
50 elem_count_keys: Vec<Vec<u8>>,
51 formula_k: Option<Vec<u8>>,
52 energy_k: Option<[u8; 20]>,
53 fmax_k: Option<[u8; 20]>,
54 mass_k: Option<[u8; 20]>,
55 volume_k: Option<[u8; 20]>,
56 pbc_k: Option<[u8; 13]>,
57 flag_keys: Vec<[u8; 13]>,
58 meta_keys: Vec<[u8; 21]>,
59}
60
61pub struct ConCorpus {
62 path: PathBuf,
63 env: Env,
64 frames: Database<Bytes, Str>,
65 traj_meta: Database<Bytes, Str>,
66 idx_natoms: Database<Bytes, Unit>,
67 idx_symbol: Database<Bytes, Unit>,
68 idx_energy: Database<Bytes, Unit>,
69 idx_flags: Database<Bytes, Unit>,
70 idx_elem_count: Database<Bytes, Unit>,
72 idx_formula: Database<Bytes, Unit>,
74 idx_fmax: Database<Bytes, Unit>,
76 idx_mass: Database<Bytes, Unit>,
77 idx_volume: Database<Bytes, Unit>,
78 idx_pbc: Database<Bytes, Unit>,
80 idx_meta: Database<Bytes, Unit>,
82 frame_by_hash: Database<Bytes, Bytes>,
83 hash_by_frame: Database<Bytes, Bytes>,
84 frames_soa: Database<Bytes, Bytes>,
86}
87
88fn frame_has_forces(frame: &ConFrame) -> bool {
89 readcon_core::index_proj::frame_has_forces(frame)
90}
91
92fn frame_has_velocities(frame: &ConFrame) -> bool {
93 readcon_core::index_proj::frame_has_velocities(frame)
94}
95
96fn frame_energy(frame: &ConFrame) -> Option<f64> {
97 readcon_core::index_proj::finite_energy(frame)
98}
99
100pub fn frame_fmax(frame: &ConFrame) -> Option<f64> {
102 readcon_core::index_proj::frame_fmax(frame)
103}
104
105fn frame_species(frame: &ConFrame) -> Vec<(String, u32)> {
106 readcon_core::index_proj::frame_species_counts(frame)
107}
108
109impl ConCorpus {
110 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
111 let path = path.as_ref().to_path_buf();
112 fs::create_dir_all(&path)?;
113 let mut opts = EnvOpenOptions::new();
114 opts.map_size(MAP_SIZE);
115 opts.max_dbs(MAX_DBS);
116 opts.max_readers(MAX_READERS);
117 let env = unsafe { opts.open(&path)? };
119
120 let mut wtxn = env.write_txn()?;
121 let frames = env.create_database(&mut wtxn, Some("frames"))?;
122 let traj_meta = env.create_database(&mut wtxn, Some("traj_meta"))?;
123 let idx_natoms = env.create_database(&mut wtxn, Some("idx_natoms"))?;
124 let idx_symbol = env.create_database(&mut wtxn, Some("idx_symbol"))?;
125 let idx_energy = env.create_database(&mut wtxn, Some("idx_energy"))?;
126 let idx_flags = env.create_database(&mut wtxn, Some("idx_flags"))?;
127 let idx_elem_count = env.create_database(&mut wtxn, Some("idx_elem_count"))?;
128 let idx_formula = env.create_database(&mut wtxn, Some("idx_formula"))?;
129 let idx_fmax = env.create_database(&mut wtxn, Some("idx_fmax"))?;
130 let idx_mass = env.create_database(&mut wtxn, Some("idx_mass"))?;
131 let idx_volume = env.create_database(&mut wtxn, Some("idx_volume"))?;
132 let idx_pbc = env.create_database(&mut wtxn, Some("idx_pbc"))?;
133 let idx_meta = env.create_database(&mut wtxn, Some("idx_meta"))?;
134 let frame_by_hash = env.create_database(&mut wtxn, Some("frame_by_hash"))?;
135 let hash_by_frame = env.create_database(&mut wtxn, Some("hash_by_frame"))?;
136 let frames_soa = env.create_database(&mut wtxn, Some("frames_soa"))?;
137 wtxn.commit()?;
138
139 Ok(Self {
140 path,
141 env,
142 frames,
143 traj_meta,
144 idx_natoms,
145 idx_symbol,
146 idx_energy,
147 idx_flags,
148 idx_elem_count,
149 idx_formula,
150 idx_fmax,
151 idx_mass,
152 idx_volume,
153 idx_pbc,
154 idx_meta,
155 frame_by_hash,
156 hash_by_frame,
157 frames_soa,
158 })
159 }
160
161 pub fn path(&self) -> &Path {
162 &self.path
163 }
164
165 pub fn list_frame_keys(&self) -> Result<Vec<FrameKey>> {
167 let rtxn = self.env.read_txn()?;
168 let mut out = Vec::new();
169 let mut iter = self.frames.iter(&rtxn)?;
170 while let Some(Ok((k, _))) = iter.next() {
171 if let Some(fk) = FrameKey::from_bytes(k) {
172 out.push(fk);
173 }
174 }
175 out.sort();
176 Ok(out)
177 }
178
179 pub fn export_frame_blob(&self, key: FrameKey) -> Result<String> {
182 self.get_frame_text(key)
183 }
184
185
186 fn build_index_puts(fk: FrameKey, frame: &ConFrame, blob: String) -> PreparedIndexPuts {
189 use readcon_core::index_proj::FrameIndexProjection;
190 let hash = hash_frame_bytes(blob.as_bytes()).to_bytes();
191 let proj = FrameIndexProjection::from_frame(frame);
192 let mut symbol_keys = Vec::new();
193 let mut elem_count_keys = Vec::new();
194 let mut seen = BTreeSet::new();
195 for (sym, cnt) in &proj.species_counts {
196 elem_count_keys.push(elem_count_key(sym, *cnt, fk));
197 if seen.insert(sym.clone()) {
198 symbol_keys.push(symbol_key(sym, fk));
199 }
200 }
201 let formula_k = if proj.formula.is_empty() {
202 None
203 } else {
204 Some(formula_key(&proj.formula, fk))
205 };
206 let mut flag_keys = Vec::new();
207 let energy_k = proj.energy.and_then(|e| {
208 flag_keys.push(flag_key(FLAG_HAS_ENERGY, fk));
209 energy_bin_key(e, fk)
210 });
211 let fmax_k = if proj.has_forces {
212 flag_keys.push(flag_key(FLAG_HAS_FORCES, fk));
213 proj.fmax.and_then(|fm| fmax_bin_key(fm, fk))
214 } else {
215 None
216 };
217 if proj.has_velocities {
218 flag_keys.push(flag_key(FLAG_HAS_VELOCITIES, fk));
219 }
220 let mass_k = proj.total_mass.and_then(|m| mass_bin_key(m, fk));
221 let volume_k = proj.cell_volume.and_then(|v| volume_bin_key(v, fk));
222 let pbc_k = proj
223 .pbc
224 .map(|p| pbc_key(crate::keys::pbc_mask_from_bools(p), fk));
225 let mut meta_keys = Vec::new();
226 for (ch, val) in [
227 (META_TIME, proj.time),
228 (META_TIMESTEP, proj.timestep),
229 (META_FRAME_INDEX, proj.frame_index),
230 (META_NEB_BEAD, proj.neb_bead),
231 (META_NEB_BAND, proj.neb_band),
232 (META_CHARGE, proj.charge),
233 (META_MAGMOM, proj.magmom),
234 ] {
235 if let Some(x) = val {
236 if let Some(k) = meta_scalar_key(ch, x, fk) {
237 meta_keys.push(k);
238 }
239 }
240 }
241 PreparedIndexPuts {
242 fk,
243 blob,
244 hash,
245 natoms_k: natoms_key(proj.n_atoms, fk),
246 symbol_keys,
247 elem_count_keys,
248 formula_k,
249 energy_k,
250 fmax_k,
251 mass_k,
252 volume_k,
253 pbc_k,
254 flag_keys,
255 meta_keys,
256 }
257 }
258
259 fn put_index_puts(&self, wtxn: &mut RwTxn, p: &PreparedIndexPuts) -> Result<()> {
261 let fk_b = p.fk.to_bytes();
262 self.frames.put(wtxn, &fk_b[..], p.blob.as_str())?;
263 self.hash_by_frame.put(wtxn, &fk_b[..], &p.hash[..])?;
264 if self.frame_by_hash.get(wtxn, &p.hash[..])?.is_none() {
265 self.frame_by_hash.put(wtxn, &p.hash[..], &fk_b[..])?;
266 }
267 self.idx_natoms.put(wtxn, &p.natoms_k[..], &())?;
268 for sk in &p.symbol_keys {
269 self.idx_symbol.put(wtxn, &sk[..], &())?;
270 }
271 for ek in &p.elem_count_keys {
272 self.idx_elem_count.put(wtxn, &ek[..], &())?;
273 }
274 if let Some(ref fk_form) = p.formula_k {
275 self.idx_formula.put(wtxn, &fk_form[..], &())?;
276 }
277 if let Some(ek) = p.energy_k {
278 self.idx_energy.put(wtxn, &ek[..], &())?;
279 }
280 if let Some(fk_fm) = p.fmax_k {
281 self.idx_fmax.put(wtxn, &fk_fm[..], &())?;
282 }
283 if let Some(k) = p.mass_k {
284 self.idx_mass.put(wtxn, &k[..], &())?;
285 }
286 if let Some(k) = p.volume_k {
287 self.idx_volume.put(wtxn, &k[..], &())?;
288 }
289 if let Some(pk) = p.pbc_k {
290 self.idx_pbc.put(wtxn, &pk[..], &())?;
291 }
292 for fk_flag in &p.flag_keys {
293 self.idx_flags.put(wtxn, &fk_flag[..], &())?;
294 }
295 for mk in &p.meta_keys {
296 self.idx_meta.put(wtxn, &mk[..], &())?;
297 }
298 Ok(())
299 }
300
301 fn index_frame(&self, wtxn: &mut RwTxn, fk: FrameKey, frame: &ConFrame, blob: &str) -> Result<()> {
303 let puts = Self::build_index_puts(fk, frame, blob.to_owned());
304 self.put_index_puts(wtxn, &puts)
305 }
306
307 fn prepare_trajectory_str(
309 traj_id: TrajId,
310 file_contents: &str,
311 ) -> Result<Vec<PreparedIndexPuts>> {
312 let mut out = Vec::new();
313 let mut frame_idx: u32 = 0;
314 let mut iter = ConFrameIterator::new(file_contents);
315 loop {
316 let (frame, blob) = match iter.next_with_raw_span(file_contents) {
317 None => break,
318 Some(Err(e)) => return Err(Error::Parse(e.to_string())),
319 Some(Ok(x)) => x,
320 };
321 let fk = FrameKey {
322 traj_id,
323 frame_idx,
324 };
325 out.push(Self::build_index_puts(fk, &frame, blob.to_owned()));
326 frame_idx += 1;
327 }
328 Ok(out)
329 }
330
331 fn prepare_trajectory_frames(
333 traj_id: TrajId,
334 frames: &[ConFrame],
335 start_idx: u32,
336 ) -> Result<Vec<PreparedIndexPuts>> {
337 let mut out = Vec::with_capacity(frames.len());
338 let mut frame_idx = start_idx;
339 for fr in frames {
340 let mut buf = Cursor::new(Vec::new());
341 {
342 let mut w = ConFrameWriter::new(&mut buf);
343 w.write_frame(fr)
344 .map_err(|e| Error::Parse(format!("serialize: {e}")))?;
345 }
346 let blob = String::from_utf8(buf.into_inner())
347 .map_err(|e| Error::Message(format!("utf8: {e}")))?;
348 let fk = FrameKey {
349 traj_id,
350 frame_idx,
351 };
352 out.push(Self::build_index_puts(fk, fr, blob));
353 frame_idx += 1;
354 }
355 Ok(out)
356 }
357
358 fn commit_prepared(
360 &self,
361 traj_id: TrajId,
362 prepared: &[PreparedIndexPuts],
363 source: String,
364 replace_meta: bool,
365 ) -> Result<u32> {
366 let mut wtxn = self.env.write_txn()?;
367 let tid_key = traj_id.to_be_bytes();
368 if !replace_meta {
369 if let Some(existing) = self.traj_meta.get(&wtxn, &tid_key[..])? {
370 let meta: TrajMeta = serde_json::from_str(existing)?;
371 return Err(Error::TrajExists(traj_id, meta.n_frames));
372 }
373 }
374 for p in prepared {
375 self.put_index_puts(&mut wtxn, p)?;
376 }
377 let n_frames = prepared
378 .last()
379 .map(|p| p.fk.frame_idx + 1)
380 .unwrap_or(0);
381 let meta = TrajMeta {
382 n_frames,
383 source,
384 };
385 self.traj_meta
386 .put(&mut wtxn, &tid_key[..], &serde_json::to_string(&meta)?)?;
387 wtxn.commit()?;
388 Ok(n_frames)
389 }
390
391 pub fn append_trajectory_path(&self, traj_id: TrajId, file: impl AsRef<Path>) -> Result<u32> {
392 let text = fs::read_to_string(file.as_ref())?;
393 self.append_trajectory_str(traj_id, &text, file.as_ref().display().to_string())
394 }
395
396 pub fn append_trajectory_str(
397 &self,
398 traj_id: TrajId,
399 file_contents: &str,
400 source: impl Into<String>,
401 ) -> Result<u32> {
402 let source = source.into();
403 let prepared = Self::prepare_trajectory_str(traj_id, file_contents)?;
405 self.commit_prepared(traj_id, &prepared, source, false)
406 }
407
408 pub fn append_trajectory_frames(
410 &self,
411 traj_id: TrajId,
412 frames: &[ConFrame],
413 source: impl Into<String>,
414 ) -> Result<u32> {
415 let source = source.into();
416 let prepared = Self::prepare_trajectory_frames(traj_id, frames, 0)?;
417 self.commit_prepared(traj_id, &prepared, source, false)
418 }
419
420 pub fn extend_trajectory_frames(
423 &self,
424 traj_id: TrajId,
425 frames: &[ConFrame],
426 source_hint: impl Into<String>,
427 ) -> Result<u32> {
428 let (start_idx, source) = {
429 let rtxn = self.env.read_txn()?;
430 let tid_key = traj_id.to_be_bytes();
431 if let Some(existing) = self.traj_meta.get(&rtxn, &tid_key[..])? {
432 let meta: TrajMeta = serde_json::from_str(existing)?;
433 (meta.n_frames, meta.source)
434 } else {
435 (0u32, source_hint.into())
436 }
437 };
438 let prepared = Self::prepare_trajectory_frames(traj_id, frames, start_idx)?;
440 let mut wtxn = self.env.write_txn()?;
441 let tid_key = traj_id.to_be_bytes();
442 let live_n = if let Some(existing) = self.traj_meta.get(&wtxn, &tid_key[..])? {
443 let meta: TrajMeta = serde_json::from_str(existing)?;
444 meta.n_frames
445 } else {
446 0u32
447 };
448 if live_n != start_idx {
449 return Err(Error::Message(format!(
450 "concurrent extend race on traj {traj_id}: expected start {start_idx}, live {live_n}"
451 )));
452 }
453 for p in &prepared {
454 self.put_index_puts(&mut wtxn, p)?;
455 }
456 let n_frames = prepared
457 .last()
458 .map(|p| p.fk.frame_idx + 1)
459 .unwrap_or(start_idx);
460 let meta = TrajMeta {
461 n_frames,
462 source,
463 };
464 self.traj_meta
465 .put(&mut wtxn, &tid_key[..], &serde_json::to_string(&meta)?)?;
466 wtxn.commit()?;
467 Ok(n_frames)
468 }
469
470 fn clear_secondary(&self, wtxn: &mut RwTxn) -> Result<()> {
471 self.idx_natoms.clear(wtxn)?;
472 self.idx_symbol.clear(wtxn)?;
473 self.idx_energy.clear(wtxn)?;
474 self.idx_flags.clear(wtxn)?;
475 self.idx_elem_count.clear(wtxn)?;
476 self.idx_formula.clear(wtxn)?;
477 self.idx_fmax.clear(wtxn)?;
478 self.idx_mass.clear(wtxn)?;
479 self.idx_volume.clear(wtxn)?;
480 self.idx_pbc.clear(wtxn)?;
481 self.idx_meta.clear(wtxn)?;
482 self.frame_by_hash.clear(wtxn)?;
483 self.hash_by_frame.clear(wtxn)?;
484 Ok(())
485 }
486
487 pub fn reindex(&self) -> Result<u32> {
489 let mut wtxn = self.env.write_txn()?;
490 self.clear_secondary(&mut wtxn)?;
491
492 let mut n = 0u32;
493 let mut keys: Vec<Vec<u8>> = Vec::new();
495 {
496 let mut iter = self.frames.iter(&wtxn)?;
497 while let Some(Ok((k, _))) = iter.next() {
498 keys.push(k.to_vec());
499 }
500 }
501 for k in keys {
502 let Some(fk) = FrameKey::from_bytes(&k) else {
503 continue;
504 };
505 let Some(blob) = self.frames.get(&wtxn, &k)? else {
506 continue;
507 };
508 let blob_owned = blob.to_owned();
509 let mut it = ConFrameIterator::new(&blob_owned);
510 let frame = it
511 .next()
512 .ok_or_else(|| Error::Parse("empty blob on reindex".into()))?
513 .map_err(|e| Error::Parse(e.to_string()))?;
514 let hash = hash_frame_bytes(blob_owned.as_bytes());
516 let hb = hash.to_bytes();
517 let fk_b = fk.to_bytes();
518 self.hash_by_frame.put(&mut wtxn, &fk_b[..], &hb[..])?;
519 if self.frame_by_hash.get(&wtxn, &hb[..])?.is_none() {
520 self.frame_by_hash.put(&mut wtxn, &hb[..], &fk_b[..])?;
521 }
522 let n_atoms = frame.atom_data.len() as u32;
523 self.idx_natoms
524 .put(&mut wtxn, &natoms_key(n_atoms, fk)[..], &())?;
525 let counts = frame_species(&frame);
526 for (sym, cnt) in &counts {
527 self.idx_elem_count
528 .put(&mut wtxn, &elem_count_key(sym, *cnt, fk)[..], &())?;
529 self.idx_symbol
530 .put(&mut wtxn, &symbol_key(sym, fk)[..], &())?;
531 }
532 let formula = composition_formula(&counts);
533 if !formula.is_empty() {
534 self.idx_formula
535 .put(&mut wtxn, &formula_key(&formula, fk)[..], &())?;
536 }
537 if let Some(e) = frame_energy(&frame) {
538 if let Some(ek) = energy_bin_key(e, fk) {
539 self.idx_energy.put(&mut wtxn, &ek[..], &())?;
540 }
541 self.idx_flags
542 .put(&mut wtxn, &flag_key(FLAG_HAS_ENERGY, fk)[..], &())?;
543 }
544 if frame_has_forces(&frame) {
545 self.idx_flags
546 .put(&mut wtxn, &flag_key(FLAG_HAS_FORCES, fk)[..], &())?;
547 if let Some(fm) = frame_fmax(&frame) {
548 if let Some(fk_fm) = fmax_bin_key(fm, fk) {
549 self.idx_fmax.put(&mut wtxn, &fk_fm[..], &())?;
550 }
551 }
552 }
553 if frame_has_velocities(&frame) {
554 self.idx_flags
555 .put(&mut wtxn, &flag_key(FLAG_HAS_VELOCITIES, fk)[..], &())?;
556 }
557 if let Some(m) = frame_total_mass(&frame) {
558 if let Some(k) = mass_bin_key(m, fk) {
559 self.idx_mass.put(&mut wtxn, &k[..], &())?;
560 }
561 }
562 if let Some(v) = frame_cell_volume(&frame) {
563 if let Some(k) = volume_bin_key(v, fk) {
564 self.idx_volume.put(&mut wtxn, &k[..], &())?;
565 }
566 }
567 if let Some(mask) = frame_pbc_mask(&frame) {
568 self.idx_pbc.put(&mut wtxn, &pbc_key(mask, fk)[..], &())?;
569 }
570 let meta_puts: [(u8, Option<f64>); 7] = [
571 (META_TIME, frame_time(&frame)),
572 (META_TIMESTEP, frame_timestep(&frame)),
573 (META_FRAME_INDEX, frame_frame_index(&frame)),
574 (META_NEB_BEAD, frame_neb_bead(&frame)),
575 (META_NEB_BAND, frame_neb_band(&frame)),
576 (META_CHARGE, frame_charge(&frame)),
577 (META_MAGMOM, frame_magmom(&frame)),
578 ];
579 for (ch, val) in meta_puts {
580 if let Some(x) = val {
581 if let Some(k) = meta_scalar_key(ch, x, fk) {
582 self.idx_meta.put(&mut wtxn, &k[..], &())?;
583 }
584 }
585 }
586 n += 1;
587 }
588 wtxn.commit()?;
589 Ok(n)
590 }
591
592 pub fn traj_meta(&self, traj_id: TrajId) -> Result<Option<TrajMeta>> {
593 let rtxn = self.env.read_txn()?;
594 let tid_key = traj_id.to_be_bytes();
595 match self.traj_meta.get(&rtxn, &tid_key[..])? {
596 None => Ok(None),
597 Some(s) => Ok(Some(serde_json::from_str(s)?)),
598 }
599 }
600
601 pub fn get_frame_text(&self, key: FrameKey) -> Result<String> {
602 let rtxn = self.env.read_txn()?;
603 self.get_frame_text_txn(&rtxn, key)
604 }
605
606 fn get_frame_text_txn(
607 &self,
608 rtxn: &heed::RoTxn<'_>,
609 key: FrameKey,
610 ) -> Result<String> {
611 let fk_b = key.to_bytes();
612 match self.frames.get(rtxn, &fk_b[..])? {
613 Some(s) => Ok(s.to_owned()),
614 None => Err(Error::MissingFrame(key)),
615 }
616 }
617
618 pub fn get_frame_texts(&self, keys: &[FrameKey]) -> Result<Vec<String>> {
620 let rtxn = self.env.read_txn()?;
621 let mut out = Vec::with_capacity(keys.len());
622 for &key in keys {
623 out.push(self.get_frame_text_txn(&rtxn, key)?);
624 }
625 Ok(out)
626 }
627
628 pub fn touch_trajectory_blobs(&self, traj_id: TrajId, n_frames: u32) -> Result<(u64, u64)> {
634 let rtxn = self.env.read_txn()?;
635 let mut total = 0u64;
636 let mut checksum = 0u64;
637 for frame_idx in 0..n_frames {
638 let key = FrameKey {
639 traj_id,
640 frame_idx,
641 };
642 let owned = self.get_frame_text_txn(&rtxn, key)?;
644 total += owned.len() as u64;
645 for (i, b) in owned.as_bytes().iter().enumerate() {
646 checksum = checksum.wrapping_add((*b as u64).wrapping_mul((i as u64).wrapping_add(1)));
647 }
648 }
649 std::hint::black_box(checksum);
651 Ok((total, checksum))
652 }
653
654 pub fn get_frame(&self, key: FrameKey) -> Result<ConFrame> {
655 let text = self.get_frame_text(key)?;
656 let mut it = ConFrameIterator::new(&text);
657 let fr = it
658 .next()
659 .ok_or_else(|| Error::Parse("empty blob".into()))?
660 .map_err(|e| Error::Parse(e.to_string()))?;
661 Ok(fr)
662 }
663
664 pub fn get_cooked_soa_bytes(&self, key: FrameKey) -> Result<Option<Vec<u8>>> {
667 let rtxn = self.env.read_txn()?;
668 let fk_b = key.to_bytes();
669 Ok(self
670 .frames_soa
671 .get(&rtxn, &fk_b[..])?
672 .map(|b| b.to_vec()))
673 }
674
675 pub fn has_valid_cooked_soa(&self, key: FrameKey) -> Result<bool> {
678 Ok(self.get_cooked_soa(key)?.is_some())
679 }
680
681 pub fn get_cooked_soa(&self, key: FrameKey) -> Result<Option<crate::cooked_soa::CookedSoa>> {
684 Ok(self
685 .get_cooked_soa_bytes(key)?
686 .and_then(|b| crate::cooked_soa::CookedSoa::try_decode(&b)))
687 }
688
689 pub fn get_positions(&self, key: FrameKey) -> Result<Vec<[f64; 3]>> {
692 if let Some(c) = self.get_cooked_soa(key)? {
694 return Ok(c.positions);
695 }
696 let fr = self.get_frame(key)?;
698 Ok(fr
699 .atom_data
700 .iter()
701 .map(|a| [a.x, a.y, a.z])
702 .collect())
703 }
704
705 pub fn get_forces(&self, key: FrameKey) -> Result<Option<Vec<[f64; 3]>>> {
708 if let Some(c) = self.get_cooked_soa(key)? {
709 if c.forces.is_some() {
710 return Ok(c.forces);
711 }
712 }
714 let fr = self.get_frame(key)?;
715 if !fr.atom_data.iter().any(|a| a.force.is_some()) {
716 return Ok(None);
717 }
718 Ok(Some(
719 fr.atom_data
720 .iter()
721 .map(|a| a.force.unwrap_or([0.0; 3]))
722 .collect(),
723 ))
724 }
725
726 pub fn get_velocities(&self, key: FrameKey) -> Result<Option<Vec<[f64; 3]>>> {
728 if let Some(c) = self.get_cooked_soa(key)? {
729 if c.velocities.is_some() {
730 return Ok(c.velocities);
731 }
732 }
733 let fr = self.get_frame(key)?;
734 if !fr.atom_data.iter().any(|a| a.velocity.is_some()) {
735 return Ok(None);
736 }
737 Ok(Some(
738 fr.atom_data
739 .iter()
740 .map(|a| a.velocity.unwrap_or([0.0; 3]))
741 .collect(),
742 ))
743 }
744
745 pub fn cook_frame(&self, key: FrameKey) -> Result<usize> {
747 let fr = self.get_frame(key)?;
748 let bytes = crate::cooked_soa::CookedSoa::encode_frame(&fr)?;
749 let mut wtxn = self.env.write_txn()?;
750 let fk_b = key.to_bytes();
751 self.frames_soa.put(&mut wtxn, &fk_b[..], &bytes[..])?;
752 wtxn.commit()?;
753 Ok(bytes.len())
754 }
755
756 pub fn recook_all(&self) -> Result<u32> {
758 let keys = self.list_frame_keys()?;
759 let mut n = 0u32;
760 for k in keys {
761 self.cook_frame(k)?;
762 n += 1;
763 }
764 Ok(n)
765 }
766
767 pub fn delete_cooked_soa(&self, key: FrameKey) -> Result<()> {
769 let mut wtxn = self.env.write_txn()?;
770 let fk_b = key.to_bytes();
771 let _ = self.frames_soa.delete(&mut wtxn, &fk_b[..])?;
772 wtxn.commit()?;
773 Ok(())
774 }
775
776 pub fn append_trajectory_path_cook(
778 &self,
779 traj_id: TrajId,
780 file: impl AsRef<Path>,
781 cook: bool,
782 ) -> Result<u32> {
783 let n = self.append_trajectory_path(traj_id, file)?;
784 if cook {
785 for i in 0..n {
786 self.cook_frame(FrameKey {
787 traj_id,
788 frame_idx: i,
789 })?;
790 }
791 }
792 Ok(n)
793 }
794
795 pub fn frame_hash(&self, key: FrameKey) -> Result<ContentHash> {
796 let rtxn = self.env.read_txn()?;
797 let fk_b = key.to_bytes();
798 match self.hash_by_frame.get(&rtxn, &fk_b[..])? {
799 Some(b) => ContentHash::from_bytes(b).ok_or_else(|| Error::Message("bad hash".into())),
800 None => {
801 let text = self.get_frame_text(key)?;
802 Ok(hash_frame_bytes(text.as_bytes()))
803 }
804 }
805 }
806
807 pub fn find_by_hash(&self, hash: ContentHash) -> Result<Option<FrameKey>> {
808 let rtxn = self.env.read_txn()?;
809 let hb = hash.to_bytes();
810 match self.frame_by_hash.get(&rtxn, &hb[..])? {
811 Some(b) => Ok(FrameKey::from_bytes(b)),
812 None => Ok(None),
813 }
814 }
815
816 pub fn hash_con_text(text: &str) -> Result<ContentHash> {
817 let mut it = ConFrameIterator::new(text);
818 let frame = it
819 .next()
820 .ok_or_else(|| Error::Parse("no frame".into()))?
821 .map_err(|e| Error::Parse(e.to_string()))?;
822 let mut buf = Cursor::new(Vec::new());
823 {
824 let mut w = ConFrameWriter::new(&mut buf);
825 w.write_frame(&frame)
826 .map_err(|e| Error::Parse(format!("re-serialize: {e}")))?;
827 }
828 Ok(hash_frame_bytes(&buf.into_inner()))
829 }
830
831 pub fn frame_formula(&self, key: FrameKey) -> Result<String> {
833 let fr = self.get_frame(key)?;
834 Ok(composition_formula(&frame_species(&fr)))
835 }
836
837 pub fn select(&self, sel: &Select) -> Result<Vec<FrameKey>> {
838 let rtxn = self.env.read_txn()?;
839 let mut sets: Vec<BTreeSet<FrameKey>> = Vec::new();
840
841 if let Some(h) = sel.content_hash {
842 let mut s = BTreeSet::new();
843 if let Some(b) = self.frame_by_hash.get(&rtxn, &h[..])? {
844 if let Some(fk) = FrameKey::from_bytes(b) {
845 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
846 s.insert(fk);
847 }
848 }
849 }
850 sets.push(s);
851 }
852
853 if sel.natoms_min.is_some() || sel.natoms_max.is_some() {
854 let lo = sel.natoms_min.unwrap_or(0);
855 let hi = sel.natoms_max.unwrap_or(u32::MAX);
856 let mut s = BTreeSet::new();
857 let mut iter = self.idx_natoms.iter(&rtxn)?;
858 while let Some(Ok((k, _))) = iter.next() {
859 if k.len() < 16 {
860 continue;
861 }
862 let mut nb = [0u8; 4];
863 nb.copy_from_slice(&k[..4]);
864 let n = u32::from_be_bytes(nb);
865 if n > hi {
866 break;
867 }
868 if n >= lo {
869 if let Some(fk) = FrameKey::from_bytes(&k[4..16]) {
870 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
871 s.insert(fk);
872 }
873 }
874 }
875 }
876 sets.push(s);
877 }
878
879 for sym in &sel.symbols_all {
880 let mut s = BTreeSet::new();
881 let pref = symbol_prefix(sym);
882 let mut iter = self.idx_symbol.prefix_iter(&rtxn, &pref)?;
883 while let Some(Ok((k, _))) = iter.next() {
884 if k.len() < 12 {
885 continue;
886 }
887 let fk_bytes = &k[k.len() - 12..];
888 if let Some(fk) = FrameKey::from_bytes(fk_bytes) {
889 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
890 s.insert(fk);
891 }
892 }
893 }
894 sets.push(s);
895 }
896
897 if let Some(ref formula) = sel.exact_formula {
898 let mut s = BTreeSet::new();
899 let pref = formula_prefix(formula);
900 let mut iter = self.idx_formula.prefix_iter(&rtxn, &pref)?;
901 while let Some(Ok((k, _))) = iter.next() {
902 if k.len() < 12 {
903 continue;
904 }
905 let fk_bytes = &k[k.len() - 12..];
906 if let Some(fk) = FrameKey::from_bytes(fk_bytes) {
907 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
908 s.insert(fk);
909 }
910 }
911 }
912 sets.push(s);
913 }
914
915 for (sym, exact_c) in &sel.element_count_exact {
916 let mut s = BTreeSet::new();
917 let pref = elem_count_symbol_prefix(sym);
918 let mut iter = self.idx_elem_count.prefix_iter(&rtxn, &pref)?;
919 while let Some(Ok((k, _))) = iter.next() {
920 if let Some((c, fk)) = parse_elem_count_key(k, sym) {
921 if c == *exact_c && sel.traj_id.is_none_or(|t| t == fk.traj_id) {
922 s.insert(fk);
923 }
924 }
925 }
926 sets.push(s);
927 }
928
929 for (sym, min_c) in &sel.element_count_min {
930 let mut s = BTreeSet::new();
931 let pref = elem_count_symbol_prefix(sym);
932 let mut iter = self.idx_elem_count.prefix_iter(&rtxn, &pref)?;
933 while let Some(Ok((k, _))) = iter.next() {
934 if let Some((c, fk)) = parse_elem_count_key(k, sym) {
935 if c >= *min_c && sel.traj_id.is_none_or(|t| t == fk.traj_id) {
936 s.insert(fk);
937 }
938 }
939 }
940 sets.push(s);
941 }
942
943 if sel.energy_min.is_some() || sel.energy_max.is_some() {
944 let lo_e = sel.energy_min.unwrap_or(f64::NEG_INFINITY);
945 let hi_e = sel.energy_max.unwrap_or(f64::INFINITY);
946 let lo_bits = ordered_f64_bits(lo_e).unwrap_or(0);
947 let hi_bits = ordered_f64_bits(hi_e).unwrap_or(u64::MAX);
948 let mut s = BTreeSet::new();
949 let mut iter = self.idx_energy.iter(&rtxn)?;
950 while let Some(Ok((k, _))) = iter.next() {
951 if k.len() < 20 {
952 continue;
953 }
954 let mut eb = [0u8; 8];
955 eb.copy_from_slice(&k[..8]);
956 let bits = u64::from_be_bytes(eb);
957 if bits > hi_bits {
958 break;
959 }
960 if bits >= lo_bits {
961 if let Some(fk) = FrameKey::from_bytes(&k[8..20]) {
962 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
963 s.insert(fk);
964 }
965 }
966 }
967 }
968 sets.push(s);
969 }
970
971 if sel.fmax_min.is_some() || sel.fmax_max.is_some() {
972 let lo_e = sel.fmax_min.unwrap_or(0.0);
973 let hi_e = sel.fmax_max.unwrap_or(f64::INFINITY);
974 let lo_bits = ordered_f64_bits(lo_e).unwrap_or(0);
975 let hi_bits = ordered_f64_bits(hi_e).unwrap_or(u64::MAX);
976 let mut s = BTreeSet::new();
977 let mut iter = self.idx_fmax.iter(&rtxn)?;
978 while let Some(Ok((k, _))) = iter.next() {
979 if k.len() < 20 {
980 continue;
981 }
982 let mut eb = [0u8; 8];
983 eb.copy_from_slice(&k[..8]);
984 let bits = u64::from_be_bytes(eb);
985 if bits > hi_bits {
986 break;
987 }
988 if bits >= lo_bits {
989 if let Some(fk) = FrameKey::from_bytes(&k[8..20]) {
990 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
991 s.insert(fk);
992 }
993 }
994 }
995 }
996 sets.push(s);
997 }
998
999 let scan_ord = |db: Database<Bytes, Unit>, lo: Option<f64>, hi: Option<f64>| -> Result<BTreeSet<FrameKey>> {
1000 let lo_e = lo.unwrap_or(f64::NEG_INFINITY);
1001 let hi_e = hi.unwrap_or(f64::INFINITY);
1002 let lo_bits = ordered_f64_bits(lo_e).unwrap_or(0);
1003 let hi_bits = ordered_f64_bits(hi_e).unwrap_or(u64::MAX);
1004 let mut s = BTreeSet::new();
1005 let mut iter = db.iter(&rtxn)?;
1006 while let Some(Ok((k, _))) = iter.next() {
1007 if k.len() < 20 {
1008 continue;
1009 }
1010 let mut eb = [0u8; 8];
1011 eb.copy_from_slice(&k[..8]);
1012 let bits = u64::from_be_bytes(eb);
1013 if bits > hi_bits {
1014 break;
1015 }
1016 if bits >= lo_bits {
1017 if let Some(fk) = FrameKey::from_bytes(&k[8..20]) {
1018 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
1019 s.insert(fk);
1020 }
1021 }
1022 }
1023 }
1024 Ok(s)
1025 };
1026
1027 if sel.mass_min.is_some() || sel.mass_max.is_some() {
1028 sets.push(scan_ord(self.idx_mass, sel.mass_min, sel.mass_max)?);
1029 }
1030 if sel.volume_min.is_some() || sel.volume_max.is_some() {
1031 sets.push(scan_ord(self.idx_volume, sel.volume_min, sel.volume_max)?);
1032 }
1033
1034 if let Some(pbc) = sel.pbc_exact {
1035 let mask = pbc_mask_from_bools(pbc);
1036 let mut s = BTreeSet::new();
1037 let pref = [mask];
1038 let mut iter = self.idx_pbc.prefix_iter(&rtxn, &pref)?;
1039 while let Some(Ok((k, _))) = iter.next() {
1040 if k.len() < 13 {
1041 continue;
1042 }
1043 if let Some(fk) = FrameKey::from_bytes(&k[1..13]) {
1044 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
1045 s.insert(fk);
1046 }
1047 }
1048 }
1049 sets.push(s);
1050 }
1051
1052 let scan_meta = |ch: u8, lo: Option<f64>, hi: Option<f64>| -> Result<BTreeSet<FrameKey>> {
1053 let lo_e = lo.unwrap_or(f64::NEG_INFINITY);
1054 let hi_e = hi.unwrap_or(f64::INFINITY);
1055 let lo_bits = ordered_f64_bits(lo_e).unwrap_or(0);
1056 let hi_bits = ordered_f64_bits(hi_e).unwrap_or(u64::MAX);
1057 let mut s = BTreeSet::new();
1058 let pref = [ch];
1059 let mut iter = self.idx_meta.prefix_iter(&rtxn, &pref)?;
1060 while let Some(Ok((k, _))) = iter.next() {
1061 if k.len() < 21 {
1062 continue;
1063 }
1064 let mut eb = [0u8; 8];
1065 eb.copy_from_slice(&k[1..9]);
1066 let bits = u64::from_be_bytes(eb);
1067 if bits > hi_bits {
1068 continue; }
1070 if bits >= lo_bits {
1071 if let Some(fk) = FrameKey::from_bytes(&k[9..21]) {
1072 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
1073 s.insert(fk);
1074 }
1075 }
1076 }
1077 }
1078 Ok(s)
1079 };
1080
1081 if sel.time_min.is_some() || sel.time_max.is_some() {
1082 sets.push(scan_meta(META_TIME, sel.time_min, sel.time_max)?);
1083 }
1084 if sel.timestep_min.is_some() || sel.timestep_max.is_some() {
1085 sets.push(scan_meta(META_TIMESTEP, sel.timestep_min, sel.timestep_max)?);
1086 }
1087 if sel.frame_index_min.is_some() || sel.frame_index_max.is_some() {
1088 sets.push(scan_meta(META_FRAME_INDEX, sel.frame_index_min, sel.frame_index_max)?);
1089 }
1090 if sel.neb_bead_min.is_some() || sel.neb_bead_max.is_some() {
1091 sets.push(scan_meta(META_NEB_BEAD, sel.neb_bead_min, sel.neb_bead_max)?);
1092 }
1093 if sel.neb_band_min.is_some() || sel.neb_band_max.is_some() {
1094 sets.push(scan_meta(META_NEB_BAND, sel.neb_band_min, sel.neb_band_max)?);
1095 }
1096 if sel.charge_min.is_some() || sel.charge_max.is_some() {
1097 sets.push(scan_meta(META_CHARGE, sel.charge_min, sel.charge_max)?);
1098 }
1099 if sel.magmom_min.is_some() || sel.magmom_max.is_some() {
1100 sets.push(scan_meta(META_MAGMOM, sel.magmom_min, sel.magmom_max)?);
1101 }
1102
1103 let mut push_flag = |flag: u8| -> Result<()> {
1104 let mut s = BTreeSet::new();
1105 let pref = [flag];
1106 let mut iter = self.idx_flags.prefix_iter(&rtxn, &pref)?;
1107 while let Some(Ok((k, _))) = iter.next() {
1108 if k.len() < 13 {
1109 continue;
1110 }
1111 if let Some(fk) = FrameKey::from_bytes(&k[1..13]) {
1112 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
1113 s.insert(fk);
1114 }
1115 }
1116 }
1117 sets.push(s);
1118 Ok(())
1119 };
1120 if sel.require_forces {
1121 push_flag(FLAG_HAS_FORCES)?;
1122 }
1123 if sel.require_velocities {
1124 push_flag(FLAG_HAS_VELOCITIES)?;
1125 }
1126 if sel.require_energy {
1127 push_flag(FLAG_HAS_ENERGY)?;
1128 }
1129
1130 if sets.is_empty() {
1131 let mut s = BTreeSet::new();
1132 let mut iter = self.frames.iter(&rtxn)?;
1133 while let Some(Ok((k, _))) = iter.next() {
1134 if let Some(fk) = FrameKey::from_bytes(k) {
1135 if sel.traj_id.is_none_or(|t| t == fk.traj_id) {
1136 s.insert(fk);
1137 }
1138 }
1139 }
1140 sets.push(s);
1141 } else if let Some(tid) = sel.traj_id {
1142 for set in &mut sets {
1143 set.retain(|fk| fk.traj_id == tid);
1144 }
1145 }
1146
1147 sets.sort_by_key(|s| s.len());
1149 let mut acc = sets.remove(0);
1150 for s in sets {
1151 if acc.is_empty() {
1152 break;
1153 }
1154 if s.len() < acc.len() {
1155 acc = s.intersection(&acc).copied().collect();
1156 } else {
1157 acc = acc.intersection(&s).copied().collect();
1158 }
1159 }
1160
1161 let mut out: Vec<FrameKey> = acc.into_iter().collect();
1162 out.sort_unstable();
1163 if let Some(lim) = sel.limit {
1164 out.truncate(lim);
1165 }
1166 Ok(out)
1167 }
1168
1169 pub fn export_extxyz(
1170 &self,
1171 keys: &[FrameKey],
1172 path: impl AsRef<Path>,
1173 energy_key: &str,
1174 ) -> Result<usize> {
1175 use std::fs::File;
1176 use std::io::BufWriter;
1177 let mut w = BufWriter::new(File::create(path)?);
1178 let mut n = 0usize;
1179 for k in keys {
1180 let fr = self.get_frame(*k)?;
1181 write_frame_extxyz(&mut w, &fr, energy_key)?;
1182 n += 1;
1183 }
1184 Ok(n)
1185 }
1186
1187 pub fn ingest_directory(
1188 &self,
1189 dir: impl AsRef<Path>,
1190 start_traj_id: TrajId,
1191 ) -> Result<Vec<(TrajId, u32, String)>> {
1192 let mut out = Vec::new();
1193 let mut tid = start_traj_id;
1194 let mut paths: Vec<_> = fs::read_dir(dir)?
1195 .filter_map(|e| e.ok())
1196 .map(|e| e.path())
1197 .filter(|p| {
1198 let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
1199 name.ends_with(".con") || name.ends_with(".convel")
1200 })
1201 .collect();
1202 paths.sort();
1203 for p in paths {
1204 let n = self.append_trajectory_path(tid, &p)?;
1205 out.push((tid, n, p.display().to_string()));
1206 tid += 1;
1207 }
1208 Ok(out)
1209 }
1210
1211 pub fn unique_frame_keys(&self, sel: &Select) -> Result<Vec<FrameKey>> {
1212 let keys = self.select(sel)?;
1213 let mut uniq = Vec::new();
1214 for k in keys {
1215 let h = self.frame_hash(k)?;
1216 if self.find_by_hash(h)? == Some(k) {
1217 uniq.push(k);
1218 }
1219 }
1220 Ok(uniq)
1221 }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::*;
1227 use std::path::PathBuf;
1228
1229 fn fixture(name: &str) -> PathBuf {
1230 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1231 .join("../readcon-core/resources/test")
1232 .join(name)
1233 }
1234
1235 fn fixtures_dir() -> PathBuf {
1236 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../readcon-core/resources/test")
1237 }
1238
1239 #[test]
1240 fn ingest_hash_dedup_and_select() {
1241 let dir = tempfile::tempdir().unwrap();
1242 let db = ConCorpus::open(dir.path()).unwrap();
1243 let n = db
1244 .append_trajectory_path(1, fixture("tiny_cuh2.con"))
1245 .unwrap();
1246 assert!(n >= 1);
1247 let k0 = FrameKey {
1248 traj_id: 1,
1249 frame_idx: 0,
1250 };
1251 let h = db.frame_hash(k0).unwrap();
1252 assert_eq!(db.find_by_hash(h).unwrap(), Some(k0));
1253
1254 let by_hash = db
1255 .select(&Select::new().exact_hash(h.to_bytes()))
1256 .unwrap();
1257 assert_eq!(by_hash, vec![k0]);
1258
1259 let cu = db
1260 .select(&Select::new().require_symbol("Cu").trajectory(1))
1261 .unwrap();
1262 assert!(!cu.is_empty());
1263
1264 let n2 = db
1265 .append_trajectory_path(2, fixture("tiny_multi_cuh2.con"))
1266 .unwrap();
1267 assert!(n2 >= 2);
1268 }
1269
1270 #[test]
1271 fn workflow_metatrain_extxyz_export() {
1272 let dir = tempfile::tempdir().unwrap();
1273 let db = ConCorpus::open(dir.path().join("corpus")).unwrap();
1274 let ingested = db.ingest_directory(fixtures_dir(), 1).unwrap();
1275 assert!(ingested.len() >= 3);
1276
1277 let keys = db
1278 .select(
1279 &Select::new()
1280 .require_symbol("Cu")
1281 .natoms_range(1, 10_000)
1282 .limit(50),
1283 )
1284 .unwrap();
1285 assert!(!keys.is_empty());
1286
1287 let uniq = db
1288 .unique_frame_keys(&Select::new().require_symbol("Cu"))
1289 .unwrap();
1290 assert!(!uniq.is_empty());
1291
1292 let xyz = dir.path().join("train_subset.xyz");
1293 let n = db.export_extxyz(&uniq, &xyz, "energy").unwrap();
1294 assert_eq!(n, uniq.len());
1295 let text = std::fs::read_to_string(&xyz).unwrap();
1296 assert!(text.contains("Lattice="));
1297 assert!(text.contains("Properties="));
1298 assert!(text.lines().any(|l| l.trim_start().starts_with("Cu ")));
1299 }
1300
1301 #[test]
1302 fn workflow_dedup_identical_ingest() {
1303 let dir = tempfile::tempdir().unwrap();
1304 let db = ConCorpus::open(dir.path()).unwrap();
1305 let f = fixture("tiny_cuh2.con");
1306 db.append_trajectory_path(1, &f).unwrap();
1307 db.append_trajectory_path(2, &f).unwrap();
1308 let k1 = FrameKey {
1309 traj_id: 1,
1310 frame_idx: 0,
1311 };
1312 let k2 = FrameKey {
1313 traj_id: 2,
1314 frame_idx: 0,
1315 };
1316 let h1 = db.frame_hash(k1).unwrap();
1317 let h2 = db.frame_hash(k2).unwrap();
1318 assert_eq!(h1, h2);
1319 assert_eq!(db.find_by_hash(h1).unwrap(), Some(k1));
1320 let uniq = db.unique_frame_keys(&Select::new()).unwrap();
1321 assert!(uniq.contains(&k1));
1322 assert!(!uniq.contains(&k2));
1323 }
1324
1325 #[test]
1326 fn metadata_indexes_forces_velocities_energy() {
1327 let dir = tempfile::tempdir().unwrap();
1328 let db = ConCorpus::open(dir.path()).unwrap();
1329 db.append_trajectory_path(1, fixture("tiny_cuh2.con"))
1330 .unwrap();
1331 db.append_trajectory_path(2, fixture("tiny_cuh2_forces.con"))
1332 .unwrap();
1333 db.append_trajectory_path(3, fixture("tiny_cuh2.convel"))
1334 .unwrap();
1335 db.append_trajectory_path(4, fixture("tiny_cuh2_vel_forces.con"))
1336 .unwrap();
1337
1338 let forces = db.select(&Select::new().require_forces()).unwrap();
1339 assert!(forces.iter().any(|k| k.traj_id == 2));
1340 assert!(forces.iter().any(|k| k.traj_id == 4));
1341 assert!(!forces.iter().any(|k| k.traj_id == 1));
1342
1343 let vels = db.select(&Select::new().require_velocities()).unwrap();
1344 assert!(vels.iter().any(|k| k.traj_id == 3));
1345 assert!(vels.iter().any(|k| k.traj_id == 4));
1346
1347 let both = db
1348 .select(&Select::new().require_forces().require_velocities())
1349 .unwrap();
1350 assert!(both.iter().any(|k| k.traj_id == 4));
1351 assert!(!both.iter().any(|k| k.traj_id == 2));
1352
1353 let with_e = db.select(&Select::new().require_energy()).unwrap();
1354 assert!(with_e.iter().any(|k| k.traj_id == 2));
1355 assert!(with_e.iter().any(|k| k.traj_id == 4));
1356 assert!(!with_e.iter().any(|k| k.traj_id == 1));
1357
1358 let in_range = db
1359 .select(&Select::new().energy_range(-43.0, -42.0).require_forces())
1360 .unwrap();
1361 assert!(in_range.iter().any(|k| k.traj_id == 2));
1362 let miss = db.select(&Select::new().energy_range(0.0, 1.0)).unwrap();
1363 assert!(miss.is_empty());
1364 }
1365
1366 #[test]
1367 fn composition_and_fmax_indexes() {
1368 let dir = tempfile::tempdir().unwrap();
1369 let db = ConCorpus::open(dir.path()).unwrap();
1370 db.append_trajectory_path(1, fixture("tiny_cuh2.con"))
1371 .unwrap();
1372 db.append_trajectory_path(2, fixture("tiny_cuh2_forces.con"))
1373 .unwrap();
1374 db.append_trajectory_path(3, fixture("sulfolene.con"))
1375 .unwrap();
1376
1377 let formula = db
1378 .frame_formula(FrameKey {
1379 traj_id: 1,
1380 frame_idx: 0,
1381 })
1382 .unwrap();
1383 assert_eq!(formula, "Cu:2|H:2");
1384
1385 let exact = db
1386 .select(&Select::new().exact_composition("Cu:2|H:2"))
1387 .unwrap();
1388 assert!(exact.iter().any(|k| k.traj_id == 1));
1389 assert!(exact.iter().any(|k| k.traj_id == 2));
1390 assert!(!exact.iter().any(|k| k.traj_id == 3));
1391
1392 let cu2 = db
1393 .select(&Select::new().element_exact("Cu", 2).element_exact("H", 2))
1394 .unwrap();
1395 assert_eq!(cu2.len(), exact.len());
1396
1397 let cu_min2 = db.select(&Select::new().element_min("Cu", 2)).unwrap();
1398 assert!(cu_min2.iter().any(|k| k.traj_id == 1));
1399
1400 let wrong = db
1401 .select(&Select::new().exact_composition("Fe:1"))
1402 .unwrap();
1403 assert!(wrong.is_empty());
1404
1405 let fr_f = db
1406 .get_frame(FrameKey {
1407 traj_id: 2,
1408 frame_idx: 0,
1409 })
1410 .unwrap();
1411 let expected_fmax = frame_fmax(&fr_f).expect("forces fixture has fmax");
1412 let in_fmax = db
1413 .select(
1414 &Select::new()
1415 .require_forces()
1416 .fmax_range(0.0, expected_fmax + 1e-6),
1417 )
1418 .unwrap();
1419 assert!(in_fmax.iter().any(|k| k.traj_id == 2));
1420 assert!(!in_fmax.iter().any(|k| k.traj_id == 1));
1421
1422 let too_small = db
1423 .select(&Select::new().fmax_range(0.0, expected_fmax * 0.0 - 1.0))
1424 .unwrap();
1425 let impossible = db
1427 .select(&Select::new().fmax_range(1e9, 1e9 + 1.0))
1428 .unwrap();
1429 assert!(impossible.is_empty());
1430 let _ = too_small;
1431 }
1432
1433 #[test]
1434 fn reindex_and_append_frames() {
1435 let dir = tempfile::tempdir().unwrap();
1436 let db = ConCorpus::open(dir.path()).unwrap();
1437 db.append_trajectory_path(1, fixture("tiny_cuh2.con"))
1438 .unwrap();
1439 db.append_trajectory_path(2, fixture("tiny_cuh2_forces.con"))
1440 .unwrap();
1441
1442 let before = db
1443 .select(&Select::new().exact_composition("Cu:2|H:2"))
1444 .unwrap();
1445 assert!(!before.is_empty());
1446
1447 let n = db.reindex().unwrap();
1448 assert!(n >= 2);
1449 let after = db
1450 .select(&Select::new().exact_composition("Cu:2|H:2"))
1451 .unwrap();
1452 assert_eq!(before, after);
1453
1454 let fr = db
1455 .get_frame(FrameKey {
1456 traj_id: 1,
1457 frame_idx: 0,
1458 })
1459 .unwrap();
1460 let n2 = db
1461 .append_trajectory_frames(10, &[fr.clone()], "mem")
1462 .unwrap();
1463 assert_eq!(n2, 1);
1464 let mem = db
1465 .select(
1466 &Select::new()
1467 .trajectory(10)
1468 .exact_composition("Cu:2|H:2"),
1469 )
1470 .unwrap();
1471 assert_eq!(mem.len(), 1);
1472
1473 let n3 = db.extend_trajectory_frames(10, &[fr], "mem-ext").unwrap();
1474 assert_eq!(n3, 2);
1475 }
1476
1477 #[test]
1478 fn reindex_twice_stable() {
1479 let dir = tempfile::tempdir().unwrap();
1480 let db = ConCorpus::open(dir.path()).unwrap();
1481 db.append_trajectory_path(1, fixture("tiny_cuh2_forces.con"))
1482 .unwrap();
1483 let a = db.reindex().unwrap();
1484 let keys1 = db.select(&Select::new().require_forces()).unwrap();
1485 let b = db.reindex().unwrap();
1486 let keys2 = db.select(&Select::new().require_forces()).unwrap();
1487 assert_eq!(a, b);
1488 assert_eq!(keys1, keys2);
1489 }
1490
1491 #[test]
1492 fn batch_touch_and_texts() {
1493 let dir = tempfile::tempdir().unwrap();
1494 let db = ConCorpus::open(dir.path()).unwrap();
1495 let n = db
1496 .append_trajectory_path(1, fixture("tiny_cuh2.con"))
1497 .unwrap();
1498 assert!(n >= 1);
1499 let (total, checksum) = db.touch_trajectory_blobs(1, n).unwrap();
1500 assert!(total > 0);
1501 assert!(checksum > 0);
1502 let texts = db
1503 .get_frame_texts(&[FrameKey {
1504 traj_id: 1,
1505 frame_idx: 0,
1506 }])
1507 .unwrap();
1508 assert_eq!(texts.len(), 1);
1509 assert_eq!(texts[0].len() as u64, total);
1510 let mut expect = 0u64;
1512 for (i, b) in texts[0].as_bytes().iter().enumerate() {
1513 expect = expect.wrapping_add((*b as u64).wrapping_mul((i as u64).wrapping_add(1)));
1514 }
1515 assert_eq!(checksum, expect);
1516 }
1517
1518 #[test]
1520 fn concurrent_writers_distinct_traj() {
1521 use std::sync::Arc;
1522 use std::thread;
1523 let dir = tempfile::tempdir().unwrap();
1524 let db = Arc::new(ConCorpus::open(dir.path()).unwrap());
1525 let text = std::fs::read_to_string(fixture("tiny_cuh2.con")).unwrap();
1526 let mut handles = Vec::new();
1527 for tid in 1u64..=8 {
1528 let db = Arc::clone(&db);
1529 let text = text.clone();
1530 handles.push(thread::spawn(move || {
1531 db.append_trajectory_str(tid, &text, format!("t{tid}")).unwrap()
1532 }));
1533 }
1534 let mut counts = Vec::new();
1535 for h in handles {
1536 counts.push(h.join().unwrap());
1537 }
1538 assert!(counts.iter().all(|&c| c >= 1));
1539 for tid in 1u64..=8 {
1540 let meta = db.traj_meta(tid).unwrap().expect("meta");
1541 assert_eq!(meta.n_frames, counts[(tid - 1) as usize]);
1542 let _ = db
1543 .get_frame_text(FrameKey {
1544 traj_id: tid,
1545 frame_idx: 0,
1546 })
1547 .unwrap();
1548 }
1549 let cu = db.select(&Select::new().require_symbol("Cu")).unwrap();
1550 assert_eq!(cu.len(), 8);
1551 }
1552
1553 #[test]
1555 fn multiproc_cli_concurrent_select() {
1556 use std::process::Command;
1557 let dir = tempfile::tempdir().unwrap();
1558 let corpus = dir.path().join("mp_corpus");
1559 {
1560 let db = ConCorpus::open(&corpus).unwrap();
1561 db.append_trajectory_path(1, fixture("tiny_cuh2.con"))
1562 .unwrap();
1563 }
1564 let Some(bin) = option_env!("CARGO_BIN_EXE_readcon-db") else {
1565 return;
1566 };
1567 let corpus_s = corpus.to_str().unwrap();
1568 let mut kids = Vec::new();
1569 for _ in 0..4 {
1570 kids.push(
1571 Command::new(bin)
1572 .args(["select", corpus_s, "--symbol", "Cu"])
1573 .stdout(std::process::Stdio::null())
1574 .spawn()
1575 .expect("spawn"),
1576 );
1577 }
1578 for mut c in kids {
1579 assert!(c.wait().unwrap().success());
1580 }
1581 }
1582
1583 #[test]
1584 fn ase_competitive_mass_volume_pbc_meta_charge() {
1585 use readcon_core::types::ConFrameBuilder;
1586 let dir = tempfile::tempdir().unwrap();
1587 let db = ConCorpus::open(dir.path()).unwrap();
1588
1589 db.append_trajectory_path(1, fixture("tiny_cuh2.con")).unwrap();
1591 let fr1 = db
1592 .get_frame(FrameKey {
1593 traj_id: 1,
1594 frame_idx: 0,
1595 })
1596 .unwrap();
1597 let mass1 = frame_total_mass(&fr1).expect("mass");
1598 let vol1 = frame_cell_volume(&fr1).expect("vol");
1599
1600 let mut fr2 = fr1.clone();
1602 fr2.header.set_frame_index(7);
1603 fr2.header.set_time(1.5);
1604 fr2.header.set_timestep(0.25);
1605 fr2.header.set_neb_bead(2);
1606 fr2.header.set_neb_band(1);
1607 fr2.header.set_pbc([true, true, false]);
1608 fr2.header
1609 .metadata
1610 .insert("charge".into(), serde_json::json!(-1.0));
1611 fr2.header
1612 .metadata
1613 .insert("magmom".into(), serde_json::json!(2.0));
1614 fr2.header.boxl = [20.0, 20.0, 20.0];
1616 db.append_trajectory_frames(2, &[fr2], "meta").unwrap();
1617
1618 let by_mass = db
1619 .select(&Select::new().mass_range(mass1 - 1e-6, mass1 + 1e-6))
1620 .unwrap();
1621 assert!(by_mass.iter().any(|k| k.traj_id == 1));
1622
1623 let by_vol_small = db
1624 .select(&Select::new().volume_range(vol1 - 1.0, vol1 + 1.0))
1625 .unwrap();
1626 assert!(by_vol_small.iter().any(|k| k.traj_id == 1));
1627 assert!(!by_vol_small.iter().any(|k| k.traj_id == 2));
1628
1629 let pbc_match = db
1630 .select(&Select::new().pbc([true, true, false]))
1631 .unwrap();
1632 assert_eq!(pbc_match.len(), 1);
1633 assert_eq!(pbc_match[0].traj_id, 2);
1634 let pbc_miss = db.select(&Select::new().pbc([true, true, true])).unwrap();
1635 assert!(pbc_miss.is_empty());
1636
1637 let fi = db
1638 .select(&Select::new().frame_index_range(7.0, 7.0))
1639 .unwrap();
1640 assert_eq!(fi, vec![FrameKey { traj_id: 2, frame_idx: 0 }]);
1641 let fi_miss = db
1642 .select(&Select::new().frame_index_range(99.0, 100.0))
1643 .unwrap();
1644 assert!(fi_miss.is_empty());
1645
1646 let bead = db.select(&Select::new().neb_bead_range(2.0, 2.0)).unwrap();
1647 assert_eq!(bead.len(), 1);
1648 let t = db.select(&Select::new().time_range(1.4, 1.6)).unwrap();
1649 assert_eq!(t.len(), 1);
1650
1651 let dt = db
1652 .select(&Select::new().timestep_range(0.25, 0.25))
1653 .unwrap();
1654 assert_eq!(
1655 dt,
1656 vec![FrameKey {
1657 traj_id: 2,
1658 frame_idx: 0
1659 }]
1660 );
1661 let dt_miss = db
1662 .select(&Select::new().timestep_range(9.0, 10.0))
1663 .unwrap();
1664 assert!(dt_miss.is_empty());
1665 assert!(!dt.iter().any(|k| k.traj_id == 1));
1667
1668 let band = db.select(&Select::new().neb_band_range(1.0, 1.0)).unwrap();
1669 assert_eq!(
1670 band,
1671 vec![FrameKey {
1672 traj_id: 2,
1673 frame_idx: 0
1674 }]
1675 );
1676 let band_miss = db
1677 .select(&Select::new().neb_band_range(99.0, 100.0))
1678 .unwrap();
1679 assert!(band_miss.is_empty());
1680
1681 let ch = db.select(&Select::new().charge_range(-1.0, -1.0)).unwrap();
1682 assert_eq!(ch.len(), 1);
1683 let mm = db.select(&Select::new().magmom_range(2.0, 2.0)).unwrap();
1684 assert_eq!(mm.len(), 1);
1685 let ch_miss = db.select(&Select::new().charge_range(0.0, 0.0)).unwrap();
1686 assert!(ch_miss.is_empty());
1687
1688 let n = db.reindex().unwrap();
1689 assert!(n >= 2);
1690 let after = db
1691 .select(&Select::new().frame_index_range(7.0, 7.0))
1692 .unwrap();
1693 assert_eq!(after.len(), 1);
1694 let after_dt = db
1695 .select(&Select::new().timestep_range(0.25, 0.25))
1696 .unwrap();
1697 assert_eq!(after_dt.len(), 1);
1698 let after_band = db.select(&Select::new().neb_band_range(1.0, 1.0)).unwrap();
1699 assert_eq!(after_band.len(), 1);
1700
1701 let form = db
1703 .select(&Select::new().exact_composition("Cu:2|H:2"))
1704 .unwrap();
1705 assert!(form.len() >= 2);
1706 }
1707
1708 #[test]
1709 fn cooked_soa_roundtrip_and_authority() {
1710 let dir = tempfile::tempdir().unwrap();
1711 let db = ConCorpus::open(dir.path()).unwrap();
1712 let key = FrameKey {
1713 traj_id: 1,
1714 frame_idx: 0,
1715 };
1716 db.append_trajectory_path(1, fixture("tiny_cuh2.con")).unwrap();
1717 assert!(db.get_cooked_soa_bytes(key).unwrap().is_none());
1719 let pos_via_parse = db.get_positions(key).unwrap();
1720 let fr = db.get_frame(key).unwrap();
1721 assert_eq!(pos_via_parse.len(), fr.atom_data.len());
1722 for (i, a) in fr.atom_data.iter().enumerate() {
1723 assert_eq!(pos_via_parse[i], [a.x, a.y, a.z]);
1724 }
1725
1726 let text_before = db.get_frame_text(key).unwrap();
1727 let h_before = db.frame_hash(key).unwrap();
1728 let nbytes = db.cook_frame(key).unwrap();
1729 assert!(nbytes > 0);
1730 let cooked = db.get_cooked_soa(key).unwrap().expect("cooked present");
1731 assert_eq!(cooked.natoms as usize, fr.atom_data.len());
1732 for (i, a) in fr.atom_data.iter().enumerate() {
1733 assert_eq!(cooked.positions[i], [a.x, a.y, a.z]);
1734 }
1735 assert_eq!(db.get_frame_text(key).unwrap(), text_before);
1737 assert_eq!(db.frame_hash(key).unwrap().to_bytes(), h_before.to_bytes());
1738 assert_eq!(
1739 db.find_by_hash(h_before).unwrap(),
1740 Some(key)
1741 );
1742 let pos_cooked = db.get_positions(key).unwrap();
1743 assert_eq!(pos_cooked, cooked.positions);
1744
1745 let keys = db.select(&Select::new().require_symbol("Cu")).unwrap();
1747 assert!(keys.contains(&key));
1748
1749 db.reindex().unwrap();
1751 assert!(db.get_cooked_soa(key).unwrap().is_some());
1752 assert!(db
1753 .select(&Select::new().require_symbol("Cu"))
1754 .unwrap()
1755 .contains(&key));
1756
1757 db.delete_cooked_soa(key).unwrap();
1759 assert!(db.get_cooked_soa_bytes(key).unwrap().is_none());
1760 let pos_again = db.get_positions(key).unwrap();
1761 assert_eq!(pos_again, pos_via_parse);
1762 assert_eq!(db.get_frame_text(key).unwrap(), text_before);
1763 }
1764
1765 #[test]
1766 fn cooked_soa_forces_and_cook_on_ingest() {
1767 let dir = tempfile::tempdir().unwrap();
1768 let db = ConCorpus::open(dir.path()).unwrap();
1769 db.append_trajectory_path_cook(3, fixture("tiny_cuh2_forces.con"), true)
1770 .unwrap();
1771 let key = FrameKey {
1772 traj_id: 3,
1773 frame_idx: 0,
1774 };
1775 assert!(db.get_cooked_soa_bytes(key).unwrap().is_some());
1776 let fr = db.get_frame(key).unwrap();
1777 let forces = db.get_forces(key).unwrap().expect("forces");
1778 for (i, a) in fr.atom_data.iter().enumerate() {
1779 if let Some(f) = a.force {
1780 assert_eq!(forces[i], f);
1781 }
1782 }
1783 let mut wtxn = db.env.write_txn().unwrap();
1785 let fk_b = key.to_bytes();
1786 db.frames_soa
1787 .put(&mut wtxn, &fk_b[..], b"XXXX_not_valid")
1788 .unwrap();
1789 wtxn.commit().unwrap();
1790 assert!(db.get_cooked_soa(key).unwrap().is_none());
1791 let pos = db.get_positions(key).unwrap();
1792 assert_eq!(pos.len(), fr.atom_data.len());
1793 assert_eq!(
1794 pos[0],
1795 [fr.atom_data[0].x, fr.atom_data[0].y, fr.atom_data[0].z]
1796 );
1797 }
1798
1799 #[test]
1800 fn cooked_soa_recook_all() {
1801 let dir = tempfile::tempdir().unwrap();
1802 let db = ConCorpus::open(dir.path()).unwrap();
1803 db.append_trajectory_path(1, fixture("tiny_cuh2.con")).unwrap();
1804 db.append_trajectory_path(2, fixture("tiny_cuh2_forces.con"))
1805 .unwrap();
1806 let n = db.recook_all().unwrap();
1807 assert_eq!(n, 2);
1808 assert!(db
1809 .get_cooked_soa(FrameKey {
1810 traj_id: 1,
1811 frame_idx: 0
1812 })
1813 .unwrap()
1814 .is_some());
1815 assert!(db
1816 .get_cooked_soa(FrameKey {
1817 traj_id: 2,
1818 frame_idx: 0
1819 })
1820 .unwrap()
1821 .is_some());
1822 }
1823
1824 #[test]
1826 fn cooked_soa_cannot_replace_missing_frames_text() {
1827 let dir = tempfile::tempdir().unwrap();
1828 let db = ConCorpus::open(dir.path()).unwrap();
1829 let key = FrameKey {
1830 traj_id: 9,
1831 frame_idx: 0,
1832 };
1833 db.append_trajectory_path_cook(9, fixture("tiny_cuh2.con"), true)
1834 .unwrap();
1835 assert!(db.has_valid_cooked_soa(key).unwrap());
1836 let pos_ok = db.get_positions(key).unwrap();
1837 assert!(!pos_ok.is_empty());
1838 {
1840 let mut wtxn = db.env.write_txn().unwrap();
1841 let fk_b = key.to_bytes();
1842 db.frames.delete(&mut wtxn, &fk_b[..]).unwrap();
1843 wtxn.commit().unwrap();
1844 }
1845 assert!(db.get_cooked_soa(key).unwrap().is_some());
1847 let pos_cooked_only = db.get_positions(key).unwrap();
1848 assert_eq!(pos_cooked_only, pos_ok);
1849 assert!(db.get_frame_text(key).is_err());
1851 assert!(db.get_frame(key).is_err());
1852 assert!(db.frame_formula(key).is_err());
1855 }
1856
1857 #[test]
1858 fn numeric_fast_path_matches_con_twice() {
1859 let dir = tempfile::tempdir().unwrap();
1860 let db = ConCorpus::open(dir.path()).unwrap();
1861 let key = FrameKey {
1862 traj_id: 1,
1863 frame_idx: 0,
1864 };
1865 for _ in 0..2 {
1866 }
1868 db.append_trajectory_path(1, fixture("tiny_cuh2_forces.con"))
1869 .unwrap();
1870 let fr = db.get_frame(key).unwrap();
1871 let expect_pos: Vec<_> = fr.atom_data.iter().map(|a| [a.x, a.y, a.z]).collect();
1872 let expect_f: Vec<_> = fr
1873 .atom_data
1874 .iter()
1875 .map(|a| a.force.unwrap_or([0.0; 3]))
1876 .collect();
1877 assert_eq!(db.get_positions(key).unwrap(), expect_pos);
1879 assert_eq!(db.get_forces(key).unwrap().unwrap(), expect_f);
1880 db.cook_frame(key).unwrap();
1881 assert!(db.has_valid_cooked_soa(key).unwrap());
1882 assert_eq!(db.get_positions(key).unwrap(), expect_pos);
1884 assert_eq!(db.get_forces(key).unwrap().unwrap(), expect_f);
1885 let text = db.get_frame_text(key).unwrap();
1886 let h = db.frame_hash(key).unwrap();
1887 db.delete_cooked_soa(key).unwrap();
1888 assert_eq!(db.get_frame_text(key).unwrap(), text);
1889 assert_eq!(db.frame_hash(key).unwrap().to_bytes(), h.to_bytes());
1890 assert_eq!(db.get_positions(key).unwrap(), expect_pos);
1891 }
1892}