Skip to main content

fx_durable_ga/services/optimization/
service_builder.rs

1use super::models::TypeErasedEvaluator;
2use crate::{
3    models::{Encodeable, Evaluator, Morphology},
4    repositories::{genotypes, morphologies, requests},
5    services::{lock, optimization::Service},
6};
7use std::collections::HashMap;
8use tracing::instrument;
9
10/// Builder for creating optimization services with registered type evaluators.
11/// Handles morphology registration and evaluator type erasure.
12pub struct ServiceBuilder {
13    pub(super) locking: lock::Service,
14    pub(super) requests: requests::Repository,
15    pub(super) morphologies: morphologies::Repository,
16    pub(super) genotypes: genotypes::Repository,
17    pub(super) evaluators: HashMap<i32, Box<dyn TypeErasedEvaluator + 'static>>,
18    pub(super) max_deduplication_attempts: i32,
19}
20
21impl ServiceBuilder {
22    /// Registers a problem type and its fitness evaluator with the optimization service.
23    ///
24    /// This is the core method for connecting your domain-specific problems to the genetic
25    /// algorithm engine. It establishes the relationship between your data structures,
26    /// their genetic encoding, and the fitness evaluation logic.
27    ///
28    /// # Core Concepts
29    ///
30    /// ## Type Registration Process
31    /// 1. **Morphology Creation**: Automatically creates and stores the search space definition
32    /// 2. **Type Erasure**: Converts your specific evaluator into a type-erased form for internal storage
33    /// 3. **Hash Mapping**: Associates your type's hash with its evaluator for efficient lookup
34    /// 4. **Database Integration**: Ensures the type's morphology exists in persistent storage
35    ///
36    /// ## Required Trait Implementations
37    /// - **`Encodeable`**: Defines how your type maps to/from genetic representation
38    /// - **`Evaluator`**: Provides the fitness function that guides evolution
39    ///
40    /// # Parameters
41    ///
42    /// * `evaluator` - Your fitness evaluator implementing `Evaluator<T::Phenotype>`
43    ///
44    /// # Type Parameters
45    ///
46    /// * `T: Encodeable` - Your problem type that can be encoded as genes
47    /// * `E: Evaluator<T::Phenotype>` - Your fitness evaluator for decoded phenotypes
48    ///
49    /// # Encodeable Implementation Requirements
50    ///
51    /// Your type must implement `Encodeable` with:
52    /// - `NAME`: Unique string identifier for your type
53    /// - `Phenotype`: The decoded form passed to evaluators
54    /// - `morphology()`: Defines the search space bounds and structure
55    /// - `encode()`: Converts instances to genetic representation
56    /// - `decode()`: Converts genes back to phenotypes for evaluation
57    ///
58    /// # Evaluator Implementation Requirements
59    ///
60    /// Your evaluator must implement `Evaluator<T::Phenotype>` with:
61    /// - `fitness()`: Async function returning fitness scores (any finite f64 values)
62    /// - Support for early termination via the `Terminated` trait
63    /// - Error handling for failed evaluations
64    ///
65    /// # Common Registration Patterns
66    ///
67    /// ## Simple Numerical Optimization
68    /// ```rust,no_run
69    /// use fx_durable_ga::models::*;
70    /// # use fx_durable_ga::optimization::ServiceBuilder;
71    /// #
72    /// # use futures::future::BoxFuture;
73    ///
74    /// #[derive(Debug, Clone)]
75    /// struct Parameters {
76    ///     learning_rate: f64,
77    ///     batch_size: i64,
78    ///     layers: i64,
79    /// }
80    ///
81    /// impl Encodeable for Parameters {
82    ///     const NAME: &'static str = "Parameters";
83    ///     type Phenotype = Parameters;
84    ///
85    ///     fn morphology() -> Vec<GeneBounds> {
86    ///         vec![
87    ///             GeneBounds::decimal(0.001, 1.0, 1000, 3).unwrap(),  // learning_rate
88    ///             GeneBounds::integer(8, 512, 32).unwrap(),            // batch_size
89    ///             GeneBounds::integer(1, 10, 10).unwrap(),             // layers
90    ///         ]
91    ///     }
92    ///
93    ///     fn encode(&self) -> Vec<i64> {
94    ///         let bounds = Self::morphology();
95    ///         vec![
96    ///             bounds[0].from_sample(self.learning_rate / 1.0),
97    ///             bounds[1].from_sample((self.batch_size - 8) as f64 / (512 - 8) as f64),
98    ///             bounds[2].from_sample((self.layers - 1) as f64 / 9.0),
99    ///         ]
100    ///     }
101    ///
102    ///     fn decode(genes: &[i64]) -> Self::Phenotype {
103    ///         let bounds = Self::morphology();
104    ///         Parameters {
105    ///             learning_rate: bounds[0].decode_f64(genes[0]),
106    ///             batch_size: (bounds[1].decode_f64(genes[1]) * (512 - 8) as f64 + 8.0) as i64,
107    ///             layers: (bounds[2].decode_f64(genes[2]) * 9.0 + 1.0) as i64,
108    ///         }
109    ///     }
110    /// }
111    ///
112    /// struct ModelEvaluator;
113    ///
114    /// impl Evaluator<Parameters> for ModelEvaluator {
115    ///     fn fitness<'a>(
116    ///         &self,
117    ///         _genotype_id: uuid::Uuid,
118    ///         params: Parameters,
119    ///         _request: &Request,
120    ///         _terminated: &'a Box<dyn Terminated>,
121    ///     ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
122    ///         Box::pin(async move {
123    ///             // Train model with parameters and return accuracy
124    ///             let accuracy = train_model(params.learning_rate, params.batch_size, params.layers).await?;
125    ///             Ok(accuracy)
126    ///         })
127    ///     }
128    /// }
129    ///
130    /// # async fn train_model(_lr: f64, _batch: i64, _layers: i64) -> Result<f64, anyhow::Error> { Ok(0.85) }
131    /// # async fn example(builder: ServiceBuilder) -> Result<ServiceBuilder, Box<dyn std::error::Error>> {
132    /// let builder = builder.register::<Parameters, _>(ModelEvaluator).await?;
133    /// # Ok(builder)
134    /// # }
135    /// ```
136    ///
137    /// ## Geometric/Spatial Problems
138    /// ```rust,no_run
139    /// use fx_durable_ga::models::*;
140    /// # use fx_durable_ga::optimization::ServiceBuilder;
141    /// # use futures::future::BoxFuture;
142    ///
143    /// #[derive(Debug, Clone)]
144    /// struct Point3D {
145    ///     x: f64,
146    ///     y: f64,
147    ///     z: f64,
148    /// }
149    ///
150    /// impl Encodeable for Point3D {
151    ///     const NAME: &'static str = "Point3D";
152    ///     type Phenotype = Point3D;
153    ///
154    ///     fn morphology() -> Vec<GeneBounds> {
155    ///         vec![
156    ///             GeneBounds::decimal(-10.0, 10.0, 2000, 3).unwrap(),  // x: -10.0 to 10.0
157    ///             GeneBounds::decimal(-10.0, 10.0, 2000, 3).unwrap(),  // y: -10.0 to 10.0
158    ///             GeneBounds::decimal(0.0, 20.0, 2000, 3).unwrap(),    // z: 0.0 to 20.0
159    ///         ]
160    ///     }
161    ///
162    ///     fn encode(&self) -> Vec<i64> {
163    ///         let bounds = Self::morphology();
164    ///         vec![
165    ///             bounds[0].from_sample((self.x + 10.0) / 20.0),
166    ///             bounds[1].from_sample((self.y + 10.0) / 20.0),
167    ///             bounds[2].from_sample(self.z / 20.0),
168    ///         ]
169    ///     }
170    ///
171    ///     fn decode(genes: &[i64]) -> Self::Phenotype {
172    ///         let bounds = Self::morphology();
173    ///         Point3D {
174    ///             x: bounds[0].decode_f64(genes[0]) * 20.0 - 10.0,
175    ///             y: bounds[1].decode_f64(genes[1]) * 20.0 - 10.0,
176    ///             z: bounds[2].decode_f64(genes[2]) * 20.0,
177    ///         }
178    ///     }
179    /// }
180    ///
181    /// struct DistanceEvaluator {
182    ///     target: Point3D,
183    /// }
184    ///
185    /// impl Evaluator<Point3D> for DistanceEvaluator {
186    ///     fn fitness<'a>(
187    ///         &self,
188    ///         _genotype_id: uuid::Uuid,
189    ///         point: Point3D,
190    ///         _request: &Request,
191    ///         terminated: &'a Box<dyn Terminated>,
192    ///     ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
193    ///         let target = self.target.clone();
194    ///         Box::pin(async move {
195    ///             // Check for early termination
196    ///             if terminated.is_terminated().await {
197    ///                 return Err(anyhow::anyhow!("Evaluation terminated"));
198    ///             }
199    ///
200    ///             let dx = point.x - target.x;
201    ///             let dy = point.y - target.y;
202    ///             let dz = point.z - target.z;
203    ///             let distance = (dx*dx + dy*dy + dz*dz).sqrt();
204    ///
205    ///             // Convert distance to fitness (closer = higher fitness)
206    ///             let fitness = 1.0 / (1.0 + distance);
207    ///             Ok(fitness)
208    ///         })
209    ///     }
210    /// }
211    ///
212    /// # async fn example(builder: ServiceBuilder) -> Result<ServiceBuilder, Box<dyn std::error::Error>> {
213    /// let evaluator = DistanceEvaluator {
214    ///     target: Point3D { x: 5.0, y: -2.0, z: 10.0 }
215    /// };
216    /// let builder = builder.register::<Point3D, _>(evaluator).await?;
217    /// # Ok(builder)
218    /// # }
219    /// ```
220    ///
221    /// ## Combinatorial/Discrete Problems
222    /// ```rust,no_run
223    /// use fx_durable_ga::models::*;
224    /// # use fx_durable_ga::optimization::ServiceBuilder;
225    /// # use futures::future::BoxFuture;
226    ///
227    /// #[derive(Debug, Clone)]
228    /// struct Schedule {
229    ///     assignments: Vec<i64>,  // job assignments to workers
230    /// }
231    ///
232    /// impl Encodeable for Schedule {
233    ///     const NAME: &'static str = "Schedule";
234    ///     type Phenotype = Schedule;
235    ///
236    ///     fn morphology() -> Vec<GeneBounds> {
237    ///         // 10 jobs, each can be assigned to 5 workers (0-4)
238    ///         (0..10)
239    ///             .map(|_| GeneBounds::integer(0, 4, 5).unwrap())
240    ///             .collect()
241    ///     }
242    ///
243    ///     fn encode(&self) -> Vec<i64> {
244    ///         self.assignments.clone()
245    ///     }
246    ///
247    ///     fn decode(genes: &[i64]) -> Self::Phenotype {
248    ///         Schedule {
249    ///             assignments: genes.to_vec()
250    ///         }
251    ///     }
252    /// }
253    ///
254    /// struct ScheduleEvaluator {
255    ///     job_costs: Vec<Vec<f64>>,  // job_costs[job][worker] = cost
256    /// }
257    ///
258    /// impl Evaluator<Schedule> for ScheduleEvaluator {
259    ///     fn fitness<'a>(
260    ///         &self,
261    ///         _genotype_id: uuid::Uuid,
262    ///         schedule: Schedule,
263    ///         _request: &Request,
264    ///         _terminated: &'a Box<dyn Terminated>,
265    ///     ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
266    ///         let costs = self.job_costs.clone();
267    ///         Box::pin(async move {
268    ///             let total_cost: f64 = schedule.assignments
269    ///                 .iter()
270    ///                 .enumerate()
271    ///                 .map(|(job, &worker)| costs[job][worker as usize])
272    ///                 .sum();
273    ///
274    ///             // Lower cost = higher fitness
275    ///             let max_possible_cost = 1000.0;
276    ///             let fitness = (max_possible_cost - total_cost) / max_possible_cost;
277    ///             Ok(fitness.max(0.0))
278    ///         })
279    ///     }
280    /// }
281    ///
282    /// # async fn example(builder: ServiceBuilder) -> Result<ServiceBuilder, Box<dyn std::error::Error>> {
283    /// let evaluator = ScheduleEvaluator {
284    ///     job_costs: vec![vec![10.0, 20.0, 15.0, 25.0, 12.0]; 10]
285    /// };
286    /// let builder = builder.register::<Schedule, _>(evaluator).await?;
287    /// # Ok(builder)
288    /// # }
289    /// ```
290    ///
291    /// # Best Practices
292    ///
293    /// ## Morphology Design
294    /// - **Use appropriate bounds**: Match your problem's natural constraints
295    /// - **Balance precision vs. search space**: More precision = larger search space
296    /// - **Consider gene interactions**: Related parameters should be adjacent when possible
297    /// - **Use meaningful step sizes**: Align with your problem's natural granularity
298    ///
299    /// ## Fitness Function Design
300    /// - **Use natural value ranges**: Return raw values like loss, accuracy, cost, profit
301    /// - **Handle edge cases gracefully**: Return appropriate default values for invalid solutions
302    /// - **Check termination regularly**: Use `terminated.is_terminated()` in long evaluations
303    /// - **Avoid fitness cliffs**: Gradual fitness landscapes converge better than binary ones
304    /// - **Return errors for true failures**: Let the system handle evaluation failures
305    ///
306    /// ## Performance Considerations
307    /// - **Minimize evaluation cost**: Fitness evaluation is the bottleneck
308    /// - **Use caching when appropriate**: Cache expensive computations
309    /// - **Parallelize evaluations**: The system handles concurrent fitness evaluations
310    /// - **Profile your evaluator**: Identify and optimize slow paths
311    ///
312    /// # Registration Order
313    ///
314    /// Multiple types can be registered with the same service:
315    /// ```rust,no_run
316    /// # use fx_durable_ga::optimization::ServiceBuilder;
317    /// # struct TypeA; struct TypeB; struct EvaluatorA; struct EvaluatorB;
318    /// # impl fx_durable_ga::models::Encodeable for TypeA {
319    /// #     const NAME: &'static str = "TypeA"; type Phenotype = TypeA;
320    /// #     fn morphology() -> Vec<fx_durable_ga::models::GeneBounds> { vec![] }
321    /// #     fn encode(&self) -> Vec<i64> { vec![] }
322    /// #     fn decode(_: &[i64]) -> Self::Phenotype { TypeA }
323    /// # }
324    /// # impl fx_durable_ga::models::Encodeable for TypeB {
325    /// #     const NAME: &'static str = "TypeB"; type Phenotype = TypeB;
326    /// #     fn morphology() -> Vec<fx_durable_ga::models::GeneBounds> { vec![] }
327    /// #     fn encode(&self) -> Vec<i64> { vec![] }
328    /// #     fn decode(_: &[i64]) -> Self::Phenotype { TypeB }
329    /// # }
330    /// # impl fx_durable_ga::models::Evaluator<TypeA> for EvaluatorA {
331    /// #     fn fitness<'a>(&self, _: uuid::Uuid, _: TypeA, _request: &fx_durable_ga::models::Request, _: &'a Box<dyn fx_durable_ga::models::Terminated>)
332    /// #         -> futures::future::BoxFuture<'a, Result<f64, anyhow::Error>>
333    /// #         { Box::pin(async move { Ok(0.5) }) }
334    /// # }
335    /// # impl fx_durable_ga::models::Evaluator<TypeB> for EvaluatorB {
336    /// #     fn fitness<'a>(&self, _: uuid::Uuid, _: TypeB, _request: &fx_durable_ga::models::Request, _: &'a Box<dyn fx_durable_ga::models::Terminated>)
337    /// #         -> futures::future::BoxFuture<'a, Result<f64, anyhow::Error>>
338    /// #         { Box::pin(async move { Ok(0.5) }) }
339    /// # }
340    /// # async fn example(builder: ServiceBuilder) -> Result<(), Box<dyn std::error::Error>> {
341    /// let service = builder
342    ///     .register::<TypeA, _>(EvaluatorA).await?
343    ///     .register::<TypeB, _>(EvaluatorB).await?
344    ///     .build();
345    /// # Ok(())
346    /// # }
347    /// ```
348    ///
349    /// # Returns
350    ///
351    /// Returns `Ok(Self)` if registration succeeds, allowing method chaining.
352    /// The morphology is created in the database if it doesn't exist.
353    ///
354    /// # Errors
355    ///
356    /// - Database connection or transaction failures
357    /// - Morphology creation failures (invalid gene bounds)
358    /// - Type hash collisions (extremely rare with different type names)
359    ///
360    /// # Example Usage
361    ///
362    /// ```rust,no_run
363    /// use fx_durable_ga::{bootstrap, models::*};
364    /// # use futures::future::BoxFuture;
365    /// # use sqlx::PgPool;
366    ///
367    /// #[derive(Debug, Clone)]
368    /// struct OptimizationTarget {
369    ///     value: f64,
370    /// }
371    ///
372    /// impl Encodeable for OptimizationTarget {
373    ///     const NAME: &'static str = "OptimizationTarget";
374    ///     type Phenotype = OptimizationTarget;
375    ///
376    ///     fn morphology() -> Vec<GeneBounds> {
377    ///         vec![GeneBounds::decimal(-100.0, 100.0, 10000, 4).unwrap()]
378    ///     }
379    ///
380    ///     fn encode(&self) -> Vec<i64> {
381    ///         let bounds = Self::morphology();
382    ///         vec![bounds[0].from_sample((self.value + 100.0) / 200.0)]
383    ///     }
384    ///
385    ///     fn decode(genes: &[i64]) -> Self::Phenotype {
386    ///         let bounds = Self::morphology();
387    ///         OptimizationTarget {
388    ///             value: bounds[0].decode_f64(genes[0]) * 200.0 - 100.0
389    ///         }
390    ///     }
391    /// }
392    ///
393    /// struct QuadraticEvaluator;
394    ///
395    /// impl Evaluator<OptimizationTarget> for QuadraticEvaluator {
396    ///     fn fitness<'a>(
397    ///         &self,
398    ///         _genotype_id: uuid::Uuid,
399    ///         target: OptimizationTarget,
400    ///         _request: &Request,
401    ///         _terminated: &'a Box<dyn Terminated>,
402    ///     ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
403    ///         Box::pin(async move {
404    ///             // Maximize -(x-10)^2 + 100, so optimal x = 10
405    ///             let fitness = -(target.value - 10.0).powi(2) + 100.0;
406    ///             Ok(fitness)  // Return raw fitness value
407    ///         })
408    ///     }
409    /// }
410    ///
411    /// # async fn example(pool: PgPool) -> Result<(), Box<dyn std::error::Error>> {
412    /// let service = bootstrap(pool)
413    ///     .await?
414    ///     .register::<OptimizationTarget, _>(QuadraticEvaluator)
415    ///     .await?
416    ///     .build();
417    /// # Ok(())
418    /// # }
419    /// ```
420    #[instrument(level = "debug", skip(self, evaluator), fields(type_name = T::NAME, type_hash = T::HASH))]
421    pub async fn register<T, E>(mut self, evaluator: E) -> Result<Self, super::Error>
422    where
423        T: Encodeable + 'static,
424        E: Evaluator<T::Phenotype> + Send + Sync + 'static,
425    {
426        // Insert the morphology of the type if it does not already exist in the database
427        if let Err(morphologies::Error::NotFound) = self.morphologies.get_morphology(T::HASH).await
428        {
429            self.morphologies
430                .new_morphology(Morphology::new(T::NAME, T::HASH, T::morphology()))
431                .await?;
432        }
433
434        // Erase the type and store the evaluator
435        let erased = super::models::ErasedEvaluator::new(evaluator, T::decode);
436        self.evaluators.insert(T::HASH, Box::new(erased));
437
438        Ok(self)
439    }
440
441    /// Sets the maximum number of deduplication attempts when breeding genotypes.
442    pub fn with_max_deduplication_attempts(mut self, attempts: i32) -> Self {
443        self.max_deduplication_attempts = attempts;
444        self
445    }
446
447    /// Builds the optimization service with all registered evaluators.
448    #[instrument(level = "debug", skip(self), fields(evaluators_count = self.evaluators.len()))]
449    pub fn build(self) -> Service {
450        Service {
451            locking: self.locking,
452            requests: self.requests,
453            morphologies: self.morphologies,
454            genotypes: self.genotypes,
455            evaluators: self.evaluators,
456            max_deduplication_attempts: self.max_deduplication_attempts,
457        }
458    }
459}