1use std::collections::HashMap;
7use std::path::Path;
8
9use crate::pipeline::PipelineConfig;
10
11#[derive(serde::Deserialize)]
12pub struct DomainSchema {
13 pub entities: Vec<EntitySchema>,
14 pub hn_types: Vec<HnSchema>,
15}
16
17fn default_entity_weight() -> f64 {
18 1.0
19}
20
21fn infer_identifier_col(entity: &EntitySchema) -> Option<String> {
24 let entity_id_name = format!("{}_id", entity.name);
25 let col_names: Vec<&str> = entity
26 .columns
27 .iter()
28 .filter_map(|c| c.get("name").and_then(|v| v.as_str()))
29 .collect();
30 if col_names.contains(&entity_id_name.as_str()) {
31 Some(entity_id_name)
32 } else if col_names.contains(&"id") {
33 Some("id".to_string())
34 } else {
35 col_names
36 .iter()
37 .find(|n| n.ends_with("_id"))
38 .map(|n| (*n).to_string())
39 }
40}
41
42#[derive(serde::Deserialize)]
43pub struct EntitySchema {
44 pub name: String,
45 pub columns: Vec<serde_json::Value>,
46 #[serde(default)]
47 pub fk_remaps: Vec<serde_json::Value>,
48 #[serde(default = "default_entity_weight")]
59 pub weight: f64,
60}
61
62#[derive(serde::Deserialize)]
63pub struct HnSchema {
64 pub entity_type: String,
65 pub config_json: String,
66}
67
68#[derive(Debug, Clone)]
69struct DifficultySettings {
70 singleton: f64,
71 doublet: f64,
72 noise_types: &'static [&'static str],
84 passes: usize,
108}
109
110const DIFFICULTY_MAP: [(&str, DifficultySettings); 3] = [
111 (
112 "light",
113 DifficultySettings {
114 singleton: 0.50,
115 doublet: 0.30,
116 noise_types: &["names", "dates"],
117 passes: 1,
118 },
119 ),
120 (
121 "medium",
122 DifficultySettings {
123 singleton: 0.30,
124 doublet: 0.40,
125 noise_types: &["typo", "names", "dates", "identifiers"],
126 passes: 3,
138 },
139 ),
140 (
141 "hell",
142 DifficultySettings {
143 singleton: 0.10,
144 doublet: 0.20,
145 noise_types: &[
151 "typo_aggressive",
152 "visual",
153 "unicode_pollution",
154 "names",
155 "dates",
156 "identifiers",
157 "addresses",
158 "companies",
159 "extra",
160 ],
161 passes: 8,
173 },
174 ),
175];
176
177pub fn default_singleton_master_fraction(difficulty: &str) -> f64 {
178 difficulty_settings(difficulty).singleton
179}
180
181fn difficulty_settings(difficulty: &str) -> DifficultySettings {
182 DIFFICULTY_MAP
183 .iter()
184 .find(|(name, _)| *name == difficulty)
185 .map(|(_, s)| s.clone())
186 .unwrap_or_else(|| DIFFICULTY_MAP[1].1.clone())
187}
188
189pub fn valid_difficulty_names() -> Vec<&'static str> {
196 DIFFICULTY_MAP.iter().map(|(name, _)| *name).collect()
197}
198
199pub fn chrono_now() -> String {
201 let start = std::time::SystemTime::now()
202 .duration_since(std::time::UNIX_EPOCH)
203 .unwrap_or_default();
204 format!("{:x}", start.as_secs())
205}
206
207#[allow(clippy::too_many_arguments)]
218pub fn deterministic_run_id(
219 domain: &str,
220 size: usize,
221 seed: u64,
222 difficulty: &str,
223 hard_neg_ratio: f64,
224 singleton_master_fraction: f64,
225 locale: &str,
226 only_entity: Option<&str>,
227 chunk_size: Option<usize>,
228) -> String {
229 use std::hash::{Hash, Hasher};
230 let mut hasher = std::collections::hash_map::DefaultHasher::new();
231 domain.hash(&mut hasher);
232 size.hash(&mut hasher);
233 seed.hash(&mut hasher);
234 difficulty.hash(&mut hasher);
235 hard_neg_ratio.to_bits().hash(&mut hasher);
236 singleton_master_fraction.to_bits().hash(&mut hasher);
237 locale.hash(&mut hasher);
238 only_entity.unwrap_or("").hash(&mut hasher);
242 chunk_size.unwrap_or(0).hash(&mut hasher);
250 format!("{}_{:x}", domain, hasher.finish())
251}
252
253pub fn load_schema(domain: &str, schemas_dir: &Path) -> Result<DomainSchema, String> {
258 let path = schemas_dir.join(format!("{domain}.json"));
259 let available = available_domain_names(schemas_dir);
265 if !available.iter().any(|d| d == domain) {
266 let hint = if available.is_empty() {
267 "no schemas found".to_string()
268 } else {
269 available.join(", ")
270 };
271 return Err(format!(
272 "schema file not found for domain '{domain}' at {path:?}. \
273 Available domains ({hint})"
274 ));
275 }
276 let data =
277 std::fs::read_to_string(&path).map_err(|e| format!("cannot read schema {path:?}: {e}"))?;
278 serde_json::from_str(&data).map_err(|e| format!("cannot parse schema {domain}.json: {e}"))
279}
280
281fn available_domain_names(dir: &Path) -> Vec<String> {
283 match std::fs::read_dir(dir) {
284 Ok(entries) => entries
285 .filter_map(|e| e.ok())
286 .filter(|e| {
287 e.path()
288 .extension()
289 .map(|ext| ext == "json")
290 .unwrap_or(false)
291 })
292 .filter_map(|e| {
293 e.path()
294 .file_stem()
295 .map(|s| s.to_string_lossy().to_string())
296 })
297 .collect(),
298 Err(_) => Vec::new(),
299 }
300}
301
302#[allow(clippy::too_many_arguments)]
307pub fn build_pipeline_config(
308 domain: &str,
309 size: usize,
310 seed: u64,
311 difficulty: &str,
312 hard_neg_ratio: f64,
313 singleton_master_fraction: f64,
314 schema: &DomainSchema,
315 run_id: &str,
316 output_format: &str,
317 graph_enabled: bool,
318 graph_format: &str,
319 only_entity: Option<&str>,
320) -> Result<PipelineConfig, String> {
321 if size < 10 {
322 return Err(format!("size must be >= 10, got {size}"));
323 }
324 if schema.entities.is_empty() {
325 return Err(format!("schema for domain '{domain}' has no entities"));
326 }
327 if !(0.0..=1.0).contains(&singleton_master_fraction) {
328 return Err(format!(
329 "singleton_master_fraction must be in [0.0, 1.0], got {singleton_master_fraction}"
330 ));
331 }
332 if let Some(target) = only_entity
333 && !schema.entities.iter().any(|e| e.name == target)
334 {
335 let available: Vec<&str> = schema.entities.iter().map(|e| e.name.as_str()).collect();
336 return Err(format!(
337 "only_entity '{target}' not found in domain '{domain}'; available entities: {}",
338 available.join(", ")
339 ));
340 }
341 let ds = difficulty_settings(difficulty);
342 let total = size;
343
344 let n_singleton = (total as f64 * singleton_master_fraction) as usize;
345 let n_doublet_float = total as f64 * ds.doublet;
346 let mut n_doublet = n_doublet_float as usize;
347 if !n_doublet.is_multiple_of(2) {
348 n_doublet -= 1;
349 }
350 if n_singleton + n_doublet > total {
351 return Err(format!(
352 "singleton_master_fraction {singleton_master_fraction} leaves no room for this \
353 difficulty's doublet share ({:.2}); reduce singleton_master_fraction",
354 ds.doublet
355 ));
356 }
357 let mut n_triplet = total - n_singleton - n_doublet;
358 let r = n_triplet % 3;
359 if r != 0 {
360 n_triplet -= r;
361 }
362 let total_unique = n_singleton + n_doublet / 2 + n_triplet / 3;
363 let n_duplicates = total.max(total_unique) - total_unique;
364
365 let active_entities: Vec<&EntitySchema> = match only_entity {
372 Some(target) => schema
373 .entities
374 .iter()
375 .filter(|e| e.name == target)
376 .collect(),
377 None => schema.entities.iter().collect(),
378 };
379
380 let total_ratio: f64 = active_entities.iter().map(|e| e.weight).sum::<f64>();
381 let raw_floats: Vec<(&str, f64)> = active_entities
382 .iter()
383 .map(|e| {
384 (
385 e.name.as_str(),
386 total_unique as f64 * e.weight / total_ratio,
387 )
388 })
389 .collect();
390
391 let n_entities = active_entities.len();
405 let mut floor_map: HashMap<&str, usize> = HashMap::new();
406 if total_unique >= 2 * n_entities {
407 for (name, r) in &raw_floats {
408 floor_map.insert(name, r.max(2.0) as usize);
409 }
410 } else {
411 for (name, r) in &raw_floats {
412 floor_map.insert(name, r.floor() as usize);
413 }
414 }
415 let floor_sum: usize = floor_map.values().sum();
416 let need = total_unique.max(floor_sum) - floor_sum;
417 if need > 0 {
418 let mut remainders: Vec<(&str, f64)> = raw_floats
419 .iter()
420 .map(|(n, r)| (*n, r - r.floor()))
421 .collect();
422 remainders.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
423 for (name, _) in remainders.iter().take(need) {
424 *floor_map.get_mut(name).unwrap_or(&mut 0) += 1;
425 }
426 }
427
428 let dup_ratios: Vec<(&str, f64)> = active_entities
429 .iter()
430 .map(|e| {
431 (
432 e.name.as_str(),
433 *floor_map.get(e.name.as_str()).unwrap_or(&2) as f64 / total_unique as f64,
434 )
435 })
436 .collect();
437 let mut dup_floor: HashMap<&str, usize> = HashMap::new();
438 for (name, r) in &dup_ratios {
439 dup_floor.insert(name, (n_duplicates as f64 * r) as usize);
440 }
441 let dup_sum: usize = dup_floor.values().sum();
442 let dup_need = n_duplicates.max(dup_sum) - dup_sum;
443 if dup_need > 0 {
444 let mut remainders: Vec<(&str, f64)> = dup_ratios
445 .iter()
446 .map(|(n, r)| (*n, r - r.floor()))
447 .collect();
448 remainders.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
449 for (name, _) in remainders.iter().take(dup_need) {
450 *dup_floor.get_mut(name).unwrap_or(&mut 0) += 1;
451 }
452 }
453
454 let noise_count = ds.noise_types.len();
455 let noise_weights: Vec<f64> = vec![1.0 / noise_count as f64; noise_count];
456
457 let mut entity_plans = Vec::new();
458 for entity in &active_entities {
459 let n_base = *floor_map.get(entity.name.as_str()).unwrap_or(&2);
460 let n_dup = *dup_floor.get(entity.name.as_str()).unwrap_or(&0);
461
462 let mut noise_entries = Vec::new();
463 if n_dup > 0 {
464 let mut counts: Vec<usize> = noise_weights
465 .iter()
466 .map(|w| (w * n_dup as f64) as usize)
467 .collect();
468 let count_sum: usize = counts.iter().sum();
469 if count_sum < n_dup {
470 *counts.last_mut().unwrap_or(&mut 0) += n_dup - count_sum;
471 }
472 for (i, noise_type) in ds.noise_types.iter().enumerate() {
473 if counts[i] == 0 {
474 continue;
475 }
476 noise_entries.push(serde_json::json!({
477 "noise_type": noise_type,
478 "columns": [],
479 "count": counts[i],
480 }));
481 }
482 }
483
484 let identifier_col = infer_identifier_col(entity);
485
486 let columns_json = serde_json::to_string(&entity.columns)
487 .map_err(|e| format!("serialize columns: {e}"))?;
488
489 entity_plans.push(serde_json::json!({
490 "name": entity.name,
491 "n_base": n_base,
492 "n_dup": n_dup,
493 "identifier_col": identifier_col,
494 "columns_json": columns_json,
495 "noise_types": noise_entries,
496 "fk_remaps": entity.fk_remaps,
497 }));
498 }
499
500 if let Some(target) = only_entity {
507 let target_schema = schema
508 .entities
509 .iter()
510 .find(|e| e.name == target)
511 .expect("only_entity validated to exist above");
512 let mut pool_target_names: Vec<&str> = target_schema
513 .fk_remaps
514 .iter()
515 .filter_map(|r| r.get("target_entity").and_then(|v| v.as_str()))
516 .filter(|&t| t != target)
517 .collect();
518 pool_target_names.sort_unstable();
519 pool_target_names.dedup();
520
521 let pool_n = crate::pipeline::FK_POOL_CAP.min(total_unique.max(2));
522 for name in pool_target_names {
523 let Some(entity) = schema.entities.iter().find(|e| e.name == name) else {
524 continue;
525 };
526 let identifier_col = infer_identifier_col(entity);
527 let columns_json = serde_json::to_string(&entity.columns)
528 .map_err(|e| format!("serialize columns: {e}"))?;
529 entity_plans.push(serde_json::json!({
530 "name": entity.name,
531 "n_base": pool_n,
532 "n_dup": 0,
533 "identifier_col": identifier_col,
534 "columns_json": columns_json,
535 "noise_types": [],
536 "fk_remaps": entity.fk_remaps,
537 "pool_only": true,
538 }));
539 }
540 }
541
542 let active_hn_types: Vec<&HnSchema> = schema
546 .hn_types
547 .iter()
548 .filter(|hn| only_entity.is_none_or(|t| hn.entity_type == t))
549 .collect();
550
551 let n_hard_neg = (n_duplicates as f64 * hard_neg_ratio * 0.125) as usize;
564 let hn_per_type = n_hard_neg / active_hn_types.len().max(1);
565 let hard_neg_types: Vec<serde_json::Value> = active_hn_types
566 .iter()
567 .map(|hn| {
568 serde_json::json!({
569 "entity_type": hn.entity_type,
570 "config_json": hn.config_json,
571 "count": hn_per_type,
572 })
573 })
574 .collect();
575
576 let config = serde_json::json!({
577 "domain": domain,
578 "size": size,
579 "seed": seed,
580 "difficulty": difficulty,
581 "output_format": output_format,
582 "run_id": run_id,
583 "entity_plans": entity_plans,
584 "hard_neg_types": hard_neg_types,
585 "hard_neg_ratio": hard_neg_ratio,
586 "noise_passes": ds.passes,
587 "graph_enabled": graph_enabled,
588 "graph_format": graph_format,
589 });
590
591 serde_json::from_value(config).map_err(|e| format!("build PipelineConfig: {e}"))
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn schemas_dir() -> std::path::PathBuf {
599 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("schemas")
600 }
601
602 fn kyc_schema() -> DomainSchema {
603 load_schema("kyc", &schemas_dir()).expect("load kyc.json")
604 }
605
606 fn aviation_schema() -> DomainSchema {
607 load_schema("aviation", &schemas_dir()).expect("load aviation.json")
608 }
609
610 fn sports_schema() -> DomainSchema {
611 load_schema("sports", &schemas_dir()).expect("load sports.json")
612 }
613
614 #[test]
615 fn test_valid_difficulty_names() {
616 assert_eq!(valid_difficulty_names(), vec!["light", "medium", "hell"]);
617 }
618
619 #[test]
620 fn test_load_schema_known_domain() {
621 let schema = kyc_schema();
622 assert_eq!(schema.entities.len(), 2);
623 assert!(schema.entities.iter().any(|e| e.name == "natural_person"));
624 assert!(!schema.hn_types.is_empty());
625 }
626
627 #[test]
628 fn test_load_schema_unknown_domain_lists_available() {
629 let err = load_schema("not-a-real-domain", &schemas_dir())
630 .err()
631 .unwrap();
632 assert!(err.contains("not-a-real-domain"));
633 assert!(err.contains("kyc"));
634 }
635
636 #[test]
637 fn test_load_schema_case_sensitive() {
638 let err = load_schema("KYC", &schemas_dir()).err().unwrap();
641 assert!(err.contains("KYC"));
642 }
643
644 #[test]
645 fn test_build_pipeline_config_basic() {
646 let schema = kyc_schema();
647 let config = build_pipeline_config(
648 "kyc", 1000, 42, "medium", 0.1, 0.3, &schema, "kyc_test", "parquet", false, "parquet",
649 None,
650 )
651 .expect("build config");
652 assert_eq!(config.domain, "kyc");
653 assert_eq!(config.size, 1000);
654 assert_eq!(config.entity_plans.len(), 2);
655 assert!(config.entity_plans.iter().all(|p| p.n_base >= 2));
657 assert!(!config.hard_neg_types.is_empty());
658 }
659
660 #[test]
661 fn test_build_pipeline_config_small_size_many_entities_does_not_overshoot() {
662 let schema = sports_schema();
670 assert_eq!(
671 schema.entities.len(),
672 7,
673 "this test assumes sports has 7 entities"
674 );
675 let size = 20;
676 let config = build_pipeline_config(
677 "sports",
678 size,
679 42,
680 "hell",
681 0.1,
682 0.10,
683 &schema,
684 "sports_test",
685 "parquet",
686 false,
687 "parquet",
688 None,
689 )
690 .expect("build config");
691 let n_base_sum: usize = config.entity_plans.iter().map(|p| p.n_base).sum();
692 assert!(
696 n_base_sum <= size,
697 "n_base sum {n_base_sum} overshoots requested size {size}"
698 );
699 }
700
701 #[test]
702 fn test_build_pipeline_config_normal_size_still_floors_at_2() {
703 let schema = sports_schema();
706 let config = build_pipeline_config(
707 "sports",
708 100_000,
709 42,
710 "hell",
711 0.1,
712 0.10,
713 &schema,
714 "sports_test2",
715 "parquet",
716 false,
717 "parquet",
718 None,
719 )
720 .expect("build config");
721 assert!(config.entity_plans.iter().all(|p| p.n_base >= 2));
722 }
723
724 #[test]
725 fn test_build_pipeline_config_rejects_size_below_10() {
726 let schema = kyc_schema();
727 let err = build_pipeline_config(
728 "kyc", 5, 42, "medium", 0.1, 0.3, &schema, "kyc_test", "parquet", false, "parquet",
729 None,
730 )
731 .unwrap_err();
732 assert!(err.contains("size must be >= 10"));
733 }
734
735 #[test]
736 fn test_build_pipeline_config_rejects_invalid_singleton_fraction() {
737 let schema = kyc_schema();
738 let err = build_pipeline_config(
739 "kyc", 1000, 42, "medium", 0.1, 1.5, &schema, "kyc_test", "parquet", false, "parquet",
740 None,
741 )
742 .unwrap_err();
743 assert!(err.contains("singleton_master_fraction"));
744 }
745
746 #[test]
747 fn test_build_pipeline_config_deterministic() {
748 let schema = kyc_schema();
749 let build = || {
750 build_pipeline_config(
751 "kyc", 1000, 42, "hell", 0.1, 0.2, &schema, "kyc_test", "parquet", false,
752 "parquet", None,
753 )
754 .expect("build config")
755 };
756 let a = build();
757 let b = build();
758 assert_eq!(a.entity_plans.len(), b.entity_plans.len());
759 for (pa, pb) in a.entity_plans.iter().zip(b.entity_plans.iter()) {
760 assert_eq!(pa.n_base, pb.n_base);
761 assert_eq!(pa.noise_types.len(), pb.noise_types.len());
762 }
763 }
764
765 #[test]
769 fn test_build_pipeline_config_only_entity_no_fk() {
770 let schema = kyc_schema();
771 let config = build_pipeline_config(
772 "kyc",
773 1000,
774 42,
775 "medium",
776 0.1,
777 0.3,
778 &schema,
779 "kyc_test",
780 "parquet",
781 false,
782 "parquet",
783 Some("natural_person"),
784 )
785 .expect("build config");
786 assert_eq!(config.entity_plans.len(), 1);
787 let plan = &config.entity_plans[0];
788 assert_eq!(plan.name, "natural_person");
789 assert!(!plan.pool_only);
790 assert!(plan.n_base > 100);
794 assert!(
795 config
796 .hard_neg_types
797 .iter()
798 .all(|hn| hn.entity_type == "natural_person")
799 );
800 }
801
802 #[test]
809 fn test_build_pipeline_config_only_entity_with_fk_pool() {
810 let schema = aviation_schema();
811 let config = build_pipeline_config(
812 "aviation",
813 10_000,
814 42,
815 "medium",
816 0.1,
817 0.3,
818 &schema,
819 "aviation_test",
820 "parquet",
821 false,
822 "parquet",
823 Some("aircraft"),
824 )
825 .expect("build config");
826 assert_eq!(config.entity_plans.len(), 2);
828 let target = config
829 .entity_plans
830 .iter()
831 .find(|p| p.name == "aircraft")
832 .expect("aircraft plan present");
833 assert!(!target.pool_only);
834 assert!(target.n_base > 1_000);
835 let pool = config
836 .entity_plans
837 .iter()
838 .find(|p| p.name == "airline")
839 .expect("airline pool-only plan present");
840 assert!(pool.pool_only);
841 assert!(pool.noise_types.is_empty());
842 assert!(pool.n_base <= crate::pipeline::FK_POOL_CAP);
844 }
845
846 #[test]
847 fn test_build_pipeline_config_only_entity_unknown_name() {
848 let schema = kyc_schema();
849 let err = build_pipeline_config(
850 "kyc",
851 1000,
852 42,
853 "medium",
854 0.1,
855 0.3,
856 &schema,
857 "kyc_test",
858 "parquet",
859 false,
860 "parquet",
861 Some("not_a_real_entity"),
862 )
863 .unwrap_err();
864 assert!(err.contains("not_a_real_entity"));
865 assert!(err.contains("natural_person"));
866 }
867
868 #[test]
869 fn test_deterministic_run_id_stable_and_sensitive() {
870 let a = deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, None);
871 let b = deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, None);
872 assert_eq!(a, b);
873 let c = deterministic_run_id("kyc", 1000, 43, "medium", 0.1, 0.3, "en", None, None);
874 assert_ne!(a, c);
875 }
876
877 #[test]
882 fn test_deterministic_run_id_sensitive_to_singleton_fraction_and_locale() {
883 let base = deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, None);
884 let diff_fraction =
885 deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.5, "en", None, None);
886 let diff_locale =
887 deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "fr", None, None);
888 assert_ne!(base, diff_fraction);
889 assert_ne!(base, diff_locale);
890 assert_ne!(diff_fraction, diff_locale);
891 }
892
893 #[test]
897 fn test_deterministic_run_id_sensitive_to_only_entity() {
898 let base = deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, None);
899 let only_a = deterministic_run_id(
900 "kyc",
901 1000,
902 42,
903 "medium",
904 0.1,
905 0.3,
906 "en",
907 Some("natural_person"),
908 None,
909 );
910 let only_b = deterministic_run_id(
911 "kyc",
912 1000,
913 42,
914 "medium",
915 0.1,
916 0.3,
917 "en",
918 Some("legal_entity"),
919 None,
920 );
921 assert_ne!(base, only_a);
922 assert_ne!(base, only_b);
923 assert_ne!(only_a, only_b);
924 }
925
926 #[test]
932 fn test_deterministic_run_id_sensitive_to_chunk_size() {
933 let base = deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, None);
934 let chunked_a =
935 deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, Some(500));
936 let chunked_b =
937 deterministic_run_id("kyc", 1000, 42, "medium", 0.1, 0.3, "en", None, Some(250));
938 assert_ne!(base, chunked_a);
939 assert_ne!(base, chunked_b);
940 assert_ne!(chunked_a, chunked_b);
941 }
942
943 #[test]
944 fn test_default_singleton_master_fraction_known_and_unknown() {
945 assert_eq!(default_singleton_master_fraction("light"), 0.50);
946 assert_eq!(default_singleton_master_fraction("hell"), 0.10);
947 assert_eq!(
949 default_singleton_master_fraction("bogus"),
950 default_singleton_master_fraction("medium")
951 );
952 }
953}