1#[cfg(feature = "python")]
7use pyo3::exceptions::PyValueError;
8#[cfg(feature = "python")]
9use pyo3::prelude::*;
10
11mod buf_gen;
12mod canary;
13mod column_gen;
14pub mod context;
15pub mod cpu_affinity;
16pub mod difficulty;
17pub mod disk;
18mod entity_gen;
19mod fast_template;
20mod fk_remap;
21mod graph_gen;
22pub mod gt;
23mod hn_common;
24mod noise;
25pub mod pipeline;
26mod pool_lookup;
27pub mod rng;
28pub mod schema;
29
30pub use context::Context;
31pub use difficulty::DifficultyReport;
32pub use pipeline::{
33 PipelineConfig, PipelineOutput, PipelineStats, run_pipeline, run_pipeline_with_progress,
34};
35pub use schema::{
36 DomainSchema, EntitySchema, HnSchema, build_pipeline_config, chrono_now, load_schema,
37};
38
39#[cfg(feature = "python")]
43#[pyclass]
44#[derive(Clone)]
45pub struct GenerateResult {
46 #[pyo3(get)]
47 pub dataset: String,
48 #[pyo3(get)]
49 pub ground_truth: String,
50 #[pyo3(get)]
51 pub total_records: usize,
52 #[pyo3(get)]
53 pub exact_dups: usize,
54 #[pyo3(get)]
55 pub fuzzy_dups: usize,
56 #[pyo3(get)]
57 pub hard_negs: usize,
58 #[pyo3(get)]
59 pub uniques: usize,
60 #[pyo3(get)]
61 pub masters: usize,
62 #[pyo3(get)]
63 pub nodes: Option<String>,
64 #[pyo3(get)]
65 pub edges: Option<String>,
66}
67
68#[cfg(feature = "python")]
69#[pymethods]
70impl GenerateResult {
71 fn __repr__(&self) -> String {
72 format!(
73 "GenerateResult(dataset={:?}, ground_truth={:?}, total_records={}, exact_dups={}, fuzzy_dups={}, hard_negs={}, uniques={}, masters={})",
74 self.dataset,
75 self.ground_truth,
76 self.total_records,
77 self.exact_dups,
78 self.fuzzy_dups,
79 self.hard_negs,
80 self.uniques,
81 self.masters,
82 )
83 }
84
85 fn __str__(&self) -> String {
86 self.__repr__()
87 }
88}
89
90#[cfg(feature = "python")]
97#[pyfunction]
98fn default_singleton_master_fraction(difficulty: &str) -> f64 {
99 schema::default_singleton_master_fraction(difficulty)
100}
101
102#[cfg(feature = "python")]
103#[pyfunction]
104fn estimate_difficulty(
105 domain: &str,
106 size: usize,
107 seed: u64,
108 difficulty: &str,
109 schemas_dir: &str,
110 hard_neg_ratio: f64,
111) -> PyResult<String> {
112 let schema =
113 load_schema(domain, std::path::Path::new(schemas_dir)).map_err(PyValueError::new_err)?;
114 let report = crate::difficulty::estimate_difficulty(
115 domain,
116 size,
117 seed,
118 difficulty,
119 hard_neg_ratio,
120 &schema,
121 )
122 .map_err(PyValueError::new_err)?;
123 serde_json::to_string(&report).map_err(|e| PyValueError::new_err(e.to_string()))
124}
125
126#[cfg(feature = "python")]
127#[pyfunction]
128#[pyo3(signature = (
129 domain, size, seed, difficulty, output_dir, locale, pools_dir, schemas_dir,
130 output_format, hard_neg_ratio, singleton_master_fraction, generate_graph,
131 graph_format, only_entity=None, chunk_size=None,
132))]
133#[allow(clippy::too_many_arguments)]
134fn generate(
135 domain: &str,
136 size: usize,
137 seed: u64,
138 difficulty: &str,
139 output_dir: &str,
140 locale: &str,
141 pools_dir: &str,
142 schemas_dir: &str,
143 output_format: &str,
144 hard_neg_ratio: f64,
145 singleton_master_fraction: f64,
146 generate_graph: bool,
147 graph_format: &str,
148 only_entity: Option<&str>,
149 chunk_size: Option<usize>,
150) -> PyResult<GenerateResult> {
151 let schema =
152 load_schema(domain, std::path::Path::new(schemas_dir)).map_err(PyValueError::new_err)?;
153
154 if output_format != "ipc" && output_format != "parquet" {
155 return Err(PyValueError::new_err(format!(
156 "invalid output format '{output_format}'; expected 'ipc' or 'parquet'"
157 )));
158 }
159 if !crate::context::LOCALES.contains(&locale) {
162 return Err(PyValueError::new_err(format!(
163 "invalid locale '{locale}'; expected one of {:?}",
164 crate::context::LOCALES
165 )));
166 }
167 if !schema::valid_difficulty_names().contains(&difficulty) {
168 return Err(PyValueError::new_err(format!(
169 "invalid difficulty '{difficulty}'; expected one of {:?}",
170 schema::valid_difficulty_names()
171 )));
172 }
173
174 let mut ctx = Context::new(domain, locale, pools_dir).map_err(PyValueError::new_err)?;
175
176 let run_id = schema::deterministic_run_id(
177 domain,
178 size,
179 seed,
180 difficulty,
181 hard_neg_ratio,
182 singleton_master_fraction,
183 locale,
184 only_entity,
185 chunk_size,
186 );
187
188 let config = build_pipeline_config(
189 domain,
190 size,
191 seed,
192 difficulty,
193 hard_neg_ratio,
194 singleton_master_fraction,
195 &schema,
196 &run_id,
197 output_format,
198 generate_graph,
199 graph_format,
200 only_entity,
201 )
202 .map_err(PyValueError::new_err)?;
203 crate::pipeline::source_date_epoch().map_err(PyValueError::new_err)?;
204 crate::pipeline::check_capacity(&config).map_err(PyValueError::new_err)?;
205 let chunked = matches!(chunk_size, Some(cs) if cs > 0 && cs < size);
206 let needed = crate::pipeline::estimate_output_bytes(&config, chunked);
207 if let Some(free) = crate::disk::available_space(std::path::Path::new(output_dir))
208 && needed > free
209 {
210 return Err(PyValueError::new_err(format!(
211 "this run needs ~{:.1} GB of disk space, but only {:.1} GB is free on the disk \
212 holding {output_dir} — free some space, choose another output_dir, use a \
213 smaller size, or output_format=\"parquet\" (6-12x smaller)",
214 crate::disk::gb(needed),
215 crate::disk::gb(free)
216 )));
217 }
218
219 let output = match chunk_size {
220 Some(cs) if cs > 0 && cs < size => {
221 ctx.enable_watermark(domain, size, seed);
222 crate::pipeline::run_chunked(
223 &ctx,
224 domain,
225 size,
226 cs,
227 seed,
228 difficulty,
229 hard_neg_ratio,
230 singleton_master_fraction,
231 &schema,
232 &run_id,
233 output_format,
234 generate_graph,
235 graph_format,
236 only_entity,
237 output_dir,
238 None,
239 false, )
241 .map_err(PyValueError::new_err)?
242 }
243 _ => {
244 ctx.enable_watermark(&config.domain, config.size, config.seed);
245 run_pipeline(&ctx, &config, output_dir).map_err(PyValueError::new_err)?
246 }
247 };
248
249 Ok(GenerateResult {
250 dataset: output.output_files.into_iter().next().unwrap_or_default(),
251 ground_truth: output.gt_file,
252 total_records: output.stats.total_records,
253 exact_dups: output.stats.exact_dups,
254 fuzzy_dups: output.stats.fuzzy_dups,
255 hard_negs: output.stats.hard_negs,
256 uniques: output.stats.uniques,
257 masters: output.stats.masters,
258 nodes: output.nodes,
259 edges: output.edges,
260 })
261}
262
263#[cfg(feature = "python")]
264#[pymodule]
265fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
266 m.add_function(wrap_pyfunction!(generate, m)?)?;
267 m.add_function(wrap_pyfunction!(estimate_difficulty, m)?)?;
268 m.add_function(wrap_pyfunction!(default_singleton_master_fraction, m)?)?;
269 m.add_class::<GenerateResult>()?;
270 Ok(())
271}