Note for AI agents: For current development status, progress tracking, and known issues, see DEVELOPMENT.md.
This package implements a custom Quality Diversity (QD) evolutionary algorithm to optimize oscillatory biological systems modeled with differential equations. The project supports two main models: a lipid oscillator system and a coupled trimer assembly system. The goal is to find parameter sets that produce desired oscillatory behavior in systems of ordinary differential equations (ODEs). The codebase integrates with the Evolutionary.jl framework, uses trait-based dispatch for model-agnostic optimization, and leverages Julia's capabilities for high-performance numerical computing.
- Overview
- Features
- Installation
- Getting Started
- Code Structure
- 1. Reaction System Model Definition
- 2. Accessor Functions for Reaction System
- 3. Trait-Based Dispatch System
- 4. Initial Population Generation
- 5. Optimization Setup
- 6. Custom Quality Diversity Algorithm
- 7. Individual Evaluation
- 8. Fitness Function
- 9. Fitness Function Helpers
- 10. Tracing and Recording
- 11. Results Processing
- Workflow
- Usage
- Examples
- Custom implementation of a Quality Diversity evolutionary algorithm.
- Integration with
Evolutionary.jlfor optimization routines. - Trait-based dispatch system for model-agnostic optimization pipeline.
- Multiple model support: Lipid oscillator and trimer assembly models.
- Evaluation of ODE systems with customizable parameters and initial conditions.
- Model-specific fitness observables automatically selected via trait system.
- Fitness functions designed to assess oscillatory behavior.
- Parallel evaluation of individuals with multithreading support.
- Comprehensive tracing and recording of optimization progress.
- Results aggregation and data processing for analysis and visualization.
Since OscillatorOptimization.jl is not yet registered in the official Julia General registry, you can install it directly from GitHub:
From source:
julia> using Pkg; Pkg.add(url="https://github.com/JohnsonBiophysicsLab/OscillatorOptimization.git")Or using the package manager interface:
julia> ] # enters the pkg interface
Pkg> add https://github.com/JohnsonBiophysicsLab/OscillatorOptimization.gitAlternatively, you can clone the repository and develop it locally:
julia> using Pkg; Pkg.develop(path="https://github.com/JohnsonBiophysicsLab/OscillatorOptimization.git")The codebase is organized into several modules, each handling different aspects of the optimization process. Below is a detailed explanation of each component.
Files: full_model.jl, trimer_model.jl
Purpose:
- Defines biochemical oscillator models as
ReactionSystemobjects using Catalyst.jl, which allows for easy construction of chemical reaction networks. - Specifies the components of the reaction systems, including rate constants, dimensionality factors (DF), species, and initial conditions.
- Lipid Oscillator Models (
full_model.jl):base_rn: 12-variable restricted model with essential complexesperipheral_rn: Additional peripheral reactionsfullrn: Complete reaction network extendingbase_rnwithperipheral_rn
- Trimer Assembly Model (
trimer_model.jl):trimer_rn: Extended model with heterotrimer assembly on membrane- Includes monomer species (B, C, D) and assembly intermediates
- Models pathway from monomers → dimers → trimers with membrane localization
Key Components:
- Lipid Models:
base_rn,peripheral_rn,fullrnwith observablesAmem_old,Amem - Trimer Model:
trimer_rnwith observablesAmem_old,TrimerYield,Tmem - make_odeprob/make_trimer_odeprob: Functions that convert reaction networks into
ODEProblemobjects
Workflow Integration:
- Models are converted to
ODESystemobjects which form the basis for optimization. Parameters and species initial conditions are tunable, enabling evolutionary search for oscillatory behavior. The trait system automatically selects appropriate observables for each model type.
File: accessor_functions.jl
Purpose:
- Provides utility functions to access and manipulate the symbolic properties of
ReactionSystemobjects. - Enables easy extraction of tunable parameters and species, making the optimization process more manageable.
- Works with both lipid oscillator and trimer assembly models.
Key Components:
- get_parameter_symbols: Extracts the parameters of the reaction system, with an option to filter only tunable ones.
- get_species_symbols: Extracts the species of the reaction system, with an option to filter only tunable ones.
- get_tunable_bounds_dictionary: Returns bounds for tunable parameters in a dictionary format.
- get_unfixed_tunable_symbols: Returns tunable symbols excluding fixed parameters.
- get_default_values: Returns default values for tunable parameters.
Workflow Integration:
- These functions are used throughout the optimization process to access and manipulate the properties of reaction systems dynamically, especially during population initialization and evaluation. They work seamlessly with both model types through the shared parameter structure.
File: traits.jl
Purpose:
- Implements a trait-based dispatch system for model-agnostic optimization.
- Automatically selects appropriate fitness observables based on the reaction system's name.
- Enables seamless integration of new models without modifying the optimization pipeline.
Key Components:
- fitness_observables trait: Returns a tuple
(fft_observable, time_domain_observable)for each model type. - Value-based dispatch: Uses
Val{:model_name}to dispatch on reaction system names. - Built-in model support:
fullrn: Uses(:Amem_old, :Amem)observablestrimer_rn: Uses(:Amem_old, :TrimerYield)observables
Workflow Integration:
- The trait system is automatically invoked during
OptimizationReactionSystemconstruction, ensuring the correct observables are used for fitness evaluation without requiring model-specific code changes in the optimization pipeline.
File: population_generation.jl
Purpose:
- Generates the initial population of potential solutions for the optimization.
- Uses a
DimArraystructure to represent individuals, allowing for efficient, labeled indexing.
Key Components:
- generate_population: Generates an initial population, excluding fixed inputs, and ensures each individual satisfies constraints if provided.
- make_tunable_distributions: Creates a distribution for each tunable parameter, allowing for sampling during population generation.
- DimArray Representation: Uses
DimensionalData.jlto create labeled data structures for easy access and manipulation of individuals.
Workflow Integration:
- This population serves as the starting point for the evolutionary optimization. It undergoes selection, mutation, and crossover to evolve towards the desired oscillatory behavior.
File: optimization.jl
Purpose:
- Initializes the optimization problem for any supported model type.
- Defines the
ObjectiveFunctionthat wraps the evaluation of individuals. - Sets up the
OptimizationReactionSystem, which includes the ODE problem, parameter and species setters, and observable getters. - Configures the FFT plans for efficient frequency analysis.
- Uses trait-based dispatch to automatically select appropriate fitness observables for each model.
Key Components:
- OptimizationReactionSystem: A struct that holds the ODE problem and related functions, with model-specific observables determined by traits.
- ObjectiveFunction: Wraps the evaluation function to be used by the evolutionary algorithm.
- FFT Plans: Pre-planned FFT computations for performance optimization.
- Trait Integration: Automatically determines fitness observables via
fitness_observables(reaction_system)trait.
Workflow Integration:
- The constructor automatically detects the model type and selects appropriate observables (e.g.,
Amem_old/Amemfor lipid models,Amem_old/TrimerYieldfor trimer models) without requiring model-specific code changes.
File: quality-diversity.jl
Purpose:
- Implements a custom QD algorithm by extending and overloading methods from Evolutionary.jl.
- Defines custom structures and functions to control the evolutionary process.
Key Components:
- QD Struct: Encapsulates algorithm parameters like population size, crossover rate, mutation rate, selection, crossover, mutation functions, and metrics.
- QDState Struct: Maintains the state of the optimization, including the best individual and objective values.
- Overloaded Methods: Custom implementations of Evolutionary.jl methods such as
initial_state,update_state!,recombine!,mutate!, andevaluate!. - Objective Function Integration: Interfaces with
EvolutionaryObjectiveto evaluate individuals using the custom objective function.
File: evaluate_individual.jl
Purpose:
- Defines how each individual (a set of parameters and initial conditions) is evaluated.
- Solves the ODEProblem with the individual's parameters.
- Calculates the fitness based on the solution.
Key Components:
- evaluate_individual!: Main function that evaluates an individual and updates the phenotype vector with fitness, period, and amplitude.
- solve_odes: Solves the ODEProblem for a given individual.
- remake_odeprob: Remakes the ODEProblem with new parameters and species initial conditions.
File: FitnessFunction.jl
Purpose:
- Calculates the fitness of an individual based on the ODE solution.
- Analyzes the time-series data to determine oscillatory behavior.
Key Components:
- calculate_fitness!: Core function that computes fitness, period, and amplitude from the time-series data.
- find_fft_peaks: Identifies significant peaks in the frequency domain.
- find_amem_peaks: Identifies peaks and troughs in the time domain.
File: fitness_function_helpers.jl
Purpose:
- Provides helper functions used in the fitness calculation.
- Handles signal processing tasks like FFT computation and peak analysis.
Key Components:
- getDif: Calculates the sum of absolute differences between consecutive peak values.
- getSTD: Computes the summed standard deviation around peaks in the FFT data.
- getFrequencies: Performs FFT on the time-series data.
- compute_period: Calculates the period between the last two peaks.
- compute_amplitude: Calculates the amplitude between the last peak and last trough.
File: trace.jl
Purpose:
- Records the state of the optimization at each iteration.
- Saves information about the population, fitness values, and solutions.
Key Components:
- trace!: Custom function that records various aspects of the optimization state, including populations and fittest individuals.
- insert_fixed_inputs!: Inserts fixed parameters back into an individual's chromosome for reporting purposes.
File: results.jl
Purpose:
- Processes and aggregates the results after the optimization run.
- Compiles the data into a
DataFramefor analysis.
Key Components:
- Results Struct: Holds the hit rate, results
DataFrame, and combined population. - Results Function: Processes the
EvolutionaryOptimizationResultsto construct aResultsobject, extracting unique individuals and compiling metrics.
-
Initialization:
- Define the ODE model and problem using
OptimizationReactionSystem. - Trait system automatically selects appropriate fitness observables for the model type.
- Set up the initial population with parameters and species initial conditions.
- Define the ODE model and problem using
-
Evaluation:
- For each individual,
evaluate_individual!solves the ODEs and calculates the fitness usingcalculate_fitness!. - Fitness calculation involves signal processing of the ODE solution to assess oscillatory behavior.
- Model-specific observables are used automatically (e.g.,
Amem_old/Amemfor lipid models,Amem_old/TrimerYieldfor trimer models).
- For each individual,
-
Evolutionary Loop:
- The custom QD algorithm evolves the population through selection, crossover, and mutation.
- Overloaded methods from Evolutionary.jl control the evolutionary process.
-
Tracing:
- At each iteration,
trace!records the optimization state, including populations and fittest individuals.
- At each iteration,
-
Results Processing:
- After optimization,
Resultsprocesses the collected data. - Unique individuals are extracted, and results are compiled into a
DataFrame.
- After optimization,
-
Analysis:
- The results can be analyzed and visualized to interpret the optimization outcomes.
-
Set Up the ODE Model:
- Define your ODE system (choose from
fullrn,trimer_rn, or define your own). - Create an
OptimizationReactionSysteminstance - traits automatically handle observable selection. - For new models: Implement the
fitness_observables(::Val{:your_model_name})trait method.
- Define your ODE system (choose from
-
Configure the Optimization:
- Initialize the
QDstruct with desired algorithm parameters. - Set up the initial population.
- Initialize the
-
Run the Optimization:
# Load the package (includes commonly used functions from dependencies) using OscillatorOptimization # Import additional solvers if needed (Rodas5P is commonly used for stiff problems) using OrdinaryDiffEq: Rodas5P # Set BLAS threads to 1 to avoid multithreading issues BLAS.set_num_threads(1) # Define fixed parameters fixed_params = Dict(:DF => 100.0) # Define ODE solver algorithm alg = Rodas5P(chunk_size = 16) # Define optimization system for lipid oscillator model fixed_opt_sys = OptimizationReactionSystem(fullrn, alg; fixed_params) # Run optimization fixed_results = run_optimization(1000, fixed_opt_sys; mutation_δ = 1.0, pm = 0.75, η = 1, n_points = Inf, num_tournament_groups = 20, mutationRate = 0.95, crossoverRate = 0.75, sbx_pm = 0.3, sbx_η = 2, parallelization = :threadprogress, callback = nothing, show_trace = true) # Access results dataframe and save to disk results_df = fixed_results.results_df write("optimization_results.csv", results_df) # Alternative: Use trimer assembly model # trimer_opt_sys = OptimizationReactionSystem(trimer_rn, alg; fixed_params) # trimer_results = run_optimization(1000, trimer_opt_sys; <same parameters>)
Note: You can use different ODE solvers by importing them from
OrdinaryDiffEq. For example:using OrdinaryDiffEq: Tsit5, Rosenbrock23, Vern9 # For non-stiff problems alg = Tsit5() # For stiff problems (alternative to Rodas5P) alg = Rosenbrock23() # For high accuracy alg = Vern9()
-
Analyze Results:
- If instead you want to re-solve for an existing solution from a
DataFramedataset, you can use thesolve_rowmethod.
# Load the package (includes commonly used functions from dependencies) using OscillatorOptimization # Set BLAS threads to 1 to avoid multithreading issues BLAS.set_num_threads(1) # Load the results dataframe results_df = read("optimization_results.csv", DataFrame) # Solve the first row, returns an `ODESolution` object sol = solve_row(results_df[1, :], fullrn) # Plot the solution with desired observables/species plot(sol, idxs = [:Amem_old, :Amem]) # Or extract individual solutions as Vectors amem = sol[:Amem] L = sol[:L] K = sol[:K] P = sol[:P] LpA = sol[:LpA]
For High-Throughput Analysis: If you need to solve many rows from the same
DataFrameefficiently, use the performant version that returns a solver function. This is useful for loops or parallel processing.# Create a performant solver function (one-time setup) fast_solver = solve_row(results_df, fullrn) # Now solve any row efficiently sol1 = fast_solver(1) # Solve first row sol42 = fast_solver(42) # Solve 42nd row sol1000 = fast_solver(1000) # Solve 1000th row # Perfect for loops or parallel processing solutions = [fast_solver(i) for i in 1:100] # Solve first 100 rows # Or for specific analysis interesting_rows = [1, 42, 100, 500, 1000] interesting_solutions = [fast_solver(i) for i in interesting_rows]
Performance Benefits:
- One-time setup cost: Column extraction and setter creation happens once
- Fast repeated solves: Uses pre-extracted matrices and SymbolicIndexingInterface
- Thread-safe: Each call creates isolated problem copies
- Memory efficient: Uses
view()for zero-copy matrix access
When to Use Each Method:
solve_row(row, odeprob): For interactive use, single solves, or when you have individual rowssolve_row(df, odeprob): For high-throughput workflows, loops, or when you need to solve many rows from the same DataFrame
- If instead you want to re-solve for an existing solution from a