1use std::collections::HashMap;
33
34use rand::Rng;
35use serde::{Deserialize, Serialize};
36use symbios_genetics::Genotype;
37
38use crate::interpreter::{Interpreter, WeightedVariant};
39use crate::ops::{ShapeOp, SplitSize};
40use crate::scope::Vec3;
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct ShapeGenotype {
53 pub rules: HashMap<String, Vec<WeightedVariant>>,
55}
56
57impl ShapeGenotype {
58 pub fn from_interpreter(interp: &Interpreter) -> Self {
60 Self {
61 rules: interp.rules().clone(),
62 }
63 }
64
65 pub fn to_interpreter(&self) -> Interpreter {
70 let mut interp = Interpreter::new();
71 for (name, variants) in &self.rules {
72 interp.set_variants(name.clone(), variants.clone());
73 }
74 interp
75 }
76}
77
78impl Genotype for ShapeGenotype {
81 fn mutate<R: Rng>(&mut self, rng: &mut R, rate: f32) {
103 let mut keys: Vec<&String> = self.rules.keys().collect();
106 keys.sort();
107 let keys: Vec<String> = keys.into_iter().cloned().collect();
108 for key in keys {
109 if let Some(variants) = self.rules.get_mut(&key) {
110 for variant in variants.iter_mut() {
111 for op in variant.ops.iter_mut() {
112 mutate_op(op, rng, rate);
113 }
114 }
115 }
116 }
117 }
118
119 fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
133 let mut child_rules = self.rules.clone();
134
135 for (name, self_variants) in &self.rules {
136 if let Some(other_variants) = other.rules.get(name) {
137 if self_variants.len() == other_variants.len() {
138 let blended: Vec<WeightedVariant> = self_variants
139 .iter()
140 .zip(other_variants.iter())
141 .map(|(sv, ov)| crossover_variant(sv, ov, rng))
142 .collect();
143 child_rules.insert(name.clone(), blended);
144 } else if rng.random::<f32>() < 0.5 {
145 child_rules.insert(name.clone(), other_variants.clone());
146 }
147 }
149 }
150
151 for (name, variants) in &other.rules {
153 if !child_rules.contains_key(name) {
154 child_rules.insert(name.clone(), variants.clone());
155 }
156 }
157
158 ShapeGenotype { rules: child_rules }
159 }
160}
161
162fn jitter<R: Rng>(rng: &mut R, rate: f32, value: f64, sigma: f64) -> f64 {
168 if rng.random::<f32>() < rate {
169 let u1: f64 = rng.random::<f64>().max(1e-15); let u2: f64 = rng.random::<f64>();
171 let gauss = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
172 value + sigma * gauss
173 } else {
174 value
175 }
176}
177
178fn mutate_op<R: Rng>(op: &mut ShapeOp, rng: &mut R, rate: f32) {
181 match op {
182 ShapeOp::Extrude(h) => {
183 *h = jitter(rng, rate, *h, 0.5).max(0.1);
184 }
185 ShapeOp::Taper(t) => {
186 *t = jitter(rng, rate, *t, 0.1).clamp(0.0, 1.0);
187 }
188 ShapeOp::Scale(v) => {
189 *v = Vec3::new(
190 jitter(rng, rate, v.x, 0.2).max(0.1),
191 jitter(rng, rate, v.y, 0.2).max(0.1),
192 jitter(rng, rate, v.z, 0.2).max(0.1),
193 );
194 }
195 ShapeOp::Translate(v) => {
196 *v = Vec3::new(
197 jitter(rng, rate, v.x, 0.5),
198 jitter(rng, rate, v.y, 0.5),
199 jitter(rng, rate, v.z, 0.5),
200 );
201 }
202 ShapeOp::Split { slots, .. } => {
203 let original_abs_sum = sum_split_size(slots, |s| matches!(s, SplitSize::Absolute(_)));
208 let original_rel_sum = sum_split_size(slots, |s| matches!(s, SplitSize::Relative(_)));
209 for slot in slots.iter_mut() {
210 mutate_split_size(&mut slot.size, rng, rate);
211 }
212 repair_split_sums(slots, original_abs_sum, original_rel_sum);
213 }
214 ShapeOp::Repeat { tile_sizes, .. } => {
215 for ts in tile_sizes.iter_mut() {
216 *ts = jitter(rng, rate, *ts, 0.3).max(0.1);
217 }
218 }
219 ShapeOp::Roof { config, .. } => {
220 config.pitch = jitter(rng, rate, config.pitch, 5.0).clamp(1.0, 89.0);
221 config.overhang = jitter(rng, rate, config.overhang, 0.2).clamp(0.0, 2.0);
222 }
223 ShapeOp::Polygon(verts) => {
224 for v in verts.iter_mut() {
225 v.x = jitter(rng, rate, v.x, 0.2);
226 v.y = jitter(rng, rate, v.y, 0.2);
227 }
228 }
229 ShapeOp::Rotate(_)
231 | ShapeOp::Comp(_)
232 | ShapeOp::Offset { .. }
233 | ShapeOp::I(_)
234 | ShapeOp::Mat(_)
235 | ShapeOp::Rule(_)
236 | ShapeOp::Align { .. }
237 | ShapeOp::Attach { .. }
238 | ShapeOp::RegSnap(_)
239 | ShapeOp::IfClear { .. }
240 | ShapeOp::IfOccluded { .. } => {}
241 }
242}
243
244fn mutate_split_size<R: Rng>(size: &mut SplitSize, rng: &mut R, rate: f32) {
245 match size {
246 SplitSize::Absolute(v) => *v = jitter(rng, rate, *v, 0.3).max(0.1),
247 SplitSize::Relative(v) => *v = jitter(rng, rate, *v, 0.05).clamp(0.01, 1.0),
248 SplitSize::Floating(v) => *v = jitter(rng, rate, *v, 0.3).max(0.1),
249 }
250}
251
252fn sum_split_size<F>(slots: &[crate::ops::SplitSlot], kind: F) -> f64
254where
255 F: Fn(&SplitSize) -> bool,
256{
257 slots
258 .iter()
259 .filter(|s| kind(&s.size))
260 .map(|s| match s.size {
261 SplitSize::Absolute(v) | SplitSize::Relative(v) | SplitSize::Floating(v) => v,
262 })
263 .sum()
264}
265
266fn repair_split_sums(
270 slots: &mut [crate::ops::SplitSlot],
271 original_abs_sum: f64,
272 original_rel_sum: f64,
273) {
274 let new_abs_sum = sum_split_size(slots, |s| matches!(s, SplitSize::Absolute(_)));
275 if original_abs_sum > 1e-9 && new_abs_sum > original_abs_sum {
276 let scale = original_abs_sum / new_abs_sum;
277 for slot in slots.iter_mut() {
278 if let SplitSize::Absolute(v) = &mut slot.size {
279 *v = (*v * scale).max(0.1);
280 }
281 }
282 }
283 let new_rel_sum = sum_split_size(slots, |s| matches!(s, SplitSize::Relative(_)));
284 if original_rel_sum > 1e-9 && new_rel_sum > original_rel_sum {
285 let scale = original_rel_sum / new_rel_sum;
286 for slot in slots.iter_mut() {
287 if let SplitSize::Relative(v) = &mut slot.size {
288 *v = (*v * scale).clamp(0.01, 1.0);
289 }
290 }
291 }
292}
293
294fn crossover_variant<R: Rng>(
297 a: &WeightedVariant,
298 b: &WeightedVariant,
299 rng: &mut R,
300) -> WeightedVariant {
301 if same_structure(&a.ops, &b.ops) {
302 let ops = a
303 .ops
304 .iter()
305 .zip(b.ops.iter())
306 .map(|(ao, bo)| blend_op(ao, bo, rng))
307 .collect();
308 WeightedVariant {
309 weight: blx(a.weight, b.weight, 0.5, rng).max(0.0),
310 ops,
311 }
312 } else {
313 if rng.random::<f32>() < 0.5 {
315 a.clone()
316 } else {
317 b.clone()
318 }
319 }
320}
321
322fn same_structure(a: &[ShapeOp], b: &[ShapeOp]) -> bool {
324 a.len() == b.len() && a.iter().zip(b.iter()).all(|(ao, bo)| same_op_kind(ao, bo))
325}
326
327fn same_op_kind(a: &ShapeOp, b: &ShapeOp) -> bool {
328 use ShapeOp::*;
329 matches!(
330 (a, b),
331 (Extrude(_), Extrude(_))
332 | (Taper(_), Taper(_))
333 | (Rotate(_), Rotate(_))
334 | (Translate(_), Translate(_))
335 | (Scale(_), Scale(_))
336 | (Split { .. }, Split { .. })
337 | (Repeat { .. }, Repeat { .. })
338 | (Comp(_), Comp(_))
339 | (I(_), I(_))
340 | (Mat(_), Mat(_))
341 | (Rule(_), Rule(_))
342 | (Align { .. }, Align { .. })
343 | (Offset { .. }, Offset { .. })
344 | (Roof { .. }, Roof { .. })
345 | (Attach { .. }, Attach { .. })
346 | (Polygon(_), Polygon(_))
347 )
348}
349
350fn blx<R: Rng>(a: f64, b: f64, alpha: f64, rng: &mut R) -> f64 {
357 let lo = a.min(b);
358 let hi = a.max(b);
359 let d = (hi - lo) * alpha;
360 let lo_ext = lo - d;
361 let hi_ext = hi + d;
362 if hi_ext <= lo_ext {
363 (a + b) / 2.0
364 } else {
365 rng.random::<f64>() * (hi_ext - lo_ext) + lo_ext
366 }
367}
368
369fn blend_op<R: Rng>(a: &ShapeOp, b: &ShapeOp, rng: &mut R) -> ShapeOp {
370 match (a, b) {
371 (ShapeOp::Extrude(ha), ShapeOp::Extrude(hb)) => {
372 ShapeOp::Extrude(blx(*ha, *hb, 0.5, rng).max(0.1))
373 }
374 (ShapeOp::Taper(ta), ShapeOp::Taper(tb)) => {
375 ShapeOp::Taper(blx(*ta, *tb, 0.5, rng).clamp(0.0, 1.0))
376 }
377 (ShapeOp::Scale(va), ShapeOp::Scale(vb)) => ShapeOp::Scale(Vec3::new(
378 blx(va.x, vb.x, 0.5, rng).max(0.1),
379 blx(va.y, vb.y, 0.5, rng).max(0.1),
380 blx(va.z, vb.z, 0.5, rng).max(0.1),
381 )),
382 (ShapeOp::Translate(va), ShapeOp::Translate(vb)) => ShapeOp::Translate(Vec3::new(
383 blx(va.x, vb.x, 0.5, rng),
384 blx(va.y, vb.y, 0.5, rng),
385 blx(va.z, vb.z, 0.5, rng),
386 )),
387 (
388 ShapeOp::Split {
389 axis,
390 slots: slots_a,
391 snap,
392 },
393 ShapeOp::Split { slots: slots_b, .. },
394 ) => {
395 let slots = if slots_a.len() == slots_b.len() {
397 slots_a
398 .iter()
399 .zip(slots_b.iter())
400 .map(|(sa, sb)| crate::ops::SplitSlot {
401 size: blend_split_size(&sa.size, &sb.size, rng),
402 rule: sa.rule.clone(),
403 })
404 .collect()
405 } else {
406 slots_a.clone()
407 };
408 ShapeOp::Split {
409 axis: *axis,
410 slots,
411 snap: snap.clone(),
412 }
413 }
414 (
415 ShapeOp::Repeat {
416 axis,
417 tile_sizes: tsa,
418 rule,
419 },
420 ShapeOp::Repeat {
421 tile_sizes: tsb, ..
422 },
423 ) => {
424 let blended: Vec<f64> = if tsa.len() == tsb.len() {
425 tsa.iter()
426 .zip(tsb.iter())
427 .map(|(a, b)| blx(*a, *b, 0.5, rng).max(0.1))
428 .collect()
429 } else {
430 tsa.clone()
431 };
432 ShapeOp::Repeat {
433 axis: *axis,
434 tile_sizes: blended,
435 rule: rule.clone(),
436 }
437 }
438 (ShapeOp::Roof { config: ca, cases }, ShapeOp::Roof { config: cb, .. }) => {
439 let mut config = ca.clone();
440 config.pitch = blx(ca.pitch, cb.pitch, 0.5, rng).clamp(1.0, 89.0);
441 config.overhang = blx(ca.overhang, cb.overhang, 0.5, rng).clamp(0.0, 2.0);
442 ShapeOp::Roof {
443 config,
444 cases: cases.clone(),
445 }
446 }
447 _ => a.clone(),
449 }
450}
451
452fn blend_split_size<R: Rng>(a: &SplitSize, b: &SplitSize, rng: &mut R) -> SplitSize {
453 match (a, b) {
454 (SplitSize::Absolute(va), SplitSize::Absolute(vb)) => {
455 SplitSize::Absolute(blx(*va, *vb, 0.5, rng).max(0.1))
456 }
457 (SplitSize::Relative(va), SplitSize::Relative(vb)) => {
458 SplitSize::Relative(blx(*va, *vb, 0.5, rng).clamp(0.01, 1.0))
459 }
460 (SplitSize::Floating(va), SplitSize::Floating(vb)) => {
461 SplitSize::Floating(blx(*va, *vb, 0.5, rng).max(0.1))
462 }
463 _ => a.clone(),
465 }
466}
467
468#[cfg(test)]
471mod tests {
472 use super::*;
473 use crate::grammar::parse_ops;
474 use rand::SeedableRng;
475 use rand_pcg::Pcg64;
476
477 fn build_interp() -> Interpreter {
478 let mut interp = Interpreter::new();
479 interp.add_rule(
480 "Lot",
481 parse_ops("Extrude(10) Split(Y) { 3: Floor | ~1: Top }").unwrap(),
482 );
483 interp.add_rule("Floor", parse_ops(r#"Taper(0.0) I("Floor")"#).unwrap());
484 interp.add_rule("Top", parse_ops(r#"Taper(0.8) I("Roof")"#).unwrap());
485 interp
486 }
487
488 #[test]
489 fn test_round_trip() {
490 let interp = build_interp();
491 let dna = ShapeGenotype::from_interpreter(&interp);
492 let interp2 = dna.to_interpreter();
493 let footprint = crate::scope::Scope::new(
495 Vec3::ZERO,
496 crate::scope::Quat::IDENTITY,
497 Vec3::new(10.0, 0.0, 10.0),
498 );
499 let m1 = interp.derive(footprint, "Lot").unwrap();
500 let m2 = interp2.derive(footprint, "Lot").unwrap();
501 assert_eq!(m1.len(), m2.len());
502 assert_eq!(m1.terminals[0].mesh_id, m2.terminals[0].mesh_id);
503 }
504
505 #[test]
506 fn test_mutate_preserves_validity() {
507 let interp = build_interp();
508 let mut dna = ShapeGenotype::from_interpreter(&interp);
509 let mut rng = Pcg64::seed_from_u64(7);
510 dna.mutate(&mut rng, 1.0);
512 let interp2 = dna.to_interpreter();
513 let footprint = crate::scope::Scope::new(
514 Vec3::ZERO,
515 crate::scope::Quat::IDENTITY,
516 Vec3::new(10.0, 0.0, 10.0),
517 );
518 interp2.derive(footprint, "Lot").unwrap();
520 }
521
522 #[test]
523 fn test_crossover_produces_valid_grammar() {
524 let interp_a = build_interp();
525 let mut interp_b = build_interp();
526 interp_b.add_rule(
528 "Lot",
529 parse_ops("Extrude(20) Split(Y) { 5: Floor | ~2: Top }").unwrap(),
530 );
531
532 let dna_a = ShapeGenotype::from_interpreter(&interp_a);
533 let dna_b = ShapeGenotype::from_interpreter(&interp_b);
534 let mut rng = Pcg64::seed_from_u64(99);
535 let child = dna_a.crossover(&dna_b, &mut rng);
536 let interp_child = child.to_interpreter();
537
538 let footprint = crate::scope::Scope::new(
539 Vec3::ZERO,
540 crate::scope::Quat::IDENTITY,
541 Vec3::new(10.0, 0.0, 10.0),
542 );
543 interp_child.derive(footprint, "Lot").unwrap();
544 }
545
546 #[test]
547 fn test_crossover_with_disjoint_rules() {
548 let mut interp_a = Interpreter::new();
549 interp_a.add_rule("A", parse_ops(r#"Extrude(5) I("Mesh")"#).unwrap());
550
551 let mut interp_b = Interpreter::new();
552 interp_b.add_rule("B", parse_ops(r#"Extrude(8) I("Mesh")"#).unwrap());
553
554 let dna_a = ShapeGenotype::from_interpreter(&interp_a);
555 let dna_b = ShapeGenotype::from_interpreter(&interp_b);
556 let mut rng = Pcg64::seed_from_u64(1);
557 let child = dna_a.crossover(&dna_b, &mut rng);
558 assert!(child.rules.contains_key("A"));
560 assert!(child.rules.contains_key("B"));
561 }
562
563 #[test]
564 fn test_mutate_extrude_clamp() {
565 let mut interp = Interpreter::new();
566 interp.add_rule("R", parse_ops("Extrude(0.11) I(M)").unwrap());
567 let mut dna = ShapeGenotype::from_interpreter(&interp);
568 let mut rng = Pcg64::seed_from_u64(0);
569 for _ in 0..500 {
571 dna.mutate(&mut rng, 1.0);
572 let h = match &dna.rules["R"][0].ops[0] {
573 ShapeOp::Extrude(h) => *h,
574 _ => panic!("expected Extrude"),
575 };
576 assert!(h >= 0.1, "Extrude height {h} < 0.1");
577 }
578 }
579
580 #[test]
581 fn test_blx_same_parents() {
582 use rand::SeedableRng;
585 let mut rng = Pcg64::seed_from_u64(42);
586 let result = blx(5.0, 5.0, 0.5, &mut rng);
587 assert!((result - 5.0).abs() < 1e-9);
588 }
589
590 #[test]
594 fn test_mutate_property_1000_genotypes_all_interpret() {
595 use crate::scope::{Quat, Scope, Vec3};
596 use rand::SeedableRng;
597
598 let mut interp = Interpreter::new();
602 interp.add_rule(
603 "Lot",
604 parse_ops("Extrude(8) Split(Y) { 3: Floor | ~1: Mid | '0.2: Cap | 1.5: Top }").unwrap(),
605 );
606 interp.add_rule("Floor", parse_ops("Repeat(X, 2.0) { Bay }").unwrap());
607 interp.add_rule(
608 "Bay",
609 parse_ops(r#"Scale(0.9, 0.9, 0.9) Translate(0.1, 0, 0) I("Bay")"#).unwrap(),
610 );
611 interp.add_rule("Mid", parse_ops(r#"Taper(0.3) I("Mid")"#).unwrap());
612 interp.add_rule("Cap", parse_ops(r#"I("Cap")"#).unwrap());
613 interp.add_rule(
614 "Top",
615 parse_ops("Roof(Gable, 35) { Slope: Tile | GableEnd: Brick }").unwrap(),
616 );
617
618 let footprint = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 0.0, 6.0));
619
620 let dna_seed = ShapeGenotype::from_interpreter(&interp);
621 let mut failures: Vec<(u64, String)> = Vec::new();
622
623 for seed in 0u64..1000 {
624 let mut dna = dna_seed.clone();
625 let mut rng = Pcg64::seed_from_u64(seed);
626 for _ in 0..3 {
629 dna.mutate(&mut rng, 1.0);
630 }
631 let interp = dna.to_interpreter();
632 match interp.derive(footprint, "Lot") {
633 Ok(_) => {}
634 Err(e) => failures.push((seed, format!("{e:?}"))),
635 }
636 }
637
638 assert!(
639 failures.is_empty(),
640 "{} of 1000 mutated genotypes failed to interpret. First failures: {:?}",
641 failures.len(),
642 failures.iter().take(5).collect::<Vec<_>>(),
643 );
644 }
645}