LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
DifferentialEvolution.m
1classdef DifferentialEvolution < handle
2 % DifferentialEvolution Self-contained port of scipy's
3 % differential_evolution that reproduces its trajectory bit-for-bit for a
4 % given integer seed, using opt.de.NumpyRandomState for all draws.
5 %
6 % Covers the configuration used by line-opt: binomial strategies (default
7 % best1bin), updating='immediate', dithered mutation, latinhypercube
8 % initialization, polish=false, and penalty-based constraints handled inside
9 % the objective. The exact numpy draw order is preserved: LHS init (uniform
10 % grid then per-column permutation), a per-generation dither uniform, and per
11 % candidate randint (fill point), shuffle (sample selection) and uniform
12 % (crossover), plus a data-dependent uniform in the bound-repair step.
13
14 properties
15 objective % function handle: energy = f(x)
16 low % 1xd lower bounds
17 high % 1xd upper bounds
18 paramCount
19 strategy
20 popsizeMult
21 maxiter
22 ditherLow
23 ditherHigh
24 recombination
25 tol
26 atol
27 callback % function handle (bestX, nit) -> logical stop, or []
28 rng % opt.de.NumpyRandomState
29 numMembers
30 population % numMembers x d, scaled to [0,1]
31 energies % 1 x numMembers
32 randomIndex % persistent shuffle buffer (0-based values)
33 scale
34 nfev
35 bestPerGen % cell array of 1xd vectors
36 end
37
38 methods
39 function obj = DifferentialEvolution(objective, low, high, strategy, ...
40 popsizeMult, maxiter, ditherLow, ditherHigh, recombination, tol, seed)
41 obj.objective = objective;
42 obj.low = low(:).';
43 obj.high = high(:).';
44 obj.paramCount = numel(low);
45 obj.strategy = strategy;
46 obj.popsizeMult = popsizeMult;
47 obj.maxiter = maxiter;
48 obj.ditherLow = min(ditherLow, ditherHigh);
49 obj.ditherHigh = max(ditherLow, ditherHigh);
50 obj.recombination = recombination;
51 obj.tol = tol;
52 obj.atol = 0.0;
53 obj.callback = [];
54 obj.scale = ditherLow;
55 obj.rng = opt.de.NumpyRandomState(seed);
56 obj.bestPerGen = {};
57 obj.initPopulationLhs();
58 end
59
60 function s1 = scaleArg1(obj, j)
61 s1 = 0.5 * (obj.low(j) + obj.high(j));
62 end
63
64 function s2 = scaleArg2(obj, j)
65 s2 = abs(obj.low(j) - obj.high(j));
66 end
67
68 function out = scaleParameters(obj, trial)
69 out = zeros(1, obj.paramCount);
70 for j = 1:obj.paramCount
71 out(j) = obj.scaleArg1(j) + (trial(j) - 0.5) * obj.scaleArg2(j);
72 end
73 end
74
75 function initPopulationLhs(obj)
76 ebCount = sum(obj.low == obj.high);
77 obj.numMembers = max(5, obj.popsizeMult * max(1, obj.paramCount - ebCount));
78 segsize = 1.0 / obj.numMembers;
79 flat = obj.rng.uniformN(0, 1, obj.numMembers * obj.paramCount);
80 samples = zeros(obj.numMembers, obj.paramCount);
81 p = 1;
82 for i = 1:obj.numMembers
83 offset = (i - 1) / obj.numMembers;
84 for j = 1:obj.paramCount
85 samples(i, j) = segsize * flat(p) + offset;
86 p = p + 1;
87 end
88 end
89 obj.population = zeros(obj.numMembers, obj.paramCount);
90 for j = 1:obj.paramCount
91 order = obj.rng.permutation(obj.numMembers); % 0-based
92 for i = 1:obj.numMembers
93 obj.population(i, j) = samples(order(i) + 1, j);
94 end
95 end
96 obj.energies = inf(1, obj.numMembers);
97 obj.randomIndex = 0:(obj.numMembers - 1);
98 obj.nfev = 0;
99 end
100
101 function b = best(obj)
102 b = obj.scaleParameters(obj.population(1, :));
103 end
104
105 function calculateInitialEnergies(obj)
106 for i = 1:obj.numMembers
107 obj.energies(i) = obj.objective(obj.scaleParameters(obj.population(i, :)));
108 obj.nfev = obj.nfev + 1;
109 end
110 end
111
112 function promoteLowestEnergy(obj)
113 [~, l] = min(obj.energies);
114 if l ~= 1
115 te = obj.energies(1); obj.energies(1) = obj.energies(l); obj.energies(l) = te;
116 tp = obj.population(1, :); obj.population(1, :) = obj.population(l, :); obj.population(l, :) = tp;
117 end
118 end
119
120 function tf = converged(obj)
121 if any(isinf(obj.energies))
122 tf = false;
123 return;
124 end
125 tf = std(obj.energies, 1) <= obj.atol + obj.tol * abs(mean(obj.energies));
126 end
127
128 function out = selectSamples(obj, candidate, numberSamples)
129 % candidate is 0-based member index
130 obj.randomIndex = obj.rng.shuffle(obj.randomIndex);
131 pick = obj.randomIndex(1:numberSamples + 1);
132 out = zeros(1, numberSamples);
133 k = 0;
134 for i = 1:numel(pick)
135 if k >= numberSamples
136 break;
137 end
138 if pick(i) ~= candidate
139 k = k + 1;
140 out(k) = pick(i);
141 end
142 end
143 end
144
145 function b = bprime(obj, candidate, s)
146 % candidate and s are 0-based member indices; convert to 1-based rows
147 P = obj.population;
148 c = candidate + 1;
149 r = s + 1;
150 switch obj.strategy
151 case {'rand1bin', 'rand1exp'}
152 b = P(r(1), :) + obj.scale * (P(r(2), :) - P(r(3), :));
153 case {'randtobest1bin', 'randtobest1exp'}
154 b = P(r(1), :);
155 b = b + obj.scale * (P(1, :) - b);
156 b = b + obj.scale * (P(r(2), :) - P(r(3), :));
157 case {'currenttobest1bin', 'currenttobest1exp'}
158 b = P(c, :) + obj.scale * (P(1, :) - P(c, :) + P(r(1), :) - P(r(2), :));
159 case {'best2bin', 'best2exp'}
160 b = P(1, :) + obj.scale * (P(r(1), :) + P(r(2), :) - P(r(3), :) - P(r(4), :));
161 case {'rand2bin', 'rand2exp'}
162 b = P(r(1), :) + obj.scale * (P(r(2), :) + P(r(3), :) - P(r(4), :) - P(r(5), :));
163 otherwise % best1bin / best1exp
164 b = P(1, :) + obj.scale * (P(r(1), :) - P(r(2), :));
165 end
166 end
167
168 function trial = mutate(obj, candidate)
169 % candidate is 0-based member index
170 fillPoint = obj.rng.randint(0, obj.paramCount); % 0-based
171 samples = obj.selectSamples(candidate, 5);
172 bp = obj.bprime(candidate, samples);
173 trial = obj.population(candidate + 1, :);
174 cross = obj.rng.uniformN(0, 1, obj.paramCount);
175 doCross = cross < obj.recombination;
176 doCross(fillPoint + 1) = true;
177 trial(doCross) = bp(doCross);
178 end
179
180 function trial = ensureConstraint(obj, trial)
181 mask = (trial > 1) | (trial < 0);
182 oob = sum(mask);
183 if oob > 0
184 repl = obj.rng.uniformN(0, 1, oob);
185 trial(mask) = repl;
186 end
187 end
188
189 function result = solve(obj)
190 warningFlag = false;
191 if any(isinf(obj.energies))
192 obj.calculateInitialEnergies();
193 obj.promoteLowestEnergy();
194 end
195 nit = 0;
196 for nit = 1:obj.maxiter
197 obj.next();
198 if ~isempty(obj.callback)
199 stop = obj.callback(obj.best(), nit);
200 if stop
201 warningFlag = true;
202 end
203 end
204 obj.bestPerGen{end+1} = obj.best(); %#ok<AGROW>
205 if warningFlag || obj.converged()
206 break;
207 end
208 end
209 if nit >= obj.maxiter
210 warningFlag = warningFlag || (nit == obj.maxiter && ~obj.converged());
211 end
212 result = struct();
213 result.x = obj.best();
214 result.fun = obj.energies(1);
215 result.nit = nit;
216 result.nfev = obj.nfev;
217 result.success = ~warningFlag;
218 result.bestPerGen = obj.bestPerGen;
219 end
220
221 function next(obj)
222 obj.scale = obj.rng.uniformScalar(obj.ditherLow, obj.ditherHigh);
223 for candidate = 0:(obj.numMembers - 1)
224 trial = obj.mutate(candidate);
225 trial = obj.ensureConstraint(trial);
226 parameters = obj.scaleParameters(trial);
227 energy = obj.objective(parameters);
228 obj.nfev = obj.nfev + 1;
229 if energy <= obj.energies(candidate + 1)
230 obj.population(candidate + 1, :) = trial;
231 obj.energies(candidate + 1) = energy;
232 if energy <= obj.energies(1)
233 obj.promoteLowestEnergy();
234 end
235 end
236 end
237 end
238 end
239end