1use std::path::{Path, PathBuf};
11
12use readcon_core::types::ConFrame;
13
14use crate::corpus::ConCorpus;
15use crate::error::{Error, Result};
16use crate::keys::{FrameKey, TrajId};
17use crate::select::Select;
18
19pub const DEFAULT_N_SHARDS: u32 = 64;
21
22const MANIFEST: &str = "shards.json";
24
25#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
26pub struct ShardManifest {
27 pub n_shards: u32,
28 pub version: u32,
29}
30
31pub struct ShardedConCorpus {
33 root: PathBuf,
34 n_shards: u32,
35 shards: Vec<Option<ConCorpus>>,
37}
38
39impl ShardedConCorpus {
40 pub fn open(root: impl AsRef<Path>, n_shards: u32) -> Result<Self> {
42 let root = root.as_ref().to_path_buf();
43 std::fs::create_dir_all(&root)?;
44 let manifest_path = root.join(MANIFEST);
45 let n_shards = if manifest_path.is_file() {
46 let s = std::fs::read_to_string(&manifest_path)?;
47 let m: ShardManifest = serde_json::from_str(&s)?;
48 m.n_shards
49 } else {
50 if n_shards == 0 {
51 return Err(Error::Message("n_shards must be >= 1".into()));
52 }
53 let m = ShardManifest {
54 n_shards,
55 version: 1,
56 };
57 std::fs::write(&manifest_path, serde_json::to_string_pretty(&m)?)?;
58 n_shards
59 };
60 let mut shards = Vec::with_capacity(n_shards as usize);
61 shards.resize_with(n_shards as usize, || None);
62 Ok(Self {
63 root,
64 n_shards,
65 shards,
66 })
67 }
68
69 pub fn n_shards(&self) -> u32 {
70 self.n_shards
71 }
72
73 pub fn root(&self) -> &Path {
74 &self.root
75 }
76
77 #[inline]
78 pub fn shard_for_traj(traj_id: TrajId, n_shards: u32) -> u32 {
79 (traj_id % u64::from(n_shards)) as u32
80 }
81
82 fn shard_path(&self, shard_id: u32) -> PathBuf {
83 self.root
84 .join(format!("shard_{shard_id:04}"))
85 }
86
87 pub fn shard_mut(&mut self, shard_id: u32) -> Result<&ConCorpus> {
89 if shard_id >= self.n_shards {
90 return Err(Error::Message(format!(
91 "shard_id {shard_id} >= n_shards {}",
92 self.n_shards
93 )));
94 }
95 let i = shard_id as usize;
96 if self.shards[i].is_none() {
97 let p = self.shard_path(shard_id);
98 self.shards[i] = Some(ConCorpus::open(p)?);
99 }
100 Ok(self.shards[i].as_ref().unwrap())
101 }
102
103 pub fn open_shard_for_traj(root: impl AsRef<Path>, traj_id: TrajId) -> Result<(u32, ConCorpus)> {
105 let root = root.as_ref();
106 let manifest_path = root.join(MANIFEST);
107 let n_shards = if manifest_path.is_file() {
108 let m: ShardManifest = serde_json::from_str(&std::fs::read_to_string(&manifest_path)?)?;
109 m.n_shards
110 } else {
111 DEFAULT_N_SHARDS
112 };
113 let sid = Self::shard_for_traj(traj_id, n_shards);
114 let corpus = ConCorpus::open(root.join(format!("shard_{sid:04}")))?;
115 Ok((sid, corpus))
116 }
117
118 pub fn open_shard(root: impl AsRef<Path>, shard_id: u32) -> Result<ConCorpus> {
120 let root = root.as_ref();
121 let manifest_path = root.join(MANIFEST);
122 let n_shards = if manifest_path.is_file() {
123 let m: ShardManifest = serde_json::from_str(&std::fs::read_to_string(&manifest_path)?)?;
124 m.n_shards
125 } else {
126 let _ = Self::open(root, DEFAULT_N_SHARDS)?;
128 DEFAULT_N_SHARDS
129 };
130 if shard_id >= n_shards {
131 return Err(Error::Message(format!(
132 "shard_id {shard_id} >= n_shards {n_shards}"
133 )));
134 }
135 ConCorpus::open(root.join(format!("shard_{shard_id:04}")))
136 }
137
138 pub fn append_trajectory_path(
139 &mut self,
140 traj_id: TrajId,
141 file: impl AsRef<Path>,
142 ) -> Result<u32> {
143 let sid = Self::shard_for_traj(traj_id, self.n_shards);
144 let c = self.shard_mut(sid)?;
145 c.append_trajectory_path(traj_id, file)
146 }
147
148 pub fn append_trajectory_str(
149 &mut self,
150 traj_id: TrajId,
151 contents: &str,
152 source: impl Into<String>,
153 ) -> Result<u32> {
154 let sid = Self::shard_for_traj(traj_id, self.n_shards);
155 let c = self.shard_mut(sid)?;
156 c.append_trajectory_str(traj_id, contents, source)
157 }
158
159 pub fn append_trajectory_frames(
160 &mut self,
161 traj_id: TrajId,
162 frames: &[ConFrame],
163 source: impl Into<String>,
164 ) -> Result<u32> {
165 let sid = Self::shard_for_traj(traj_id, self.n_shards);
166 let c = self.shard_mut(sid)?;
167 c.append_trajectory_frames(traj_id, frames, source)
168 }
169
170 pub fn select(&mut self, sel: &Select) -> Result<Vec<FrameKey>> {
172 let mut out = Vec::new();
173 for sid in 0..self.n_shards {
174 let c = self.shard_mut(sid)?;
175 out.extend(c.select(sel)?);
176 }
177 out.sort();
178 if let Some(lim) = sel.limit {
179 out.truncate(lim);
180 }
181 Ok(out)
182 }
183
184 pub fn get_frame_text(&mut self, key: FrameKey) -> Result<String> {
185 let sid = Self::shard_for_traj(key.traj_id, self.n_shards);
186 self.shard_mut(sid)?.get_frame_text(key)
187 }
188
189 pub fn reindex_all(&mut self) -> Result<u32> {
190 let mut n = 0u32;
191 for sid in 0..self.n_shards {
192 if self.shard_path(sid).is_dir() {
193 n += self.shard_mut(sid)?.reindex()?;
194 }
195 }
196 Ok(n)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use std::sync::Arc;
204 use std::thread;
205
206 fn fixture(name: &str) -> PathBuf {
207 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
208 .join("../readcon-core/resources/test")
209 .join(name)
210 }
211
212 #[test]
213 fn parallel_writers_different_shards() {
214 let dir = tempfile::tempdir().unwrap();
215 let root = dir.path().join("hpc");
216 let n_shards = 8u32;
218 ShardedConCorpus::open(&root, n_shards).unwrap();
219 let text = std::fs::read_to_string(fixture("tiny_cuh2.con")).unwrap();
220 let root = Arc::new(root);
221 let mut joins = Vec::new();
222 for sid in 0..n_shards {
223 let root = Arc::clone(&root);
224 let text = text.clone();
225 joins.push(thread::spawn(move || {
226 let db = ShardedConCorpus::open_shard(root.as_path(), sid).unwrap();
228 let traj = u64::from(sid); db.append_trajectory_str(traj, &text, format!("shard{sid}"))
230 .unwrap()
231 }));
232 }
233 let mut ns = Vec::new();
234 for j in joins {
235 ns.push(j.join().unwrap());
236 }
237 assert!(ns.iter().all(|&n| n >= 1));
238 let mut fan = ShardedConCorpus::open(root.as_path(), n_shards).unwrap();
239 let keys = fan.select(&Select::new().require_symbol("Cu")).unwrap();
240 assert_eq!(keys.len(), 8);
241 }
242
243 #[test]
244 fn traj_routing_stable() {
245 assert_eq!(ShardedConCorpus::shard_for_traj(0, 64), 0);
246 assert_eq!(ShardedConCorpus::shard_for_traj(65, 64), 1);
247 }
248}
249
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
252pub enum CorpusExportKind {
253 ShardedLmdb,
255 SingleEnvLmdb,
257 ExtXyz,
259}
260
261impl CorpusExportKind {
262 pub fn as_str(self) -> &'static str {
263 match self {
264 Self::ShardedLmdb => "sharded-lmdb",
265 Self::SingleEnvLmdb => "single-env-lmdb",
266 Self::ExtXyz => "extxyz",
267 }
268 }
269}
270
271impl ShardedConCorpus {
272 pub fn join_to_single_env(&mut self, dst: impl AsRef<Path>) -> Result<u32> {
277 let dst = dst.as_ref();
278 if dst.exists() {
279 std::fs::remove_dir_all(dst).ok();
280 }
281 let out = ConCorpus::open(dst)?;
282 let mut n = 0u32;
283 let mut seen_traj = std::collections::BTreeSet::new();
284 for sid in 0..self.n_shards {
285 if !self.shard_path(sid).is_dir() {
286 continue;
287 }
288 let shard = self.shard_mut(sid)?;
289 for fk in shard.list_frame_keys()? {
290 if fk.frame_idx == 0 {
291 if !seen_traj.insert(fk.traj_id) {
292 return Err(Error::Message(format!(
293 "traj_id {} appears in multiple shards; cannot join without remap",
294 fk.traj_id
295 )));
296 }
297 }
298 }
299 let keys = shard.list_frame_keys()?;
301 let mut by_traj: std::collections::BTreeMap<u64, Vec<FrameKey>> =
302 std::collections::BTreeMap::new();
303 for fk in keys {
304 by_traj.entry(fk.traj_id).or_default().push(fk);
305 }
306 for (tid, mut fks) in by_traj {
307 fks.sort();
308 let mut concat = String::new();
309 for fk in &fks {
310 concat.push_str(&shard.get_frame_text(*fk)?);
311 }
312 let nf = out.append_trajectory_str(tid, &concat, format!("join-from-shard-{sid}"))?;
313 n += nf;
314 }
315 }
316 Ok(n)
317 }
318
319 pub fn split_single_to_sharded(
322 single: &ConCorpus,
323 dst_root: impl AsRef<Path>,
324 n_shards: u32,
325 ) -> Result<u32> {
326 if n_shards == 0 {
327 return Err(Error::Message("n_shards must be >= 1".into()));
328 }
329 let dst_root = dst_root.as_ref();
330 if dst_root.exists() {
331 std::fs::remove_dir_all(dst_root).ok();
332 }
333 let mut sharded = ShardedConCorpus::open(dst_root, n_shards)?;
334 let keys = single.list_frame_keys()?;
335 let mut by_traj: std::collections::BTreeMap<u64, Vec<FrameKey>> =
336 std::collections::BTreeMap::new();
337 for fk in keys {
338 by_traj.entry(fk.traj_id).or_default().push(fk);
339 }
340 let mut n = 0u32;
341 for (tid, mut fks) in by_traj {
342 fks.sort();
343 let mut concat = String::new();
344 for fk in &fks {
345 concat.push_str(&single.get_frame_text(*fk)?);
346 }
347 let nf = sharded.append_trajectory_str(tid, &concat, "split-from-single")?;
348 n += nf;
349 }
350 Ok(n)
351 }
352}
353
354pub fn join_corpus_dirs(sources: &[PathBuf], dst: impl AsRef<Path>) -> Result<u32> {
356 let dst = dst.as_ref();
357 if dst.exists() {
358 std::fs::remove_dir_all(dst).ok();
359 }
360 let out = ConCorpus::open(dst)?;
361 let mut n = 0u32;
362 let mut seen = std::collections::BTreeSet::new();
363 for src in sources {
364 let c = ConCorpus::open(src)?;
365 let keys = c.list_frame_keys()?;
366 let mut by_traj: std::collections::BTreeMap<u64, Vec<FrameKey>> =
367 std::collections::BTreeMap::new();
368 for fk in keys {
369 by_traj.entry(fk.traj_id).or_default().push(fk);
370 }
371 for (tid, mut fks) in by_traj {
372 if !seen.insert(tid) {
373 return Err(Error::Message(format!(
374 "duplicate traj_id {tid} across join sources"
375 )));
376 }
377 fks.sort();
378 let mut concat = String::new();
379 for fk in &fks {
380 concat.push_str(&c.get_frame_text(*fk)?);
381 }
382 n += out.append_trajectory_str(tid, &concat, src.display().to_string())?;
383 }
384 }
385 Ok(n)
386}
387
388#[cfg(test)]
389mod compaction_tests {
390 use super::*;
391 use crate::select::Select;
392
393 fn fixture(name: &str) -> PathBuf {
394 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
395 .join("../readcon-core/resources/test")
396 .join(name)
397 }
398
399 #[test]
400 fn join_split_reversible_membership() {
401 let dir = tempfile::tempdir().unwrap();
402 let sharded_root = dir.path().join("sharded");
403 let con_text = std::fs::read_to_string(fixture("tiny_cuh2.con")).unwrap();
404 {
405 let mut s = ShardedConCorpus::open(&sharded_root, 4).unwrap();
406 for tid in [0u64, 1, 2, 3] {
407 s.append_trajectory_str(tid, &con_text, "t").unwrap();
408 }
409 }
410 let mut s = ShardedConCorpus::open(&sharded_root, 4).unwrap();
411 let before = s.select(&Select::new()).unwrap();
412 assert_eq!(before.len(), 4);
413
414 let joined = dir.path().join("joined");
415 let n = s.join_to_single_env(&joined).unwrap();
416 assert_eq!(n, 4);
417 let joined_c = ConCorpus::open(&joined).unwrap();
418 let mid = joined_c.select(&Select::new()).unwrap();
419 assert_eq!(mid.len(), 4);
420
421 let split_root = dir.path().join("split_again");
422 let n2 = ShardedConCorpus::split_single_to_sharded(&joined_c, &split_root, 4).unwrap();
423 assert_eq!(n2, 4);
424 let mut s2 = ShardedConCorpus::open(&split_root, 4).unwrap();
425 let after = s2.select(&Select::new()).unwrap();
426 assert_eq!(after.len(), before.len());
427 let mut bt: Vec<_> = before.iter().map(|k| k.traj_id).collect();
429 let mut at: Vec<_> = after.iter().map(|k| k.traj_id).collect();
430 bt.sort();
431 at.sort();
432 assert_eq!(bt, at);
433 }
434
435 #[test]
436 fn export_kinds_documented() {
437 assert_eq!(CorpusExportKind::ShardedLmdb.as_str(), "sharded-lmdb");
438 assert_eq!(CorpusExportKind::SingleEnvLmdb.as_str(), "single-env-lmdb");
439 assert_eq!(CorpusExportKind::ExtXyz.as_str(), "extxyz");
440 }
441}