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}