Skip to main content

point_search/
point_search.rs

1//! # Point Optimization Example
2//!
3//! This example demonstrates how to use the fx_durable_ga crate to solve an optimization problem
4//! using genetic algorithms. We'll optimize 3D coordinates to find points that are closest
5//! to a target point using unbounded fitness values (raw distance).
6//!
7//! ## Key Concepts
8//!
9//! - **Genotype**: The encoded representation (genes as integers)
10//! - **Phenotype**: The decoded representation (3D coordinates as floats)
11//! - **Fitness**: How good a solution is (higher = better)
12//! - **Population**: A collection of candidate solutions
13//! - **Generation**: One iteration of the evolutionary process
14
15use anyhow::Result;
16use fx_durable_ga::{
17    bootstrap,
18    models::{
19        Crossover, Distribution, Encodeable, Evaluator, FitnessGoal, GeneBounds, Mutagen,
20        MutationRate, Request, Schedule, Selector, Temperature, Terminated,
21    },
22    register_event_handlers, register_job_handlers,
23};
24use fx_mq_jobs::FX_MQ_JOBS_SCHEMA_NAME;
25use fx_mq_jobs::Queries;
26use sqlx::postgres::PgPoolOptions;
27use std::time::Duration;
28use std::{env, sync::Arc};
29use uuid::Uuid;
30
31/// A 3D point with x, y, z coordinates.
32///
33/// This struct defines how we encode/decode between genetic representation (integers)
34/// and the actual problem space (floating-point coordinates).
35#[derive(Debug, Copy, Clone)]
36struct Point {
37    x: f64,
38    y: f64,
39    z: f64,
40}
41
42impl Encodeable for Point {
43    const NAME: &'static str = "point";
44
45    type Phenotype = Point;
46
47    /// Defines the search space for each dimension.
48    ///
49    /// Each GeneBounds specifies the range and precision for one coordinate:
50    /// - x: 0.5 to 1.75 with 0.001 precision
51    /// - y: 0.75 to 2.0 with 0.001 precision
52    /// - z: 2.0 to 3.25 with 0.001 precision
53    fn morphology() -> Vec<GeneBounds> {
54        vec![
55            GeneBounds::decimal(0.5, 1.75, 1000, 3).unwrap(), // x: 0.500 to 1.750 (scaled: 500 to 1750)
56            GeneBounds::decimal(0.75, 2.00, 1000, 3).unwrap(), // y: 0.750 to 2.000 (scaled: 750 to 2000)
57            GeneBounds::decimal(2.00, 3.25, 1000, 3).unwrap(), // z: 2.000 to 3.250 (scaled: 2000 to 3250)
58        ]
59    }
60
61    /// Converts this point to genetic representation.
62    ///
63    /// Uses the bounds' encode_f64 method to directly convert coordinates to gene indices.
64    fn encode(&self) -> Vec<i64> {
65        let bounds = Self::morphology();
66        vec![
67            bounds[0]
68                .encode_f64(self.x)
69                .expect("x coordinate within bounds"),
70            bounds[1]
71                .encode_f64(self.y)
72                .expect("y coordinate within bounds"),
73            bounds[2]
74                .encode_f64(self.z)
75                .expect("z coordinate within bounds"),
76        ]
77    }
78
79    /// Converts genotype (integer genes) to phenotype (Point).
80    ///
81    /// Maps integer gene values to floating-point coordinates within the defined bounds.
82    fn decode(genes: &[i64]) -> Self::Phenotype {
83        let bounds = Self::morphology();
84        Point {
85            x: bounds[0].decode_f64(genes[0]), // Convert to actual decimal coordinates
86            y: bounds[1].decode_f64(genes[1]), // e.g., gene 0 → 0.5, gene 999 → 1.75
87            z: bounds[2].decode_f64(genes[2]), // e.g., gene 0 → 2.0, gene 999 → 3.25
88        }
89    }
90}
91
92/// Fitness evaluator that finds points closest to the target point.
93///
94/// Returns the raw distance to target - lower distances are better (minimization problem).
95struct PointDistanceEvaluator {
96    target_point: Point,
97}
98
99impl Evaluator<Point> for PointDistanceEvaluator {
100    /// Returns the raw distance to the target point.
101    ///
102    /// Lower distances are better - this is a minimization problem.
103    fn fitness<'a>(
104        &self,
105        _genotype_id: Uuid,
106        phenotype: Point,
107        _: &Request,
108        terminated: &'a Box<dyn Terminated>,
109    ) -> futures::future::BoxFuture<'a, Result<f64, anyhow::Error>> {
110        let target = self.target_point;
111        Box::pin(async move {
112            if terminated.is_terminated().await {
113                return Ok(f64::MAX); // Return worst possible fitness if terminated
114            }
115
116            // Calculate direct distance to target point
117            let dx = phenotype.x - target.x;
118            let dy = phenotype.y - target.y;
119            let dz = phenotype.z - target.z;
120            let distance = (dx * dx + dy * dy + dz * dz).sqrt();
121
122            Ok(distance) // Raw distance - lower is better
123        })
124    }
125}
126
127const TIMEOUT_SECONDS: u64 = 900;
128
129#[tokio::main]
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}