diff --git a/colonyx/__init__.py b/colonyx/__init__.py new file mode 100644 index 0000000..c82a828 --- /dev/null +++ b/colonyx/__init__.py @@ -0,0 +1,34 @@ +""" +colonyx: Swarm Intelligence Optimization Library + +A Python library for solving optimization problems using swarm intelligence algorithms +like Ant Colony Optimization (ACO), Particle Swarm Optimization (PSO), and +Artificial Bee Colony (ABC) with a scikit-learn compatible interface. +""" + +__version__ = "0.1.0" +__author__ = "Minh, Le Duc" +__email__ = "minh.leduc.0210@gmail.com" + +# Import the main interface +from .auto import AutoColony + +# Import individual algorithms (will be implemented) +# from .algorithms import AntColonyOptimizer, ParticleSwarmOptimizer, ArtificialBeeColonyOptimizer + +# Import utilities (will be implemented) +# from .utils import check_optimization_problem, check_bounds + +# Import datasets (will be implemented) +# from .datasets import load_tsp_data, benchmark_functions + +__all__ = [ + "AutoColony", + # "AntColonyOptimizer", + # "ParticleSwarmOptimizer", + # "ArtificialBeeColonyOptimizer", + # "check_optimization_problem", + # "check_bounds", + # "load_tsp_data", + # "benchmark_functions", +] \ No newline at end of file diff --git a/colonyx/auto.py b/colonyx/auto.py new file mode 100644 index 0000000..0ee6258 --- /dev/null +++ b/colonyx/auto.py @@ -0,0 +1,216 @@ +""" +AutoColony: Main interface for swarm intelligence optimization algorithms +""" + +from typing import Optional, Union, Dict, Any, Callable +import numpy as np +from sklearn.base import BaseEstimator + + +class AutoColony(BaseEstimator): + """ + Unified interface for swarm intelligence optimization algorithms. + + Similar to HuggingFace's AutoModel, this class provides a single interface + for multiple optimization algorithms selected via the 'mode' parameter. + + Parameters + ---------- + mode : str, default='auto' + Algorithm selection mode: + - 'auto': Automatically select algorithm based on problem type + - 'aco': Ant Colony Optimization + - 'pso': Particle Swarm Optimization + - 'abc': Artificial Bee Colony + + n_iterations : int, default=100 + Number of iterations to run + + random_state : int, default=None + Random seed for reproducibility + + **kwargs : dict + Algorithm-specific parameters + """ + + def __init__( + self, + mode: str = 'auto', + n_iterations: int = 100, + random_state: Optional[int] = None, + **kwargs + ): + self.mode = mode + self.n_iterations = n_iterations + self.random_state = random_state + + # Algorithm-specific parameters + self.kwargs = kwargs + + # Internal state + self._fitted = False + self._best_solution = None + self._best_score = None + self._algorithm = None + + # Validate mode + valid_modes = ['auto', 'aco', 'pso', 'abc'] + if mode not in valid_modes: + raise ValueError(f"Invalid mode '{mode}'. Must be one of {valid_modes}") + + def _detect_problem_type(self, X, y=None): + """Auto-detect problem type for algorithm selection""" + if y is not None: + # Supervised learning problem - use PSO + return 'pso' + elif hasattr(X, 'shape') and len(X.shape) == 2: + # Distance matrix (TSP-like) - use ACO + if X.shape[0] == X.shape[1]: + return 'aco' + else: + return 'pso' + else: + # Default to PSO for continuous problems + return 'pso' + + def _create_algorithm(self, algorithm_mode: str): + """Create the appropriate algorithm instance""" + + if algorithm_mode == 'aco': + # TODO: Import and create ACO instance + # from .algorithms import AntColonyOptimizer + # return AntColonyOptimizer(**self._filter_params('aco')) + raise NotImplementedError("ACO algorithm not yet implemented") + + elif algorithm_mode == 'pso': + # TODO: Import and create PSO instance + # from .algorithms import ParticleSwarmOptimizer + # return ParticleSwarmOptimizer(**self._filter_params('pso')) + raise NotImplementedError("PSO algorithm not yet implemented") + + elif algorithm_mode == 'abc': + # TODO: Import and create ABC instance + # from .algorithms import ArtificialBeeColonyOptimizer + # return ArtificialBeeColonyOptimizer(**self._filter_params('abc')) + raise NotImplementedError("ABC algorithm not yet implemented") + + else: + raise ValueError(f"Unknown algorithm mode: {algorithm_mode}") + + def _filter_params(self, algorithm_mode: str) -> Dict[str, Any]: + """Filter parameters relevant to the specific algorithm""" + base_params = { + 'n_iterations': self.n_iterations, + 'random_state': self.random_state, + } + + if algorithm_mode == 'aco': + aco_params = { + 'n_ants': self.kwargs.get('n_ants', 50), + 'alpha': self.kwargs.get('alpha', 1.0), + 'beta': self.kwargs.get('beta', 2.0), + 'rho': self.kwargs.get('rho', 0.5), + 'q': self.kwargs.get('q', 1.0), + } + return {**base_params, **aco_params} + + elif algorithm_mode == 'pso': + pso_params = { + 'n_particles': self.kwargs.get('n_particles', 30), + 'w': self.kwargs.get('w', 0.9), + 'c1': self.kwargs.get('c1', 2.0), + 'c2': self.kwargs.get('c2', 2.0), + } + return {**base_params, **pso_params} + + elif algorithm_mode == 'abc': + abc_params = { + 'n_bees': self.kwargs.get('n_bees', 50), + 'limit': self.kwargs.get('limit', 10), + } + return {**base_params, **abc_params} + + return base_params + + def fit(self, X, y=None): + """ + Fit the optimizer to the problem + + Parameters + ---------- + X : array-like or callable + Problem data (distance matrix, objective function, etc.) + y : array-like, optional + Target values for supervised problems + + Returns + ------- + self : AutoColony + Returns self for method chaining + """ + # Determine algorithm mode + if self.mode == 'auto': + algorithm_mode = self._detect_problem_type(X, y) + else: + algorithm_mode = self.mode + + # Create algorithm instance + self._algorithm = self._create_algorithm(algorithm_mode) + + # Fit the algorithm + # TODO: Implement actual fitting logic + self._fitted = True + + return self + + def predict(self): + """ + Get the best solution found + + Returns + ------- + solution : array-like + Best solution found by the algorithm + """ + if not self._fitted: + raise ValueError("Must call fit() before predict()") + + # TODO: Return actual best solution + return self._best_solution + + def score(self, X=None, y=None): + """ + Get the best score/fitness value + + Returns + ------- + score : float + Best score found by the algorithm + """ + if not self._fitted: + raise ValueError("Must call fit() before score()") + + # TODO: Return actual best score + return self._best_score + + def get_params(self, deep=True): + """Get parameters for this estimator""" + params = { + 'mode': self.mode, + 'n_iterations': self.n_iterations, + 'random_state': self.random_state, + } + params.update(self.kwargs) + return params + + def set_params(self, **params): + """Set parameters for this estimator""" + valid_params = self.get_params() + + for key, value in params.items(): + if key in ['mode', 'n_iterations', 'random_state']: + setattr(self, key, value) + else: + self.kwargs[key] = value + + return self \ No newline at end of file diff --git a/docs/NOTE.md b/docs/NOTE.md index 80305f7..4d3f314 100644 --- a/docs/NOTE.md +++ b/docs/NOTE.md @@ -1,3 +1,6 @@ + +## Algorithms + | Order | Algorithm | Type | Use Case | Notes | | ----- | --------- | ---------- | ---------------------------- | --------------------- | | 1 | ACO | Discrete | TSP, routing, scheduling | Good starter | @@ -6,3 +9,39 @@ | 4 | Firefly | Continuous | Multimodal optimization | Easy to add after PSO | | 5 | Glowworm | Continuous | Multi-solution discovery | Experimental | | 6 | Bacterial | Mixed | Bio-inspired optimization | Complex | + + +## Directory + +``` + +colonyx/ +├── src/ # Rust core implementation +│ ├── lib.rs # Main library entry point +│ ├── algorithms/ # Algorithm implementations +│ │ ├── mod.rs # Algorithms module +│ │ ├── base.rs # Base traits and types +│ │ ├── aco.rs # Ant Colony Optimization +│ │ ├── pso.rs # Particle Swarm Optimization +│ │ └── abc.rs # Artificial Bee Colony +│ ├── core/ # Core optimization structures +│ │ ├── mod.rs +│ │ ├── problem.rs # Problem definitions +│ │ └── solution.rs # Solution representations +│ ├── utils/ # Utilities +│ │ ├── mod.rs +│ │ └── math.rs # Math utilities +│ └── bindings.rs # Python bindings +│ +├── colonyx/ # Python package +│ ├── __init__.py # Main exports +│ ├── auto.py # AutoColony class +│ ├── base.py # Base classes +│ ├── utils.py # Python utilities +│ └── datasets.py # Benchmark datasets +│ +├── examples/ # Usage examples +├── tests/ # Tests +└── docs/ # Documentation + +``` \ No newline at end of file diff --git a/hello.py b/hello.py deleted file mode 100644 index 06b8194..0000000 --- a/hello.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from colonyx!") - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 303ff93..f3df5f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,16 @@ build-backend = "maturin" [project] name = "colonyx" requires-python = ">=3.8" +description = "A Pythonic toolkit for Ant Colony, Particle Swarm, and Bee Colony optimization — written in Rust for high performance, designed for real-world use." +readme = "README.md" classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version"] + + [tool.maturin] features = ["pyo3/extension-module"] -python-source = "python" -compatibility = "py38, py39, py310, py311, py312" +python-source = "colonyx" diff --git a/src/algorithms/aco.rs b/src/algorithms/aco.rs new file mode 100644 index 0000000..454ea05 --- /dev/null +++ b/src/algorithms/aco.rs @@ -0,0 +1,121 @@ +use crate::core::{Problem, Solution, SolutionSet}; +use crate::algorithms::base::{Optimizer, OptimizationError}; +use std::collections::HashMap; + +pub struct AntColony { + pub n_ants: usize, + pub n_iterations: usize, + pub alpha: f64, // Pheromone importance + pub beta: f64, // Heuristic importance + pub rho: f64, // Evaporation rate + pub q: f64, // Q value for pheromone update + + // Internal state using core types + pheromone_matrix: Option>>, + best_solution: Option, + + #[allow(dead_code)] + random_seed: Option, +} + +impl AntColony { + pub fn new( + n_ants: usize, + n_iterations: usize, + alpha: f64, + beta: f64, + rho: f64, + q: f64, + ) -> Self { + Self { + n_ants, + n_iterations, + alpha, + beta, + rho, + q, + pheromone_matrix: None, + best_solution: None, + random_seed: None, + } + } + + fn initialize_pheromone_matrix(&mut self, size: usize) { + let initial_pheromone = 1.0 / (size as f64); + self.pheromone_matrix = Some(vec![vec![initial_pheromone; size]; size]); + } + + fn construct_solution(&self, problem: &dyn Problem) -> Solution { + // TODO: Implement proper ant construction logic + let dimensions = problem.dimensions(); + let variables = (0..dimensions).map(|i| i as f64).collect(); + Solution::new(variables) + } +} + +impl Optimizer for AntColony { + type Solution = Solution; + + fn fit(&mut self, problem: &dyn Problem) -> Result<(), OptimizationError> { + if problem.dimensions() == 0 { + return Err(OptimizationError::InvalidInput( + "Problem must have at least one dimension".to_string() + )); + } + + // Initialize pheromone matrix for discrete problems + if problem.is_discrete() { + self.initialize_pheromone_matrix(problem.dimensions()); + } + + let mut best_fitness = f64::INFINITY; + + // Main ACO loop + for iteration in 0..self.n_iterations { + let mut solutions = Vec::new(); + + // Generate solutions with ants + for _ant in 0..self.n_ants { + let mut solution = self.construct_solution(problem); + + // Evaluate solution + let fitness = problem.evaluate(&solution.variables); + solution.set_fitness(fitness); + solution.add_metadata("iteration".to_string(), iteration.to_string()); + + // Update best solution + if fitness < best_fitness { + best_fitness = fitness; + self.best_solution = Some(solution.clone()); + } + + solutions.push(solution); + } + + // Update pheromones (placeholder for now) + let _solution_set = SolutionSet::new(solutions); + // TODO: Implement pheromone update logic + } + + Ok(()) + } + + fn predict(&self) -> Option { + self.best_solution.clone() + } + + fn score(&self) -> Option { + self.best_solution.as_ref().and_then(|s| s.fitness) + } + + fn get_params(&self) -> HashMap { + let mut params = HashMap::new(); + params.insert("n_ants".to_string(), self.n_ants as f64); + params.insert("n_iterations".to_string(), self.n_iterations as f64); + params.insert("alpha".to_string(), self.alpha); + params.insert("beta".to_string(), self.beta); + params.insert("rho".to_string(), self.rho); + params.insert("q".to_string(), self.q); + params + } +} \ No newline at end of file diff --git a/src/algorithms/base.rs b/src/algorithms/base.rs new file mode 100644 index 0000000..1453b5b --- /dev/null +++ b/src/algorithms/base.rs @@ -0,0 +1,39 @@ +/// Base trait for all optimization algorithms +pub trait Optimizer { + type Solution; + + /// Fit the optimizer to the problem + fn fit(&mut self, problem: &dyn Problem) -> Result<(), OptimizationError>; + + /// Get the best solution found + fn predict(&self) -> Option; + + /// Get the best score/fitness + fn score(&self) -> Option; + + /// Get algorithm-specific parameters + fn get_params(&self) -> std::collections::HashMap; +} + +// Use the Problem trait from core module +pub use crate::core::Problem; + +/// Error types for optimization +#[derive(Debug)] +pub enum OptimizationError { + InvalidInput(String), + ConvergenceError(String), + DimensionMismatch(String), +} + +impl std::fmt::Display for OptimizationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OptimizationError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), + OptimizationError::ConvergenceError(msg) => write!(f, "Convergence error: {}", msg), + OptimizationError::DimensionMismatch(msg) => write!(f, "Dimension mismatch: {}", msg), + } + } +} + +impl std::error::Error for OptimizationError {} \ No newline at end of file diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs new file mode 100644 index 0000000..25b7f1b --- /dev/null +++ b/src/algorithms/mod.rs @@ -0,0 +1,6 @@ +pub mod base; +pub mod aco; + +// Re-export main types +pub use base::{Optimizer, OptimizationError}; +pub use aco::AntColony; diff --git a/src/core/bounds.rs b/src/core/bounds.rs new file mode 100644 index 0000000..3f4d792 --- /dev/null +++ b/src/core/bounds.rs @@ -0,0 +1,85 @@ +/// Bounds for optimization variables +#[derive(Debug, Clone)] +pub struct Bounds { + pub lower: Vec, + pub upper: Vec, +} + +impl Bounds { + pub fn new(lower: Vec, upper: Vec) -> Result { + if lower.len() != upper.len() { + return Err("Lower and upper bounds must have the same length".to_string()); + } + + for (i, (&l, &u)) in lower.iter().zip(upper.iter()).enumerate() { + if l > u { + return Err(format!("Lower bound {} > upper bound {} at index {}", l, u, i)); + } + } + + Ok(Self { lower, upper }) + } + + /// Create uniform bounds for all dimensions + pub fn uniform(dimensions: usize, lower: f64, upper: f64) -> Result { + if lower > upper { + return Err("Lower bound must be <= upper bound".to_string()); + } + + Ok(Self { + lower: vec![lower; dimensions], + upper: vec![upper; dimensions], + }) + } + + /// Check if a solution is within bounds + pub fn contains(&self, solution: &[f64]) -> bool { + if solution.len() != self.lower.len() { + return false; + } + + for (i, &value) in solution.iter().enumerate() { + if value < self.lower[i] || value > self.upper[i] { + return false; + } + } + + true + } + + /// Clamp a solution to be within bounds + pub fn clamp(&self, solution: &mut [f64]) { + for (i, value) in solution.iter_mut().enumerate() { + if i < self.lower.len() { + *value = value.max(self.lower[i]).min(self.upper[i]); + } + } + } + + /// Get the range (upper - lower) for each dimension + pub fn ranges(&self) -> Vec { + self.upper.iter().zip(self.lower.iter()) + .map(|(u, l)| u - l) + .collect() + } + + /// Get the midpoint of bounds + pub fn midpoint(&self) -> Vec { + self.upper.iter().zip(self.lower.iter()) + .map(|(u, l)| (u + l) / 2.0) + .collect() + } +} + +/// Bound constraint types +#[derive(Debug, Clone)] +pub enum BoundConstraint { + /// Hard constraint - solutions outside bounds are invalid + Hard, + /// Soft constraint - solutions outside bounds are penalized + Soft { penalty: f64 }, + /// Reflect constraint - solutions outside bounds are reflected back + Reflect, + /// Wrap constraint - solutions outside bounds wrap around + Wrap, +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..93164b8 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,8 @@ +pub mod problem; +pub mod solution; +pub mod bounds; + +// Re-export main types +pub use problem::{Problem, ContinuousProblem, DiscreteProblem}; +pub use solution::{Solution, SolutionSet}; +pub use bounds::{Bounds, BoundConstraint}; \ No newline at end of file diff --git a/src/core/problem.rs b/src/core/problem.rs new file mode 100644 index 0000000..1f74b7c --- /dev/null +++ b/src/core/problem.rs @@ -0,0 +1,73 @@ +/// Generic problem trait +pub trait Problem { + fn evaluate(&self, solution: &[f64]) -> f64; + fn dimensions(&self) -> usize; + fn is_discrete(&self) -> bool; + fn name(&self) -> &str; +} + +/// Continuous problem (PSO, ABC) +pub struct ContinuousProblem { + pub name: String, + pub dimensions: usize, + pub objective_function: Box f64>, +} + +impl ContinuousProblem { + pub fn sphere(dimensions: usize, _bounds: Option) -> Self { + Self { + name: "Sphere".to_string(), + dimensions, + objective_function: Box::new(|x: &[f64]| x.iter().map(|&xi| xi * xi).sum()), + } + } +} + +impl Problem for ContinuousProblem { + fn evaluate(&self, solution: &[f64]) -> f64 { + (self.objective_function)(solution) + } + + fn dimensions(&self) -> usize { + self.dimensions + } + + fn is_discrete(&self) -> bool { + false + } + + fn name(&self) -> &str { + &self.name + } +} + +/// Discrete problem (ACO, TSP) +pub struct DiscreteProblem { + pub name: String, + pub distance_matrix: Vec>, +} + +impl Problem for DiscreteProblem { + fn evaluate(&self, solution: &[f64]) -> f64 { + // Calculate tour length + let mut total = 0.0; + for i in 0..solution.len() { + let from = solution[i] as usize; + let to = solution[(i + 1) % solution.len()] as usize; + total += self.distance_matrix[from][to]; + } + total + } + + fn dimensions(&self) -> usize { + self.distance_matrix.len() + } + + fn is_discrete(&self) -> bool { + true + } + + fn name(&self) -> &str { + &self.name + } +} \ No newline at end of file diff --git a/src/core/solution.rs b/src/core/solution.rs new file mode 100644 index 0000000..b4a60c4 --- /dev/null +++ b/src/core/solution.rs @@ -0,0 +1,96 @@ +/// A solution to an optimization problem +#[derive(Debug, Clone)] +pub struct Solution { + pub variables: Vec, + pub fitness: Option, + pub is_feasible: bool, + pub metadata: std::collections::HashMap, +} + +impl Solution { + pub fn new(variables: Vec) -> Self { + Self { + variables, + fitness: None, + is_feasible: true, + metadata: std::collections::HashMap::new(), + } + } + + pub fn with_fitness(variables: Vec, fitness: f64) -> Self { + Self { + variables, + fitness: Some(fitness), + is_feasible: true, + metadata: std::collections::HashMap::new(), + } + } + + pub fn set_fitness(&mut self, fitness: f64) { + self.fitness = Some(fitness); + } + + pub fn add_metadata(&mut self, key: String, value: String) { + self.metadata.insert(key, value); + } + + pub fn dimensions(&self) -> usize { + self.variables.len() + } +} + +/// A set of solutions (population) +#[derive(Debug, Clone)] +pub struct SolutionSet { + pub solutions: Vec, + pub best_index: Option, + pub generation: usize, +} + +impl SolutionSet { + pub fn new(solutions: Vec) -> Self { + Self { + solutions, + best_index: None, + generation: 0, + } + } + + pub fn find_best(&mut self) -> Option<&Solution> { + if self.solutions.is_empty() { + return None; + } + + let mut best_idx = 0; + let mut best_fitness = self.solutions[0].fitness.unwrap_or(f64::INFINITY); + + for (i, solution) in self.solutions.iter().enumerate() { + if let Some(fitness) = solution.fitness { + if fitness < best_fitness { + best_fitness = fitness; + best_idx = i; + } + } + } + + self.best_index = Some(best_idx); + Some(&self.solutions[best_idx]) + } + + pub fn get_best(&self) -> Option<&Solution> { + self.best_index.map(|idx| &self.solutions[idx]) + } + + pub fn size(&self) -> usize { + self.solutions.len() + } + + pub fn push(&mut self, solution: Solution) { + self.solutions.push(solution); + } + + pub fn next_generation(&mut self) { + self.generation += 1; + self.best_index = None; // Reset best index for new generation + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index a925158..df5eefd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,10 @@ use pyo3::prelude::*; +// Core modules +pub mod core; +pub mod algorithms; +pub mod utils; + /// Formats the sum of two numbers as string. #[pyfunction] fn sum_as_string(a: usize, b: usize) -> PyResult { diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..e69de29