Skip to main content

ServiceBuilder

Struct ServiceBuilder 

Source
pub struct ServiceBuilder { /* private fields */ }
Expand description

Builder for creating optimization services with registered type evaluators. Handles morphology registration and evaluator type erasure.

Implementations§

Source§

impl ServiceBuilder

Source

pub async fn register<T, E>(self, evaluator: E) -> Result<Self, Error>
where T: Encodeable + 'static, E: Evaluator<T::Phenotype> + Send + Sync + 'static,

Registers a problem type and its fitness evaluator with the optimization service.

This is the core method for connecting your domain-specific problems to the genetic algorithm engine. It establishes the relationship between your data structures, their genetic encoding, and the fitness evaluation logic.

§Core Concepts
§Type Registration Process
  1. Morphology Creation: Automatically creates and stores the search space definition
  2. Type Erasure: Converts your specific evaluator into a type-erased form for internal storage
  3. Hash Mapping: Associates your type’s hash with its evaluator for efficient lookup
  4. Database Integration: Ensures the type’s morphology exists in persistent storage
§Required Trait Implementations
  • Encodeable: Defines how your type maps to/from genetic representation
  • Evaluator: Provides the fitness function that guides evolution
§Parameters
  • evaluator - Your fitness evaluator implementing Evaluator<T::Phenotype>
§Type Parameters
  • T: Encodeable - Your problem type that can be encoded as genes
  • E: Evaluator<T::Phenotype> - Your fitness evaluator for decoded phenotypes
§Encodeable Implementation Requirements

Your type must implement Encodeable with:

  • NAME: Unique string identifier for your type
  • Phenotype: The decoded form passed to evaluators
  • morphology(): Defines the search space bounds and structure
  • encode(): Converts instances to genetic representation
  • decode(): Converts genes back to phenotypes for evaluation
§Evaluator Implementation Requirements

Your evaluator must implement Evaluator<T::Phenotype> with:

  • fitness(): Async function returning fitness scores (any finite f64 values)
  • Support for early termination via the Terminated trait
  • Error handling for failed evaluations
§Common Registration Patterns
§Simple Numerical Optimization
use fx_durable_ga::models::*;

#[derive(Debug, Clone)]
struct Parameters {
    learning_rate: f64,
    batch_size: i64,
    layers: i64,
}

impl Encodeable for Parameters {
    const NAME: &'static str = "Parameters";
    type Phenotype = Parameters;

    fn morphology() -> Vec<GeneBounds> {
        vec![
            GeneBounds::decimal(0.001, 1.0, 1000, 3).unwrap(),  // learning_rate
            GeneBounds::integer(8, 512, 32).unwrap(),            // batch_size
            GeneBounds::integer(1, 10, 10).unwrap(),             // layers
        ]
    }

    fn encode(&self) -> Vec<i64> {
        let bounds = Self::morphology();
        vec![
            bounds[0].from_sample(self.learning_rate / 1.0),
            bounds[1].from_sample((self.batch_size - 8) as f64 / (512 - 8) as f64),
            bounds[2].from_sample((self.layers - 1) as f64 / 9.0),
        ]
    }

    fn decode(genes: &[i64]) -> Self::Phenotype {
        let bounds = Self::morphology();
        Parameters {
            learning_rate: bounds[0].decode_f64(genes[0]),
            batch_size: (bounds[1].decode_f64(genes[1]) * (512 - 8) as f64 + 8.0) as i64,
            layers: (bounds[2].decode_f64(genes[2]) * 9.0 + 1.0) as i64,
        }
    }
}

struct ModelEvaluator;

impl Evaluator<Parameters> for ModelEvaluator {
    fn fitness<'a>(
        &self,
        _genotype_id: uuid::Uuid,
        params: Parameters,
        _request: &Request,
        _terminated: &'a Box<dyn Terminated>,
    ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
        Box::pin(async move {
            // Train model with parameters and return accuracy
            let accuracy = train_model(params.learning_rate, params.batch_size, params.layers).await?;
            Ok(accuracy)
        })
    }
}

let builder = builder.register::<Parameters, _>(ModelEvaluator).await?;
§Geometric/Spatial Problems
use fx_durable_ga::models::*;

#[derive(Debug, Clone)]
struct Point3D {
    x: f64,
    y: f64,
    z: f64,
}

impl Encodeable for Point3D {
    const NAME: &'static str = "Point3D";
    type Phenotype = Point3D;

    fn morphology() -> Vec<GeneBounds> {
        vec![
            GeneBounds::decimal(-10.0, 10.0, 2000, 3).unwrap(),  // x: -10.0 to 10.0
            GeneBounds::decimal(-10.0, 10.0, 2000, 3).unwrap(),  // y: -10.0 to 10.0
            GeneBounds::decimal(0.0, 20.0, 2000, 3).unwrap(),    // z: 0.0 to 20.0
        ]
    }

    fn encode(&self) -> Vec<i64> {
        let bounds = Self::morphology();
        vec![
            bounds[0].from_sample((self.x + 10.0) / 20.0),
            bounds[1].from_sample((self.y + 10.0) / 20.0),
            bounds[2].from_sample(self.z / 20.0),
        ]
    }

    fn decode(genes: &[i64]) -> Self::Phenotype {
        let bounds = Self::morphology();
        Point3D {
            x: bounds[0].decode_f64(genes[0]) * 20.0 - 10.0,
            y: bounds[1].decode_f64(genes[1]) * 20.0 - 10.0,
            z: bounds[2].decode_f64(genes[2]) * 20.0,
        }
    }
}

struct DistanceEvaluator {
    target: Point3D,
}

impl Evaluator<Point3D> for DistanceEvaluator {
    fn fitness<'a>(
        &self,
        _genotype_id: uuid::Uuid,
        point: Point3D,
        _request: &Request,
        terminated: &'a Box<dyn Terminated>,
    ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
        let target = self.target.clone();
        Box::pin(async move {
            // Check for early termination
            if terminated.is_terminated().await {
                return Err(anyhow::anyhow!("Evaluation terminated"));
            }

            let dx = point.x - target.x;
            let dy = point.y - target.y;
            let dz = point.z - target.z;
            let distance = (dx*dx + dy*dy + dz*dz).sqrt();

            // Convert distance to fitness (closer = higher fitness)
            let fitness = 1.0 / (1.0 + distance);
            Ok(fitness)
        })
    }
}

let evaluator = DistanceEvaluator {
    target: Point3D { x: 5.0, y: -2.0, z: 10.0 }
};
let builder = builder.register::<Point3D, _>(evaluator).await?;
§Combinatorial/Discrete Problems
use fx_durable_ga::models::*;

#[derive(Debug, Clone)]
struct Schedule {
    assignments: Vec<i64>,  // job assignments to workers
}

impl Encodeable for Schedule {
    const NAME: &'static str = "Schedule";
    type Phenotype = Schedule;

    fn morphology() -> Vec<GeneBounds> {
        // 10 jobs, each can be assigned to 5 workers (0-4)
        (0..10)
            .map(|_| GeneBounds::integer(0, 4, 5).unwrap())
            .collect()
    }

    fn encode(&self) -> Vec<i64> {
        self.assignments.clone()
    }

    fn decode(genes: &[i64]) -> Self::Phenotype {
        Schedule {
            assignments: genes.to_vec()
        }
    }
}

struct ScheduleEvaluator {
    job_costs: Vec<Vec<f64>>,  // job_costs[job][worker] = cost
}

impl Evaluator<Schedule> for ScheduleEvaluator {
    fn fitness<'a>(
        &self,
        _genotype_id: uuid::Uuid,
        schedule: Schedule,
        _request: &Request,
        _terminated: &'a Box<dyn Terminated>,
    ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
        let costs = self.job_costs.clone();
        Box::pin(async move {
            let total_cost: f64 = schedule.assignments
                .iter()
                .enumerate()
                .map(|(job, &worker)| costs[job][worker as usize])
                .sum();

            // Lower cost = higher fitness
            let max_possible_cost = 1000.0;
            let fitness = (max_possible_cost - total_cost) / max_possible_cost;
            Ok(fitness.max(0.0))
        })
    }
}

let evaluator = ScheduleEvaluator {
    job_costs: vec![vec![10.0, 20.0, 15.0, 25.0, 12.0]; 10]
};
let builder = builder.register::<Schedule, _>(evaluator).await?;
§Best Practices
§Morphology Design
  • Use appropriate bounds: Match your problem’s natural constraints
  • Balance precision vs. search space: More precision = larger search space
  • Consider gene interactions: Related parameters should be adjacent when possible
  • Use meaningful step sizes: Align with your problem’s natural granularity
§Fitness Function Design
  • Use natural value ranges: Return raw values like loss, accuracy, cost, profit
  • Handle edge cases gracefully: Return appropriate default values for invalid solutions
  • Check termination regularly: Use terminated.is_terminated() in long evaluations
  • Avoid fitness cliffs: Gradual fitness landscapes converge better than binary ones
  • Return errors for true failures: Let the system handle evaluation failures
§Performance Considerations
  • Minimize evaluation cost: Fitness evaluation is the bottleneck
  • Use caching when appropriate: Cache expensive computations
  • Parallelize evaluations: The system handles concurrent fitness evaluations
  • Profile your evaluator: Identify and optimize slow paths
§Registration Order

Multiple types can be registered with the same service:

let service = builder
    .register::<TypeA, _>(EvaluatorA).await?
    .register::<TypeB, _>(EvaluatorB).await?
    .build();
§Returns

Returns Ok(Self) if registration succeeds, allowing method chaining. The morphology is created in the database if it doesn’t exist.

§Errors
  • Database connection or transaction failures
  • Morphology creation failures (invalid gene bounds)
  • Type hash collisions (extremely rare with different type names)
§Example Usage
use fx_durable_ga::{bootstrap, models::*};

#[derive(Debug, Clone)]
struct OptimizationTarget {
    value: f64,
}

impl Encodeable for OptimizationTarget {
    const NAME: &'static str = "OptimizationTarget";
    type Phenotype = OptimizationTarget;

    fn morphology() -> Vec<GeneBounds> {
        vec![GeneBounds::decimal(-100.0, 100.0, 10000, 4).unwrap()]
    }

    fn encode(&self) -> Vec<i64> {
        let bounds = Self::morphology();
        vec![bounds[0].from_sample((self.value + 100.0) / 200.0)]
    }

    fn decode(genes: &[i64]) -> Self::Phenotype {
        let bounds = Self::morphology();
        OptimizationTarget {
            value: bounds[0].decode_f64(genes[0]) * 200.0 - 100.0
        }
    }
}

struct QuadraticEvaluator;

impl Evaluator<OptimizationTarget> for QuadraticEvaluator {
    fn fitness<'a>(
        &self,
        _genotype_id: uuid::Uuid,
        target: OptimizationTarget,
        _request: &Request,
        _terminated: &'a Box<dyn Terminated>,
    ) -> BoxFuture<'a, Result<f64, anyhow::Error>> {
        Box::pin(async move {
            // Maximize -(x-10)^2 + 100, so optimal x = 10
            let fitness = -(target.value - 10.0).powi(2) + 100.0;
            Ok(fitness)  // Return raw fitness value
        })
    }
}

let service = bootstrap(pool)
    .await?
    .register::<OptimizationTarget, _>(QuadraticEvaluator)
    .await?
    .build();
Examples found in repository?
examples/regression_model.rs (line 215)
195async fn main() -> Result<()> {
196    dotenv::from_filename(".env.local").ok();
197    tracing_subscriber::fmt()
198        .pretty()
199        .with_thread_ids(true)
200        .with_max_level(Level::INFO)
201        .init();
202
203    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
204    let pool = PgPoolOptions::new()
205        .max_connections(10)
206        .connect(&database_url)
207        .await?;
208
209    // Run all default migrations
210    fx_durable_ga::migrations::run_default_migrations(&pool).await?;
211
212    let service = Arc::new(
213        bootstrap(pool.clone())
214            .await?
215            .register::<NeuralArchitecture, _>(ArchitectureEvaluator)
216            .await?
217            .build(),
218    );
219
220    let mut registry = fx_event_bus::EventHandlerRegistry::new();
221    register_event_handlers(
222        Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
223        service.clone(),
224        &mut registry,
225    );
226    let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
227    tokio::spawn(async move { listener.listen(None).await });
228
229    let host_id = Uuid::parse_str("00000000-0000-0000-0000-123456789abc")?;
230    let mut jobs_listener = fx_mq_jobs::Listener::new(
231        pool.clone(),
232        register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
233        WORKERS,
234        host_id,
235        Duration::from_secs(600),
236    )
237    .await?;
238    tokio::spawn(async move { jobs_listener.listen().await });
239
240    service
241        .new_optimization_request(
242            NeuralArchitecture::NAME,
243            NeuralArchitecture::HASH,
244            FitnessGoal::minimize(FITNESS_TARGET)?,
245            Schedule::generational(10, 10),
246            Selector::tournament(5, 15)?,
247            Mutagen::new(Temperature::constant(0.8)?, MutationRate::constant(0.4)?),
248            Crossover::uniform(0.5)?,
249            Distribution::latin_hypercube(15),
250            None::<()>,
251        )
252        .await?;
253
254    tokio::time::sleep(Duration::from_secs(3600)).await;
255    Ok(())
256}
More examples
Hide additional examples
examples/point_search.rs (line 158)
130async fn main() -> Result<()> {
131    dotenv::from_filename(".env.local").ok();
132
133    // Initialize logging to see optimization progress
134    tracing_subscriber::fmt()
135        .with_max_level(tracing::Level::INFO)
136        .init();
137
138    // Database setup - genetic algorithms need persistent storage for populations
139    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
140    let pool = PgPoolOptions::new()
141        .max_connections(50)
142        .connect(&database_url)
143        .await?;
144
145    fx_event_bus::run_migrations(&pool).await?;
146    fx_mq_jobs::run_migrations(&pool, fx_mq_jobs::FX_MQ_JOBS_SCHEMA_NAME).await?;
147
148    // Bootstrap the optimization service and register our problem type
149    let target_point = Point {
150        x: 1.0,
151        y: 1.5,
152        z: 2.5,
153    }; // Target point to find
154    let evaluator = PointDistanceEvaluator { target_point };
155    let service = Arc::new(
156        bootstrap(pool.clone())
157            .await?
158            .register::<Point, _>(evaluator)
159            .await?
160            .build(),
161    );
162
163    // setup event handling and spawn an event handling agent
164    let mut registry = fx_event_bus::EventHandlerRegistry::new();
165    register_event_handlers(
166        Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
167        service.clone(),
168        &mut registry,
169    );
170    let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
171    let _events_handle = tokio::spawn(async move {
172        listener.listen(None).await?;
173        Ok::<(), sqlx::Error>(())
174    });
175
176    // setup job handling and initiate workers
177    let host_id = Uuid::parse_str("ba3a4752-c4ce-4129-aa1d-55a0b2107e68").expect("valid uuid");
178    let hold_for = Duration::from_secs(TIMEOUT_SECONDS);
179    let mut jobs_listener = fx_mq_jobs::Listener::new(
180        pool.clone(),
181        register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
182        8,
183        host_id,
184        hold_for,
185    )
186    .await?;
187    let _jobs_handle = tokio::spawn(async move {
188        jobs_listener.listen().await?;
189        Ok::<(), anyhow::Error>(())
190    });
191
192    // Create multiple optimization requests with different genetic algorithm parameters
193    for _ in 0..1 {
194        service
195            .new_optimization_request(
196                Point::NAME,
197                Point::HASH,
198                FitnessGoal::minimize(0.01)?, // Stop when distance ≤ 0.01
199                Schedule::generational(200, 30),
200                Selector::tournament(7, 100)?,
201                Mutagen::new(Temperature::constant(0.7)?, MutationRate::constant(0.3)?),
202                Crossover::uniform(0.5)?,
203                Distribution::latin_hypercube(1000),
204                None::<()>,
205            )
206            .await?;
207    }
208
209    let timeout_duration = Duration::from_secs(TIMEOUT_SECONDS);
210    let start_time = std::time::Instant::now();
211
212    loop {
213        tokio::time::sleep(Duration::from_secs(1)).await;
214
215        // Check if we've exceeded the timeout
216        if start_time.elapsed() >= timeout_duration {
217            println!(
218                "Timeout reached after {} seconds. Stopping optimization.",
219                timeout_duration.as_secs()
220            );
221            break;
222        }
223    }
224
225    Ok(())
226}
examples/feature_engineering.rs (line 397)
377async fn main() -> Result<()> {
378    dotenv::from_filename(".env.local").ok();
379    tracing_subscriber::fmt()
380        .pretty()
381        .with_thread_ids(true)
382        .with_max_level(Level::INFO)
383        .init();
384
385    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
386    let pool = PgPoolOptions::new()
387        .max_connections(10)
388        .connect(&database_url)
389        .await?;
390
391    // Run all default migrations
392    fx_durable_ga::migrations::run_default_migrations(&pool).await?;
393
394    let service = Arc::new(
395        bootstrap(pool.clone())
396            .await?
397            .register::<FeatureConfig, _>(FeatureEvaluator)
398            .await?
399            .build(),
400    );
401
402    let mut registry = fx_event_bus::EventHandlerRegistry::new();
403    register_event_handlers(
404        Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
405        service.clone(),
406        &mut registry,
407    );
408    let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
409    tokio::spawn(async move { listener.listen(None).await });
410
411    let host_id = Uuid::parse_str("00000000-0000-0000-0000-123456789abc")?;
412    let mut jobs_listener = fx_mq_jobs::Listener::new(
413        pool.clone(),
414        register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
415        WORKERS,
416        host_id,
417        Duration::from_secs(600),
418    )
419    .await?;
420    tokio::spawn(async move { jobs_listener.listen().await });
421
422    let request_id = service
423        .new_optimization_request(
424            FeatureConfig::NAME,
425            FeatureConfig::HASH,
426            FitnessGoal::minimize(FITNESS_TARGET)?,
427            Schedule::generational(40, 10),
428            Selector::tournament(5, 45)?,
429            Mutagen::new(Temperature::constant(0.7)?, MutationRate::constant(0.35)?),
430            Crossover::uniform(0.5)?,
431            Distribution::latin_hypercube(40),
432            None::<()>,
433        )
434        .await?;
435
436    // Poll for completion every 15 seconds (timeout after 1 hour)
437    let timeout = Duration::from_secs(7200);
438    let poll_interval = Duration::from_secs(15);
439    let start = std::time::Instant::now();
440
441    loop {
442        if service.is_request_concluded(request_id).await? {
443            println!("\nOptimization completed!");
444            break;
445        }
446
447        if start.elapsed() > timeout {
448            println!("\nOptimization timed out after 1 hour.");
449            break;
450        }
451
452        tokio::time::sleep(poll_interval).await;
453    }
454
455    // Get and print the best configuration
456    if let Some((genotype, fitness)) = service.get_best_genotype(request_id).await? {
457        let config = FeatureConfig::decode(&genotype.genome());
458        println!("\n=== Best Configuration ===");
459        println!("Fitness (MSE): {:.6}", fitness);
460        println!("RMSE: {:.6}°C", fitness.sqrt());
461        println!("\nHyperparameters:");
462        println!("  Hidden Size: {}", config.hidden_size);
463        println!("  Learning Rate: {}", config.learning_rate);
464        println!("  Sequence Length: {}", config.sequence_length);
465        println!("\nFeatures:");
466        for (i, feature) in config.features.iter().enumerate() {
467            let pipeline = feature
468                .transforms
469                .iter()
470                .map(|t| t.to_string())
471                .collect::<Vec<_>>()
472                .join(" → ");
473            if pipeline.is_empty() {
474                println!("  feat_{}: {}", i, feature.source);
475            } else {
476                println!("  feat_{}: {} → {}", i, feature.source, pipeline);
477            }
478        }
479    } else {
480        println!("No genotypes were evaluated.");
481    }
482
483    Ok(())
484}
Source

pub fn with_max_deduplication_attempts(self, attempts: i32) -> Self

Sets the maximum number of deduplication attempts when breeding genotypes.

Source

pub fn build(self) -> Service

Builds the optimization service with all registered evaluators.

Examples found in repository?
examples/regression_model.rs (line 217)
195async fn main() -> Result<()> {
196    dotenv::from_filename(".env.local").ok();
197    tracing_subscriber::fmt()
198        .pretty()
199        .with_thread_ids(true)
200        .with_max_level(Level::INFO)
201        .init();
202
203    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
204    let pool = PgPoolOptions::new()
205        .max_connections(10)
206        .connect(&database_url)
207        .await?;
208
209    // Run all default migrations
210    fx_durable_ga::migrations::run_default_migrations(&pool).await?;
211
212    let service = Arc::new(
213        bootstrap(pool.clone())
214            .await?
215            .register::<NeuralArchitecture, _>(ArchitectureEvaluator)
216            .await?
217            .build(),
218    );
219
220    let mut registry = fx_event_bus::EventHandlerRegistry::new();
221    register_event_handlers(
222        Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
223        service.clone(),
224        &mut registry,
225    );
226    let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
227    tokio::spawn(async move { listener.listen(None).await });
228
229    let host_id = Uuid::parse_str("00000000-0000-0000-0000-123456789abc")?;
230    let mut jobs_listener = fx_mq_jobs::Listener::new(
231        pool.clone(),
232        register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
233        WORKERS,
234        host_id,
235        Duration::from_secs(600),
236    )
237    .await?;
238    tokio::spawn(async move { jobs_listener.listen().await });
239
240    service
241        .new_optimization_request(
242            NeuralArchitecture::NAME,
243            NeuralArchitecture::HASH,
244            FitnessGoal::minimize(FITNESS_TARGET)?,
245            Schedule::generational(10, 10),
246            Selector::tournament(5, 15)?,
247            Mutagen::new(Temperature::constant(0.8)?, MutationRate::constant(0.4)?),
248            Crossover::uniform(0.5)?,
249            Distribution::latin_hypercube(15),
250            None::<()>,
251        )
252        .await?;
253
254    tokio::time::sleep(Duration::from_secs(3600)).await;
255    Ok(())
256}
More examples
Hide additional examples
examples/point_search.rs (line 160)
130async fn main() -> Result<()> {
131    dotenv::from_filename(".env.local").ok();
132
133    // Initialize logging to see optimization progress
134    tracing_subscriber::fmt()
135        .with_max_level(tracing::Level::INFO)
136        .init();
137
138    // Database setup - genetic algorithms need persistent storage for populations
139    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
140    let pool = PgPoolOptions::new()
141        .max_connections(50)
142        .connect(&database_url)
143        .await?;
144
145    fx_event_bus::run_migrations(&pool).await?;
146    fx_mq_jobs::run_migrations(&pool, fx_mq_jobs::FX_MQ_JOBS_SCHEMA_NAME).await?;
147
148    // Bootstrap the optimization service and register our problem type
149    let target_point = Point {
150        x: 1.0,
151        y: 1.5,
152        z: 2.5,
153    }; // Target point to find
154    let evaluator = PointDistanceEvaluator { target_point };
155    let service = Arc::new(
156        bootstrap(pool.clone())
157            .await?
158            .register::<Point, _>(evaluator)
159            .await?
160            .build(),
161    );
162
163    // setup event handling and spawn an event handling agent
164    let mut registry = fx_event_bus::EventHandlerRegistry::new();
165    register_event_handlers(
166        Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
167        service.clone(),
168        &mut registry,
169    );
170    let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
171    let _events_handle = tokio::spawn(async move {
172        listener.listen(None).await?;
173        Ok::<(), sqlx::Error>(())
174    });
175
176    // setup job handling and initiate workers
177    let host_id = Uuid::parse_str("ba3a4752-c4ce-4129-aa1d-55a0b2107e68").expect("valid uuid");
178    let hold_for = Duration::from_secs(TIMEOUT_SECONDS);
179    let mut jobs_listener = fx_mq_jobs::Listener::new(
180        pool.clone(),
181        register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
182        8,
183        host_id,
184        hold_for,
185    )
186    .await?;
187    let _jobs_handle = tokio::spawn(async move {
188        jobs_listener.listen().await?;
189        Ok::<(), anyhow::Error>(())
190    });
191
192    // Create multiple optimization requests with different genetic algorithm parameters
193    for _ in 0..1 {
194        service
195            .new_optimization_request(
196                Point::NAME,
197                Point::HASH,
198                FitnessGoal::minimize(0.01)?, // Stop when distance ≤ 0.01
199                Schedule::generational(200, 30),
200                Selector::tournament(7, 100)?,
201                Mutagen::new(Temperature::constant(0.7)?, MutationRate::constant(0.3)?),
202                Crossover::uniform(0.5)?,
203                Distribution::latin_hypercube(1000),
204                None::<()>,
205            )
206            .await?;
207    }
208
209    let timeout_duration = Duration::from_secs(TIMEOUT_SECONDS);
210    let start_time = std::time::Instant::now();
211
212    loop {
213        tokio::time::sleep(Duration::from_secs(1)).await;
214
215        // Check if we've exceeded the timeout
216        if start_time.elapsed() >= timeout_duration {
217            println!(
218                "Timeout reached after {} seconds. Stopping optimization.",
219                timeout_duration.as_secs()
220            );
221            break;
222        }
223    }
224
225    Ok(())
226}
examples/feature_engineering.rs (line 399)
377async fn main() -> Result<()> {
378    dotenv::from_filename(".env.local").ok();
379    tracing_subscriber::fmt()
380        .pretty()
381        .with_thread_ids(true)
382        .with_max_level(Level::INFO)
383        .init();
384
385    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
386    let pool = PgPoolOptions::new()
387        .max_connections(10)
388        .connect(&database_url)
389        .await?;
390
391    // Run all default migrations
392    fx_durable_ga::migrations::run_default_migrations(&pool).await?;
393
394    let service = Arc::new(
395        bootstrap(pool.clone())
396            .await?
397            .register::<FeatureConfig, _>(FeatureEvaluator)
398            .await?
399            .build(),
400    );
401
402    let mut registry = fx_event_bus::EventHandlerRegistry::new();
403    register_event_handlers(
404        Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
405        service.clone(),
406        &mut registry,
407    );
408    let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
409    tokio::spawn(async move { listener.listen(None).await });
410
411    let host_id = Uuid::parse_str("00000000-0000-0000-0000-123456789abc")?;
412    let mut jobs_listener = fx_mq_jobs::Listener::new(
413        pool.clone(),
414        register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
415        WORKERS,
416        host_id,
417        Duration::from_secs(600),
418    )
419    .await?;
420    tokio::spawn(async move { jobs_listener.listen().await });
421
422    let request_id = service
423        .new_optimization_request(
424            FeatureConfig::NAME,
425            FeatureConfig::HASH,
426            FitnessGoal::minimize(FITNESS_TARGET)?,
427            Schedule::generational(40, 10),
428            Selector::tournament(5, 45)?,
429            Mutagen::new(Temperature::constant(0.7)?, MutationRate::constant(0.35)?),
430            Crossover::uniform(0.5)?,
431            Distribution::latin_hypercube(40),
432            None::<()>,
433        )
434        .await?;
435
436    // Poll for completion every 15 seconds (timeout after 1 hour)
437    let timeout = Duration::from_secs(7200);
438    let poll_interval = Duration::from_secs(15);
439    let start = std::time::Instant::now();
440
441    loop {
442        if service.is_request_concluded(request_id).await? {
443            println!("\nOptimization completed!");
444            break;
445        }
446
447        if start.elapsed() > timeout {
448            println!("\nOptimization timed out after 1 hour.");
449            break;
450        }
451
452        tokio::time::sleep(poll_interval).await;
453    }
454
455    // Get and print the best configuration
456    if let Some((genotype, fitness)) = service.get_best_genotype(request_id).await? {
457        let config = FeatureConfig::decode(&genotype.genome());
458        println!("\n=== Best Configuration ===");
459        println!("Fitness (MSE): {:.6}", fitness);
460        println!("RMSE: {:.6}°C", fitness.sqrt());
461        println!("\nHyperparameters:");
462        println!("  Hidden Size: {}", config.hidden_size);
463        println!("  Learning Rate: {}", config.learning_rate);
464        println!("  Sequence Length: {}", config.sequence_length);
465        println!("\nFeatures:");
466        for (i, feature) in config.features.iter().enumerate() {
467            let pipeline = feature
468                .transforms
469                .iter()
470                .map(|t| t.to_string())
471                .collect::<Vec<_>>()
472                .join(" → ");
473            if pipeline.is_empty() {
474                println!("  feat_{}: {}", i, feature.source);
475            } else {
476                println!("  feat_{}: {} → {}", i, feature.source, pipeline);
477            }
478        }
479    } else {
480        println!("No genotypes were evaluated.");
481    }
482
483    Ok(())
484}

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more