1use super::Error;
2use super::events::{
3 GenotypeEvaluatedEvent, GenotypeGenerated, OptimizationRequestedEvent, RequestCompletedEvent,
4 RequestInterruptedEvent, RequestTerminatedEvent,
5};
6use crate::models::{
7 Breeder, Crossover, Distribution, Encodeable, Fitness, FitnessGoal, Genotype, Mutagen, Request,
8 RequestConclusion, Schedule, ScheduleDecision, Selector, Terminated,
9};
10use crate::repositories::chainable::{Chain, FromTx, ToTx};
11use crate::repositories::{genotypes, morphologies, requests};
12use crate::services::lock;
13use crate::services::optimization::models::{Terminator, TypeErasedEvaluator};
14use fx_event_bus::Publisher;
15use serde::Serialize;
16use std::collections::{HashMap, HashSet};
17use tracing::instrument;
18use uuid::Uuid;
19
20pub struct Service {
23 pub(super) locking: lock::Service,
24 pub(super) requests: requests::Repository,
25 pub(super) morphologies: morphologies::Repository,
26 pub(super) genotypes: genotypes::Repository,
27 pub(super) evaluators: HashMap<i32, Box<dyn TypeErasedEvaluator + 'static>>,
28 pub(super) max_deduplication_attempts: i32,
29}
30
31impl Service {
32 #[instrument(level = "debug", skip_all)]
34 pub(crate) fn builder(
35 locking: lock::Service,
36 requests: requests::Repository,
37 morphologies: morphologies::Repository,
38 genotypes: genotypes::Repository,
39 ) -> super::ServiceBuilder {
40 super::ServiceBuilder {
41 locking,
42 requests,
43 morphologies,
44 genotypes,
45 evaluators: HashMap::new(),
46 max_deduplication_attempts: 5, }
48 }
49
50 #[instrument(level = "debug", skip(self, data), fields(type_name = type_name, type_hash = type_hash, goal = ?goal, mutagen = ?mutagen, crossover = ?crossover))]
271 pub async fn new_optimization_request(
272 &self,
273 type_name: &str,
274 type_hash: i32,
275 goal: FitnessGoal,
276 schedule: Schedule,
277 selector: Selector,
278 mutagen: Mutagen,
279 crossover: Crossover,
280 distribution: Distribution,
281 data: Option<impl Serialize + Send + Sync>,
282 ) -> Result<Uuid, Error> {
283 tracing::info!("Optimization request received");
284
285 let request = self
286 .requests
287 .chain(|mut tx_requests| {
288 Box::pin(async move {
289 let request = tx_requests
291 .new_request(Request::new(
292 type_name,
293 type_hash,
294 goal,
295 selector,
296 schedule,
297 mutagen,
298 crossover,
299 distribution,
300 data,
301 )?)
302 .await?;
303
304 let mut publisher = fx_event_bus::Publisher::from_tx(tx_requests);
306
307 publisher
309 .publish(OptimizationRequestedEvent::new(request.id))
310 .await?;
311
312 Ok((publisher, request))
313 })
314 })
315 .await?;
316
317 Ok(request.id)
318 }
319
320 #[instrument(level = "debug", skip(self), fields(request_id = %request_id))]
323 pub(crate) async fn generate_initial_population(&self, request_id: Uuid) -> Result<(), Error> {
324 tracing::info!("Generating initial population");
325
326 let request = self.requests.get_request(request_id).await.map_err(|e| {
328 tracing::error!(
329 "Failed to fetch request in generate_initial_population with ID {}: {:?}",
330 request_id,
331 e
332 );
333 e
334 })?;
335
336 let morphology = self.morphologies.get_morphology(request.type_hash).await?;
338
339 let genomes = request.distribution.distribute(&morphology);
341
342 let mut genotypes = Vec::with_capacity(genomes.len());
343 let mut events = Vec::with_capacity(genomes.len());
344
345 for genome in genomes {
346 let genotype =
347 Genotype::new(&request.type_name, request.type_hash, genome, request.id, 1);
348 let event = GenotypeGenerated::new(request.id, genotype.id());
349
350 genotypes.push(genotype);
351 events.push(event);
352 }
353
354 self.genotypes
355 .chain(|mut tx_genotypes| {
356 Box::pin(async move {
357 let inserted_genotypes = tx_genotypes.new_genotypes(genotypes).await?;
359
360 if !inserted_genotypes.is_empty() {
362 let mut publisher = fx_event_bus::Publisher::from_tx(tx_genotypes);
364
365 publisher.publish_many(&events).await?;
367
368 Ok((publisher, request))
369 } else {
370 let publisher = fx_event_bus::Publisher::from_tx(tx_genotypes);
373 Ok((publisher, request))
374 }
375 })
376 })
377 .await?;
378
379 Ok(())
380 }
381
382 #[instrument(level = "debug", skip(self), fields(request_id = %request_id, genotype_id = %genotype_id))]
385 pub(crate) async fn evaluate_genotype(
386 &self,
387 request_id: Uuid,
388 genotype_id: Uuid,
389 ) -> Result<(), Error> {
390 let genotype = self
392 .genotypes
393 .get_genotype(&genotype_id)
394 .await
395 .map_err(|e| {
396 tracing::error!("Failed to fetch genotype with ID {}: {:?}", genotype_id, e);
397 e
398 })?;
399
400 let request = self.requests.get_request(request_id).await?;
402
403 let evaluator =
405 self.evaluators
406 .get(&genotype.type_hash())
407 .ok_or(Error::UnknownTypeError {
408 type_hash: genotype.type_hash(),
409 type_name: genotype.type_name().to_string(),
410 })?;
411
412 let terminator: Box<dyn Terminated> =
414 Box::new(Terminator::new(self.requests.clone(), request_id));
415 let fitness = evaluator.fitness(&genotype, &request, &terminator).await?;
416
417 self.genotypes
418 .chain(|mut tx_genotypes| {
419 Box::pin(async move {
420 tx_genotypes
422 .record_fitness(&Fitness::new(genotype_id, fitness))
423 .await?;
424
425 let mut publisher = fx_event_bus::Publisher::from_tx(tx_genotypes);
427
428 publisher
430 .publish(GenotypeEvaluatedEvent::new(request_id, genotype_id))
431 .await?;
432
433 Ok((publisher, ()))
434 })
435 })
436 .await?;
437
438 Ok(())
439 }
440
441 #[instrument(level = "debug", skip(self, request), fields(request_id = %request.id, num_offspring = num_offspring, next_generation_id = next_generation_id))]
442 async fn breed_genotypes(
443 &self,
444 request: &Request,
445 num_offspring: usize,
446 next_generation_id: i32,
447 ) -> Result<(), Error> {
448 let exists = self
450 .genotypes
451 .check_if_generation_exists(request.id, next_generation_id)
452 .await?;
453 if exists {
454 return Ok(());
455 }
456
457 tracing::info!("Breeding genotypes");
458 let candidates_with_fitness = self
460 .genotypes
461 .search_genotypes(
462 &genotypes::Filter::default()
463 .with_request_id(request.id)
464 .with_fitness(true)
465 .with_order_random(),
466 request.selector.sample_size(),
467 )
468 .await?;
469
470 let morphology = self.morphologies.get_morphology(request.type_hash).await?;
472
473 let population = self.genotypes.get_population(&request.id).await?;
475
476 let best_fitness = *request
477 .goal
478 .best_fitness(&population.min_fitness, &population.max_fitness);
479
480 let mut final_genotypes = Vec::with_capacity(num_offspring);
482 let mut generated_hashes = HashSet::new();
483 let mut deduplication_attempts = 0;
484
485 while final_genotypes.len() < num_offspring
486 && deduplication_attempts < self.max_deduplication_attempts
487 {
488 let needed = num_offspring - final_genotypes.len();
489
490 let parent_pairs = request
492 .selector
493 .select_parents(needed, &candidates_with_fitness, &request.goal)
494 .map_err(|e| Error::SelectionError(e))?;
495
496 let batch_genotypes = {
498 let mut rng = rand::rng();
499 Breeder::breed_batch(
500 &request,
501 &morphology,
502 &parent_pairs,
503 next_generation_id,
504 request.goal.calculate_progress(best_fitness),
505 &mut rng,
506 )
507 };
508
509 let batch_hashes: Vec<i64> = batch_genotypes.iter().map(|g| g.genome_hash()).collect();
511
512 let intersecting_hashes = self
514 .genotypes
515 .get_intersection(request.id, &batch_hashes)
516 .await?
517 .into_iter()
518 .collect::<HashSet<i64>>();
519
520 let mut unique_count = 0;
522 for genotype in batch_genotypes {
523 if !intersecting_hashes.contains(&genotype.genome_hash())
524 && !generated_hashes.contains(&genotype.genome_hash())
525 {
526 generated_hashes.insert(genotype.genome_hash());
527 final_genotypes.push(genotype);
528 unique_count += 1;
529 }
530 }
531
532 if unique_count < num_offspring {
533 tracing::info!(
535 "Breeding generated {} non-unique genomes during attempt {}",
536 num_offspring - unique_count,
537 deduplication_attempts
538 );
539 }
540
541 if unique_count == 0 {
543 deduplication_attempts += 1;
544 } else {
545 deduplication_attempts = 0;
546 }
547 }
548
549 if deduplication_attempts >= self.max_deduplication_attempts {
551 tracing::warn!(
552 "Breeding exhausted max_deduplication_attempts ({}) for request {}. Consider tuning selection parameters or increasing diversity.",
553 self.max_deduplication_attempts,
554 request.id
555 );
556 }
557
558 if final_genotypes.len() < num_offspring {
560 tracing::warn!(
561 "Could only generate {} unique genotypes out of {} requested for request {}",
562 final_genotypes.len(),
563 num_offspring,
564 request.id
565 );
566 }
567
568 if !final_genotypes.is_empty() {
570 let events: Vec<GenotypeGenerated> = final_genotypes
572 .iter()
573 .map(|genotype| GenotypeGenerated::new(request.id, genotype.id()))
574 .collect();
575
576 self.genotypes
577 .chain(|mut tx_genotypes| {
578 Box::pin(async move {
579 let inserted_genotypes =
580 tx_genotypes.new_genotypes(final_genotypes).await?;
581
582 if !inserted_genotypes.is_empty() {
584 let mut publisher = fx_event_bus::Publisher::from_tx(tx_genotypes);
585 publisher.publish_many(&events).await?;
586
587 Ok((publisher, ()))
588 } else {
589 let publisher = fx_event_bus::Publisher::from_tx(tx_genotypes);
591 Ok((publisher, ()))
592 }
593 })
594 })
595 .await?;
596 }
597
598 Ok(())
599 }
600
601 #[instrument(level = "debug", skip(self), fields(request_id = %request_id))]
604 pub(crate) async fn maintain_population(&self, request_id: Uuid) -> Result<(), Error> {
605 let key = format!("maintain_population_{}", request_id);
606 self.locking
607 .lock_while(&key, || async {
608 if let Some(_) = self.requests.get_request_conclusion(&request_id).await? {
610 tracing::debug!("Request already concluded, skipping maintenance");
611 return Ok(());
612 }
613
614 let request = self.requests.get_request(request_id).await.map_err(|e| {
616 tracing::error!("Failed to fetch request with ID {}: {:?}", request_id, e);
617 e
618 })?;
619
620 let population = self.genotypes.get_population(&request.id).await?;
622
623 let Some(best_fitness) = *request
624 .goal
625 .best_fitness(&population.min_fitness, &population.max_fitness)
626 else {
627 return Ok(());
628 };
629
630 if request.is_completed(best_fitness) {
631 self.genotypes
633 .chain(|tx_genotypes| {
634 Box::pin(async move {
635 let mut publisher = fx_event_bus::Publisher::from_tx(tx_genotypes);
637
638 publisher
640 .publish(RequestCompletedEvent::new(request.id))
641 .await?;
642
643 Ok((publisher, ()))
644 })
645 })
646 .await?;
647 return Ok(());
648 }
649
650 match request.schedule.should_breed(&population) {
651 ScheduleDecision::Wait => Ok(()), ScheduleDecision::Terminate => self.publish_terminated(request.id).await,
653 ScheduleDecision::Breed {
654 num_offspring,
655 next_generation_id,
656 } => {
657 self.breed_genotypes(&request, num_offspring, next_generation_id)
658 .await
659 }
660 }
661 })
662 .await?
663 }
664
665 #[instrument(level = "debug", skip(self), fields(request_id = %request_conclusion.request_id, concluded_at = %request_conclusion.concluded_at, concluded_with = ?request_conclusion.concluded_with))]
668 pub(crate) async fn conclude_request(
669 &self,
670 request_conclusion: RequestConclusion,
671 ) -> Result<(), Error> {
672 let key = format!("conclude_request_{}", request_conclusion.request_id);
673 self.locking
674 .lock_while(&key, || async {
675 if let Some(_) = self
676 .requests
677 .get_request_conclusion(&request_conclusion.request_id)
678 .await?
679 {
680 return Ok::<(), super::Error>(());
681 }
682
683 tracing::info!("Concluding request");
684
685 self.requests
686 .new_request_conclusion(&request_conclusion)
687 .await?;
688
689 Ok(())
690 })
691 .await?
692 }
693
694 #[instrument(level = "debug", skip(self), fields(request_id = %request_id))]
696 pub(crate) async fn publish_terminated(&self, request_id: Uuid) -> Result<(), Error> {
697 self.genotypes
698 .chain(|tx_genotypes| {
699 Box::pin(async move {
700 let mut publisher = Publisher::new(tx_genotypes.tx());
701 let ret = publisher
702 .publish(RequestTerminatedEvent::new(request_id))
703 .await?;
704
705 Ok((publisher, ret))
706 })
707 })
708 .await?;
709
710 Ok(())
711 }
712
713 #[instrument(level = "debug", skip(self), fields(request_id = %request_id))]
716 pub async fn interrupt_request(&self, request_id: Uuid) -> Result<(), Error> {
717 self.genotypes
718 .chain(|tx_genotypes| {
719 Box::pin(async move {
720 let mut publisher = Publisher::new(tx_genotypes.tx());
721 let ret = publisher
722 .publish(RequestInterruptedEvent::new(request_id))
723 .await?;
724
725 Ok((publisher, ret))
726 })
727 })
728 .await?;
729
730 Ok(())
731 }
732
733 #[instrument(level = "debug", skip(self), fields(request_id = %request_id))]
736 pub async fn get_best_genotype(
737 &self,
738 request_id: Uuid,
739 ) -> Result<Option<(Genotype, f64)>, Error> {
740 let request = self.requests.get_request(request_id).await?;
741
742 let filter = match request.goal {
744 FitnessGoal::Minimize { .. } => genotypes::Filter::default()
745 .with_request_id(request_id)
746 .with_fitness(true)
747 .with_order_fitness_asc(),
748 FitnessGoal::Maximize { .. } => genotypes::Filter::default()
749 .with_request_id(request_id)
750 .with_fitness(true)
751 .with_order_fitness_desc(),
752 };
753
754 let results = self.genotypes.search_genotypes(&filter, 1).await?;
755
756 let best = results
758 .into_iter()
759 .next()
760 .and_then(|(genotype, fitness_opt)| fitness_opt.map(|fitness| (genotype, fitness)));
761
762 Ok(best)
763 }
764
765 #[instrument(level = "debug", skip(self), fields(request_id = %request_id))]
768 pub async fn is_request_concluded(&self, request_id: Uuid) -> Result<bool, Error> {
769 Ok(self
770 .requests
771 .get_request_conclusion(&request_id)
772 .await?
773 .is_some())
774 }
775
776 #[instrument(level = "debug", skip(self), fields(type_name))]
778 pub async fn search_genotypes(
779 &self,
780 filter: &genotypes::Filter,
781 limit: i64,
782 ) -> Result<Vec<(Genotype, Option<f64>)>, Error> {
783 let genotypes = self.genotypes.search_genotypes(filter, limit).await?;
784 Ok(genotypes)
785 }
786
787 #[instrument(level = "debug", skip(self), fields(type_name))]
789 pub async fn get_phenotype<T: Encodeable>(
790 &self,
791 genotype_id: &Uuid,
792 ) -> Result<T::Phenotype, Error> {
793 let genotype = self.genotypes.get_genotype(genotype_id).await?;
794
795 if genotype.type_hash() != T::HASH {
796 return Err(Error::UnknownPhenotype {
797 type_name: genotype.type_name().to_string(),
798 type_hash: genotype.type_hash(),
799 });
800 }
801
802 Ok(T::decode(&genotype.genome()))
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use crate::bootstrap;
810 use crate::models::{
811 Crossover, Distribution, Encodeable, Evaluator, FitnessGoal, GeneBounds, Mutagen,
812 MutationRate, Request, Schedule, Selector, Temperature, Terminated,
813 };
814 use futures::future::BoxFuture;
815
816 async fn create_test_service(pool: sqlx::PgPool) -> Service {
817 let builder = bootstrap::bootstrap(pool).await.unwrap();
818 builder.build()
819 }
820
821 #[sqlx::test(migrations = false)]
822 async fn test_new_optimization_request_happy_path(pool: sqlx::PgPool) -> anyhow::Result<()> {
823 crate::migrations::run_default_migrations(&pool).await?;
824
825 let service = create_test_service(pool.clone()).await;
827
828 let type_name = "TestType";
830 let type_hash = 123;
831 let goal = FitnessGoal::maximize(0.95)?;
832 let schedule = Schedule::generational(100, 10);
833 let selector = Selector::tournament(3, 20).expect("is valid");
834 let mutagen = Mutagen::new(Temperature::constant(0.5)?, MutationRate::constant(0.3)?);
835 let crossover = Crossover::uniform(0.5)?;
836 let distribution = Distribution::random(50);
837
838 let result = service
840 .new_optimization_request(
841 type_name,
842 type_hash,
843 goal.clone(),
844 schedule.clone(),
845 selector.clone(),
846 mutagen.clone(),
847 crossover.clone(),
848 distribution.clone(),
849 None::<()>,
850 )
851 .await;
852
853 assert!(result.is_ok(), "new_optimization_request should succeed");
855
856 let request_count = sqlx::query_scalar!(
858 "SELECT COUNT(*) FROM fx_durable_ga.requests WHERE type_name = $1 AND type_hash = $2",
859 type_name,
860 type_hash
861 )
862 .fetch_one(&pool)
863 .await?
864 .unwrap_or(0);
865
866 assert_eq!(
867 request_count, 1,
868 "Exactly one request should be stored in database"
869 );
870
871 let event_count: i64 = sqlx::query_scalar::<_, i64>(
873 "SELECT COUNT(*) FROM fx_event_bus.events_unacknowledged WHERE name = $1",
874 )
875 .bind("OptimizationRequested")
876 .fetch_one(&pool)
877 .await?;
878
879 assert_eq!(
880 event_count, 1,
881 "Exactly one OptimizationRequestedEvent should be published"
882 );
883
884 Ok(())
885 }
886
887 #[sqlx::test(migrations = false)]
888 async fn test_generate_initial_population_happy_path(pool: sqlx::PgPool) -> anyhow::Result<()> {
889 crate::migrations::run_default_migrations(&pool).await?;
890
891 let service = create_test_service(pool.clone()).await;
893
894 let type_name = "TestType";
896 let type_hash = 456;
897 let morphology = crate::models::Morphology::new(
898 type_name,
899 type_hash,
900 vec![
901 crate::models::GeneBounds::integer(0, 9, 10).unwrap(), crate::models::GeneBounds::integer(0, 4, 5).unwrap(), ],
904 );
905
906 service.morphologies.new_morphology(morphology).await?;
908
909 let goal = FitnessGoal::maximize(0.95)?;
911 let schedule = Schedule::generational(100, 10);
912 let selector = Selector::tournament(3, 20).expect("is valid");
913 let mutagen = Mutagen::new(Temperature::constant(0.5)?, MutationRate::constant(0.3)?);
914 let crossover = Crossover::uniform(0.5)?;
915 let distribution = Distribution::random(10); let request = crate::models::Request::new(
918 type_name,
919 type_hash,
920 goal,
921 selector,
922 schedule,
923 mutagen,
924 crossover,
925 distribution,
926 None::<()>,
927 )?;
928 let request_id = request.id;
929
930 service
932 .requests
933 .chain(|mut tx_requests| {
934 Box::pin(async move {
935 let request = tx_requests.new_request(request).await?;
936 Ok((tx_requests, request))
937 })
938 })
939 .await?;
940
941 let result = service.generate_initial_population(request_id).await;
943
944 assert!(result.is_ok(), "generate_initial_population should succeed");
946
947 let genotype_count = sqlx::query_scalar!(
949 "SELECT COUNT(*) FROM fx_durable_ga.genotypes WHERE request_id = $1 AND generation_id = 1",
950 request_id
951 )
952 .fetch_one(&pool)
953 .await?
954 .unwrap_or(0);
955
956 assert_eq!(
957 genotype_count, 10,
958 "Exactly 10 genotypes should be created for generation 1"
959 );
960
961 let event_count: i64 = sqlx::query_scalar::<_, i64>(
963 "SELECT COUNT(*) FROM fx_event_bus.events_unacknowledged WHERE name = $1",
964 )
965 .bind("GenotypeGenerated")
966 .fetch_one(&pool)
967 .await?;
968
969 assert_eq!(
970 event_count, 10,
971 "Exactly 10 GenotypeGenerated events should be published"
972 );
973
974 Ok(())
975 }
976
977 #[sqlx::test(migrations = false)]
978 async fn test_evaluate_genotype_happy_path(pool: sqlx::PgPool) -> anyhow::Result<()> {
979 crate::migrations::run_default_migrations(&pool).await?;
980
981 #[derive(Debug)]
983 struct TestType;
984
985 impl Encodeable for TestType {
986 const NAME: &'static str = "TestType";
987 type Phenotype = (i64, i64);
988
989 fn morphology() -> Vec<GeneBounds> {
990 vec![
991 GeneBounds::integer(0, 9, 10).unwrap(),
992 GeneBounds::integer(0, 4, 5).unwrap(),
993 ]
994 }
995
996 fn encode(&self) -> Vec<i64> {
997 vec![5, 2] }
999
1000 fn decode(genes: &[i64]) -> Self::Phenotype {
1001 (genes[0], genes[1])
1002 }
1003 }
1004
1005 struct TestEvaluator;
1007
1008 impl Evaluator<(i64, i64)> for TestEvaluator {
1009 fn fitness<'a>(
1010 &self,
1011 _genotype_id: uuid::Uuid,
1012 _phenotype: (i64, i64),
1013 _request: &'a Request,
1014 _terminated: &'a Box<dyn Terminated>,
1015 ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
1016 Box::pin(async move { Ok(0.75) })
1017 }
1018 }
1019
1020 let service = bootstrap::bootstrap(pool.clone())
1022 .await?
1023 .register::<TestType, _>(TestEvaluator)
1024 .await?
1025 .build();
1026
1027 let goal = FitnessGoal::maximize(0.95)?;
1029 let schedule = Schedule::generational(100, 10);
1030 let selector = Selector::tournament(3, 20).expect("is valid");
1031 let mutagen = Mutagen::new(Temperature::constant(0.5)?, MutationRate::constant(0.3)?);
1032 let crossover = Crossover::uniform(0.5)?;
1033 let distribution = Distribution::random(1);
1034
1035 let request = crate::models::Request::new(
1036 TestType::NAME,
1037 TestType::HASH,
1038 goal,
1039 selector,
1040 schedule,
1041 mutagen,
1042 crossover,
1043 distribution,
1044 None::<()>,
1045 )?;
1046 let request_id = request.id;
1047
1048 service
1050 .requests
1051 .chain(|mut tx_requests| {
1052 Box::pin(async move {
1053 let request = tx_requests.new_request(request).await?;
1054 Ok((tx_requests, request))
1055 })
1056 })
1057 .await?;
1058
1059 let genotype = crate::models::Genotype::new(
1061 TestType::NAME,
1062 TestType::HASH,
1063 vec![7, 3], request_id,
1065 1, );
1067 let genotype_id = genotype.id();
1068
1069 service
1071 .genotypes
1072 .chain(|mut tx_genotypes| {
1073 Box::pin(async move {
1074 let genotypes = tx_genotypes.new_genotypes(vec![genotype]).await?;
1075 Ok((tx_genotypes, genotypes))
1076 })
1077 })
1078 .await?;
1079
1080 let result = service.evaluate_genotype(request_id, genotype_id).await;
1082
1083 assert!(result.is_ok(), "evaluate_genotype should succeed");
1085
1086 let fitness = sqlx::query_scalar!(
1088 "SELECT fitness FROM fx_durable_ga.fitness WHERE genotype_id = $1",
1089 genotype_id
1090 )
1091 .fetch_one(&pool)
1092 .await?;
1093
1094 assert_eq!(fitness, 0.75, "Fitness should be recorded as 0.75");
1095
1096 let event_count: i64 = sqlx::query_scalar::<_, i64>(
1098 "SELECT COUNT(*) FROM fx_event_bus.events_unacknowledged WHERE name = $1",
1099 )
1100 .bind("GenotypeEvaluated")
1101 .fetch_one(&pool)
1102 .await?;
1103
1104 assert_eq!(
1105 event_count, 1,
1106 "Exactly one GenotypeEvaluatedEvent should be published"
1107 );
1108
1109 Ok(())
1110 }
1111
1112 #[sqlx::test(migrations = false)]
1113 async fn test_maintain_population_request_completion(pool: sqlx::PgPool) -> anyhow::Result<()> {
1114 crate::migrations::run_default_migrations(&pool).await?;
1115
1116 let service = create_test_service(pool.clone()).await;
1118
1119 let type_name = "TestType";
1121 let type_hash = 789;
1122 let morphology = crate::models::Morphology::new(
1123 type_name,
1124 type_hash,
1125 vec![
1126 crate::models::GeneBounds::integer(0, 9, 10).unwrap(),
1127 crate::models::GeneBounds::integer(0, 4, 5).unwrap(),
1128 ],
1129 );
1130 service.morphologies.new_morphology(morphology).await?;
1131
1132 let goal = FitnessGoal::maximize(0.5)?;
1134 let schedule = Schedule::generational(100, 10);
1135 let selector = Selector::tournament(3, 20).expect("is valid");
1136 let mutagen = Mutagen::new(Temperature::constant(0.5)?, MutationRate::constant(0.3)?);
1137 let crossover = Crossover::uniform(0.5)?;
1138 let distribution = Distribution::random(5);
1139
1140 let request = crate::models::Request::new(
1141 type_name,
1142 type_hash,
1143 goal,
1144 selector,
1145 schedule,
1146 mutagen,
1147 crossover,
1148 distribution,
1149 None::<()>,
1150 )?;
1151 let request_id = request.id;
1152
1153 service
1155 .requests
1156 .chain(|mut tx_requests| {
1157 Box::pin(async move {
1158 let request = tx_requests.new_request(request).await?;
1159 Ok((tx_requests, request))
1160 })
1161 })
1162 .await?;
1163
1164 let genotype =
1166 crate::models::Genotype::new(type_name, type_hash, vec![5, 2], request_id, 1);
1167 let genotype_id = genotype.id();
1168
1169 service
1171 .genotypes
1172 .chain(|mut tx_genotypes| {
1173 Box::pin(async move {
1174 tx_genotypes.new_genotypes(vec![genotype]).await?;
1176
1177 let fitness = crate::models::Fitness::new(genotype_id, 0.8);
1179 tx_genotypes.record_fitness(&fitness).await?;
1180
1181 Ok((tx_genotypes, ()))
1182 })
1183 })
1184 .await?;
1185
1186 let result = service.maintain_population(request_id).await;
1188
1189 assert!(result.is_ok(), "maintain_population should succeed");
1191
1192 let event_count: i64 = sqlx::query_scalar::<_, i64>(
1194 "SELECT COUNT(*) FROM fx_event_bus.events_unacknowledged WHERE name = $1",
1195 )
1196 .bind("RequestCompleted")
1197 .fetch_one(&pool)
1198 .await?;
1199
1200 assert_eq!(
1201 event_count, 1,
1202 "Exactly one RequestCompletedEvent should be published"
1203 );
1204
1205 Ok(())
1206 }
1207
1208 #[sqlx::test(migrations = false)]
1209 async fn test_interrupt_request(pool: sqlx::PgPool) -> anyhow::Result<()> {
1210 crate::migrations::run_default_migrations(&pool).await?;
1211
1212 let service = create_test_service(pool.clone()).await;
1214
1215 let goal = FitnessGoal::maximize(0.95)?;
1217 let schedule = Schedule::generational(100, 10);
1218 let selector = Selector::tournament(3, 20).expect("is valid");
1219 let mutagen = Mutagen::new(Temperature::constant(0.5)?, MutationRate::constant(0.3)?);
1220 let crossover = Crossover::uniform(0.5)?;
1221 let distribution = Distribution::random(5);
1222
1223 let request = crate::models::Request::new(
1224 "TestType",
1225 123,
1226 goal,
1227 selector,
1228 schedule,
1229 mutagen,
1230 crossover,
1231 distribution,
1232 None::<()>,
1233 )?;
1234 let request_id = request.id;
1235
1236 service
1238 .requests
1239 .chain(|mut tx_requests| {
1240 Box::pin(async move {
1241 let request = tx_requests.new_request(request).await?;
1242 Ok((tx_requests, request))
1243 })
1244 })
1245 .await?;
1246
1247 let result = service.interrupt_request(request_id).await;
1249
1250 assert!(result.is_ok(), "interrupt_request should succeed");
1252
1253 let event_count: i64 = sqlx::query_scalar::<_, i64>(
1255 "SELECT COUNT(*) FROM fx_event_bus.events_unacknowledged WHERE name = $1",
1256 )
1257 .bind("RequestInterrupted")
1258 .fetch_one(&pool)
1259 .await?;
1260
1261 assert_eq!(
1262 event_count, 1,
1263 "Exactly one RequestInterruptedEvent should be published"
1264 );
1265
1266 Ok(())
1267 }
1268
1269 #[sqlx::test(migrations = false)]
1270 async fn test_get_phenotype(pool: sqlx::PgPool) -> anyhow::Result<()> {
1271 crate::migrations::run_default_migrations(&pool).await?;
1272
1273 #[derive(Debug, PartialEq)]
1274 struct TestPoint {
1275 x: i64,
1276 y: i64,
1277 }
1278
1279 impl Encodeable for TestPoint {
1280 const NAME: &'static str = "TestPoint";
1281 type Phenotype = TestPoint;
1282
1283 fn morphology() -> Vec<GeneBounds> {
1284 vec![
1285 GeneBounds::integer(0, 9, 10).unwrap(),
1286 GeneBounds::integer(0, 4, 5).unwrap(),
1287 ]
1288 }
1289
1290 fn encode(&self) -> Vec<i64> {
1291 vec![self.x, self.y]
1292 }
1293
1294 fn decode(genes: &[i64]) -> Self::Phenotype {
1295 TestPoint {
1296 x: genes[0],
1297 y: genes[1],
1298 }
1299 }
1300 }
1301
1302 let service = create_test_service(pool.clone()).await;
1303
1304 let goal = FitnessGoal::maximize(0.95)?;
1305 let schedule = Schedule::generational(100, 10);
1306 let selector = Selector::tournament(3, 20).expect("is valid");
1307 let mutagen = Mutagen::new(Temperature::constant(0.5)?, MutationRate::constant(0.3)?);
1308 let crossover = Crossover::uniform(0.5)?;
1309 let distribution = Distribution::random(10);
1310
1311 let request = crate::models::Request::new(
1312 TestPoint::NAME,
1313 TestPoint::HASH,
1314 goal,
1315 selector,
1316 schedule,
1317 mutagen,
1318 crossover,
1319 distribution,
1320 None::<()>,
1321 )?;
1322 let request_id = request.id;
1323
1324 service
1325 .requests
1326 .chain(|mut tx_requests| {
1327 Box::pin(async move {
1328 let request = tx_requests.new_request(request).await?;
1329 Ok((tx_requests, request))
1330 })
1331 })
1332 .await?;
1333
1334 let genotype = crate::models::Genotype::new(
1335 TestPoint::NAME,
1336 TestPoint::HASH,
1337 vec![7, 3],
1338 request_id,
1339 1,
1340 );
1341 let genotype_id = genotype.id();
1342
1343 service
1344 .genotypes
1345 .chain(|mut tx_genotypes| {
1346 Box::pin(async move {
1347 tx_genotypes.new_genotypes(vec![genotype]).await?;
1348 Ok((tx_genotypes, ()))
1349 })
1350 })
1351 .await?;
1352
1353 let result: TestPoint = service.get_phenotype::<TestPoint>(&genotype_id).await?;
1354
1355 assert_eq!(result.x, 7);
1356 assert_eq!(result.y, 3);
1357
1358 Ok(())
1359 }
1360}