JADE is an Adaptive Differential Evolution with external archive, p-best selection, and is parallel in this implementation.
This a very powerfull, population based algorithm for numerical optimization that can minimize the Rosenbrock function optimization problem in the following time on my computer running Linux Omarchy ( ARCH ). Rosenbrock is a very hard problem to optimize in high dimensions.
Function to optimize Rosenbrock
Dimensions | Time | Population size
------------------------------------------------
10 | 0.062 sec | 25
50 | 0.865 sec | 24
100 | 3.38 sec | 25
200 | 6 sec | 50
500 | 16.8 sec | 100
1000 | 1 m 3 sec | 200
2000 | 7 m 23 sec | 700
2500 | 9 m 37 sec | 900
3000 | 17 m 40 sec | 1200
3500 | 27 m 24 sec | 1200
4000 | 56 m 57 sec | 2000
make
./jade_optimizer_odin.exeMIT Open Source License
The program is a parallel implementation of JADE, which is an adaptive variant of Differential Evolution ( DE ).
It minimizes a function ( f( x ) )
over a bounded domain ( lo <= x <= hi )
where:
- Each “candidate solution” is a vector ( x in Rn ).
- A population of size ( NP ) evolves over generations.
- Each generation creates a “trial” vector for each population member via mutation + crossover, evaluates it, and keeps the better between parent and trial ( selection ).
You run it on either:
- Rastrigin ( highly multimodal ), or
- Rosenbrock ( banana-shaped valley; hard in high dimension ).
The key features JADE adds over classic DE are:
- p-best selection ( use one of the top p% individuals as a guide )
- External archive ( store replaced parents to increase diversity )
- Adaptive parameters ( mu_F ) and ( mu_CR ) updated from successful trials
Representation:
popA,popB: flat arrays of lengthNP * ncostA,costB: arrays of lengthNPholding objective values
vec_view( pop, i, n ) returns the slice corresponding to individual ( x_i ):
pop[ i * n : i * n + n ]
The objective signature is:
Objective_Fn :: proc ( x : [ ]f64, ctx : rawptr ) -> f64So JADE is generic: it works with any objective.
Instead of random uniform points, you do Latin Hypercube Sampling:
- For each dimension ( j ), the program divide ( [ lo_j, hi_j ] ) into ( NP ) bins.
- It pick exactly one sample from each bin ( with jitter ), and permute them.
- This spreads points more evenly than pure random.
That’s latin_hypercube_init.
Then it evaluate all initial candidates in parallel (JOB_INIT_EVAL).
Every generation it sorts indices rank[ 0 .. NP - 1 ] by increasing cost.
Then the p-best set is the top:
pcount = max( 2, | p . NP | )
And pbest_idx is picked uniformly from rank[ 0 .. pcount - 1 ].
That’s JADE’s “p-best”: it biases toward good solutions while still allowing variety.
JADE doesn’t use fixed CR and F. It samples them per-individual per-generation from distributions centered at adaptive means:
It sample:
CR ~ Normal( mu_CR, sigma )
Then clamp to ( [ 0, 1 ]).
In code:
CR := mu_CR + sigma * rand_normal( )- clamp
It sample:
F ~ Cauchy( mu_F, gamma )
Then:
- resample while (
F <= 0) - clamp to (
F <= 1)
Cauchy has heavier tails than normal implies that occasionally gives large steps ( good for escaping ).
Classic DE mutation often looks like:
v = x_r1 + F( x_r2 - x_r3 )
JADE uses ( one common form ):
v = x_i + F( x_pbest - x_i ) + F( x_r1 - x_r2 )
This is your code:
mut[ d ] = xi[ d ] + F * ( xpbest[ d ] - xi[ d ] ) + F*( xr1[ d ] - xr2[ d ] )
Where:
- ( x_i ) is the current individual
- ( x_pbest ) comes from the top
p% - ( x_r1 ) is random from population excluding duplicates
- ( x_r2 ) is random from the union of population and archive
The archive is crucial : it injects diversity by letting ( x_r2 ) come from older “discarded” individuals.
After mutation, you enforce bounds with clamp_reflect:
If a component goes below lo, you reflect it back in a randomized way; similarly if above hi.
This avoids the “stuck on boundary” behavior that simple clamping can cause, and keeps some randomness.
It creates a trial vector ( u ) by mixing the mutated vector ( v ) and parent ( x_i ):
For each component (d):
- with probability
CR, takemut[ d ] - otherwise take
xi[ d ]
But ensure at least one dimension comes from mutation by forcing jrand to be mutated.
That is classic binomial crossover.
You evaluate the trial ( f( u ) ). If it’s better or equal, accept it:
pop_next[ i ] = trialcost_next[ i ] = ft
Otherwise keep parent.
This is the classic DE greedy selection.
Whenever a trial is accepted, JADE stores the replaced parent into the archive.
In this implementation:
- Each thread stores replaced parents into its own
arch_addchunk buffer. - After the generation, main thread merges them into the global archive.
Archive rules:
- archive capacity is
NP - if there is free space, append
- if overflow, overwrite random archive slots
This is consistent with JADE’s idea: archive is a bounded “bag” of past solutions.
The core JADE adaptive logic:
Collect successful parameters ( only from accepted trials ):
- ( CR_s ) and ( F_s ) for all successes
Then compute:
- mean CR : arithmetic average
- mean F : Lehmer mean
Lehmer( F ) = sum( F^2 ) / sum( F )
This biases toward larger successful (F).
Then update:
mu_CR <- ( 1 - c ) x mu_CR + c . mean( CR_s )
mu_F <- ( 1 - c ) x mu_F + c . Lehmer( F_s )
In the code, c is par.c.
That’s what makes JADE “self-tuning”: it learns the parameter region that produces improvements.
The parallel design uses a fixed worker pool and two barriers per job.
Main thread doesn’t create threads per generation. It creates them once, then repeatedly tells them what to do:
JOB_INIT_EVALJOB_GEN_STEPJOB_ARCHIVE_COPY
Workers wait at a start barrier, do their chunk, then wait at a done barrier.
This is a common high-performance pattern because it avoids thread creation overhead and is naturally deterministic in structure.
Shared is basically the “command packet” for a job:
jobsays what work to do- pointers/slices like
pop_cur,cost_cur,rank,pop_next, etc. tell workers where to read/write - parameters
mu_F,mu_CR, etc. are “snapshotted” into Shared before job starts
Workers only read these snapshots during the job.
This avoids per-thread disagreement about values.
This program uses:
b_start: all workers + main rendezvous before work beginsb_done: all workers + main rendezvous after work completes
The sequence each generation is:
- Main fills out Shared fields
- Main sets
job - Main calls
barrier_wait( b_start ) - Workers pass
b_start, run job - Workers wait
b_done - Main waits
b_done - Main reduces stats / updates parameters / handles archive
So the barrier acts like a “frame boundary”: all writes before the barrier are visible after it.
Workers partition indices using:
chunk = ( NP + NUM_THREADS - 1 ) / NUM_THREADS
start = tid * chunk
end = min( start + chunk, NP )So each worker gets a contiguous segment.
This is used for:
INIT_EVAL: evaluate costs for its chunkGEN_STEP: create trial vectors and select for its chunkARCHIVE_COPY: copy its ownarch_addentries into archive slots
This partitioning is simple and avoids synchronization within the job.
In GEN_STEP:
Each worker only writes:
pop_next[ i ]cost_next[ i ]
for its own i range.
Everything it reads is read-only:
pop_cur,cost_cur,rank,arch
So there are no write-write races.
Also each worker has its own:
- RNG state (
Worker.rng) - scratch buffers (
mut,trial)
So no shared temp buffers, no contention.
This is important.
If every worker wrote into the global archive directly during GEN_STEP, you would have races because:
- multiple accepted trials can happen concurrently
- archive append/overflow logic needs coordination
So you instead do:
- During
GEN_STEP, each worker stores replaced parents into its ownarch_addbuffer - After
GEN_STEP, main thread computes where those entries should go ( dest slots ) - Then it runs
JOB_ARCHIVE_COPYwhere workers only do memcopies
That makes archive updates parallel but still cleanly coordinated.
In this version we intentionally ensure that when overflow happens, we do not pick the same destination slot twice in the same generation.
That avoids a subtle data race: if two threads got the same dest slot, they would write to the same memory location concurrently during archive copy.
So:
- appended slots are inherently unique
- overflow picks random slots but enforced uniqueness using
used[ ]
This makes JOB_ARCHIVE_COPY fully race-free.
Workers do:
- wait
b_start - check
stop - if stop it still wait
b_done→ then break
Main does:
- set stop=1
- do
b_start,b_done
This guarantees that the barrier counts still match, so the program won’t hang at shutdown.
Best regards,
Joao Carvalho