Vita
evolution.tcc
1/**
2 * \file
3 * \remark This file is part of VITA.
4 *
5 * \copyright Copyright (C) 2013-2023 EOS di Manlio Morini.
6 *
7 * \license
8 * This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
10 * You can obtain one at http://mozilla.org/MPL/2.0/
11 */
12
13#if !defined(VITA_EVOLUTION_H)
14# error "Don't include this file directly, include the specific .h instead"
15#endif
16
17#if !defined(VITA_EVOLUTION_TCC)
18#define VITA_EVOLUTION_TCC
19
20namespace term
21{
22///
23/// \return `true` when the user presses the '.' key
24///
25inline bool user_stop()
26{
27 const bool stop(keypressed('.'));
28
29 if (stop)
30 {
31 vitaINFO << "Stopping evolution...";
32 }
33
34 return stop;
35}
36
37///
38/// Resets the term and restores the default signal handlers.
39///
40inline void reset()
41{
42 std::signal(SIGABRT, SIG_DFL);
43 std::signal(SIGINT, SIG_DFL);
44 std::signal(SIGTERM, SIG_DFL);
45
46 term_raw_mode(false);
47}
48
49///
50/// If the program receives a SIGABRT / SIGINT / SIGTERM, it must handle
51/// the signal and reset the terminal to the initial state.
52///
53inline void signal_handler(int signum)
54{
55 term::reset();
56
57 std::raise(signum);
58}
59
60///
61/// Sets the term in raw mode and handles the interrupt signals.
62///
63inline void set()
64{
65 // Install our signal handler.
66 std::signal(SIGABRT, term::signal_handler);
67 std::signal(SIGINT, term::signal_handler);
68 std::signal(SIGTERM, term::signal_handler);
69
70 term_raw_mode(true);
71}
72} // namespace term
73
74///
75/// \param[in] p the current problem
76/// \param[in] eva evaluator used during the evolution
77///
78template<class T, template<class> class ES>
79evolution<T, ES>::evolution(const problem &p, evaluator<T> &eva)
80 : pop_(p), eva_(eva), es_(pop_, eva_, &stats_), after_generation_callback_()
81{
82 Ensures(is_valid());
83}
84
85///
86/// Sets a callback function called at the end of every generation.
87///
88/// \param[in] f callback function
89/// \return a reference to `*this` object (fluent interface)
90///
91template<class T, template<class> class ES>
92evolution<T, ES> &evolution<T, ES>::after_generation(
93 after_generation_callback_t f)
94{
95 after_generation_callback_ = f;
96 return *this;
97}
98
99///
100/// \param[in] s an up to date evolution summary
101/// \return `true` when evolution should be interrupted
102///
103template<class T, template<class> class ES>
104bool evolution<T, ES>::stop_condition(const summary<T> &s) const
105{
106 const auto generations(pop_.get_problem().env.generations);
107 Expects(generations);
108
109 // Check the number of generations.
110 if (s.gen > generations)
111 return true;
112
113 if (term::user_stop())
114 return true;
115
116 // Check strategy specific stop conditions.
117 return es_.stop_condition();
118}
119
120///
121/// \return statistical information about the population
122///
123template<class T, template<class> class ES>
124analyzer<T> evolution<T, ES>::get_stats() const
125{
126 analyzer<T> az;
127
128 for (auto it(pop_.begin()), end(pop_.end()); it != end; ++it)
129 az.add(*it, eva_(*it), it.layer());
130
131 return az;
132}
133
134///
135/// Saves working / statistical informations in a log file.
136///
137/// \param[in] run_count run number
138///
139/// Data are written in a CSV-like fashion and are partitioned in blocks
140/// separated by two blank lines:
141///
142/// [BLOCK_1]\n\n
143/// [BLOCK_2]\n\n
144/// ...
145/// [BLOCK_x]
146///
147/// where each block is a set of line like this:
148///
149/// data_1 [space] data_2 [space] ... [space] data_n
150///
151/// We use this format, instead of XML, because statistics are produced
152/// incrementally and so it's simple and fast to append new data to a
153/// CSV-like file. Note also that it's simple to extract and plot data with
154/// GNU Plot.
155///
156template<class T, template<class> class ES>
157void evolution<T, ES>::log_evolution(unsigned run_count) const
158{
159 static unsigned last_run(0);
160
161 const auto &env(pop_.get_problem().env);
162
163 const auto fullpath([env](const std::filesystem::path &f)
164 {
165 return env.stat.dir / f;
166 });
167
168 if (!env.stat.dynamic_file.empty())
169 {
170 std::ofstream f_dyn(fullpath(env.stat.dynamic_file), std::ios_base::app);
171 if (f_dyn.good())
172 {
173 if (last_run != run_count)
174 f_dyn << "\n\n";
175
176 f_dyn << run_count << ' ' << stats_.gen;
177
178 if (stats_.best.solution.empty())
179 f_dyn << " ?";
180 else
181 f_dyn << ' ' << stats_.best.score.fitness[0];
182
183 f_dyn << ' ' << stats_.az.fit_dist().mean()[0]
184 << ' ' << stats_.az.fit_dist().standard_deviation()[0]
185 << ' ' << stats_.az.fit_dist().entropy()
186 << ' ' << stats_.az.fit_dist().min()[0]
187 << ' ' << static_cast<unsigned>(stats_.az.length_dist().mean())
188 << ' ' << stats_.az.length_dist().standard_deviation()
189 << ' ' << static_cast<unsigned>(stats_.az.length_dist().max())
190 << ' ' << stats_.mutations
191 << ' ' << stats_.crossovers
192 << ' ' << stats_.az.functions(0)
193 << ' ' << stats_.az.terminals(0)
194 << ' ' << stats_.az.functions(1)
195 << ' ' << stats_.az.terminals(1);
196
197 for (unsigned active(0); active <= 1; ++active)
198 for (const auto &symb_stat : stats_.az)
199 f_dyn << ' ' << symb_stat.first->name()
200 << ' ' << symb_stat.second.counter[active];
201
202 f_dyn << " \"";
203 if (!stats_.best.solution.empty())
204 f_dyn << out::in_line << stats_.best.solution;
205 f_dyn << "\"\n";
206 }
207 }
208
209 if (!env.stat.population_file.empty())
210 {
211 std::ofstream f_pop(fullpath(env.stat.population_file),
212 std::ios_base::app);
213 if (f_pop.good())
214 {
215 if (last_run != run_count)
216 f_pop << "\n\n";
217
218 for (const auto &f : stats_.az.fit_dist().seen())
219 // f.first: value, f.second: frequency
220 f_pop << run_count << ' ' << stats_.gen << ' '
221 << std::fixed << std::scientific
222 << std::setprecision(
223 std::numeric_limits<fitness_t::value_type>::digits10 + 2)
224 << f.first[0] << ' ' << f.second << '\n';
225 }
226 }
227
228 es_.log_strategy(last_run, run_count);
229
230 if (last_run != run_count)
231 last_run = run_count;
232}
233
234///
235/// Prints evolution information (if `log::reporting_level >= log::lOUTPUT`).
236///
237/// \param[in] k current generation
238/// \param[in] run_count total number of runs planned
239/// \param[in] summary if `true` prints a summary line
240/// \param[in] from_last_msg time elapsed from the last message
241///
242template<class T, template<class> class ES>
243void evolution<T, ES>::print_progress(unsigned k, unsigned run_count,
244 bool summary, timer *from_last_msg) const
245{
246 if (log::lOUTPUT >= log::reporting_level)
247 {
248 const unsigned perc(100 * k / pop_.individuals());
249 if (summary)
250 std::cout << "Run " << run_count << '.' << std::setw(6)
251 << stats_.gen << " (" << std::setw(3)
252 << perc << "%): fitness " << stats_.best.score.fitness
253 << '\n';
254 else
255 std::cout << "Crunching " << run_count << '.' << stats_.gen << " ("
256 << std::setw(3) << perc << "%)\r";
257
258 std::cout << std::flush;
259
260 from_last_msg->restart();
261 }
262}
263
264///
265/// The evolutionary core loop.
266///
267/// \param[in] run_count run number (used for printing and logging)
268/// \param[in] shake the "shake data" function. It's used to alter the
269/// training set so that evolution would take place in a
270/// dynamic environment
271/// \return a partial summary of the search (see notes)
272///
273/// The genetic programming loop:
274///
275/// * select the individual(s) to participate (default algorithm: tournament
276/// selection) in the genetic operation;
277/// * perform genetic operation creating a new offspring individual;
278/// * place the offspring into the original population (steady state)
279/// replacing a bad individual.
280///
281/// This whole process repeats until the termination criteria is satisfied.
282/// With any luck, it will produce an individual that solves the problem at
283/// hand.
284///
285/// \note
286/// The return value is a partial summary: the `measurement` section is only
287/// partially filled (fitness) since many metrics are expensive to calculate
288/// and not significative for all kind of problems (e.g. f1-score for a
289/// symbolic regression problem). The src_search class has a simple scheme to
290/// request the computation of additional metrics.
291///
292template<class T, template<class> class ES>
293template<class S>
294const summary<T> &evolution<T, ES>::run(unsigned run_count, S shake)
295{
296 stats_.clear();
297 stats_.best.solution = pop_[{0, 0}];
298 stats_.best.score.fitness = eva_(stats_.best.solution);
299
300 timer measure;
301 timer from_last_msg;
302
303 bool stop(false);
304 term::set();
305
306 es_.init(); // customizatin point for strategy-specific initialization
307
308 for (stats_.gen = 0; !stop_condition(stats_) && !stop; ++stats_.gen)
309 {
310 if (shake(stats_.gen))
311 {
312 // The `shake` functions clear cached fitness values (they refer to the
313 // previous dataset). So we must recalculate the fitness of the best
314 // individual found.
315 assert(!stats_.best.solution.empty());
316 stats_.best.score.fitness = eva_(stats_.best.solution);
317
318 print_progress(0, run_count, true, &from_last_msg);
319 }
320
321 stats_.az = get_stats();
322 log_evolution(run_count);
323
324 for (unsigned k(0); k < pop_.individuals() && !stop; ++k)
325 {
326 if (from_last_msg.elapsed() > std::chrono::seconds(2))
327 {
328 print_progress(k, run_count, false, &from_last_msg);
329
330 stop = term::user_stop();
331 }
332
333 // --------- SELECTION ---------
334 auto parents(es_.selection.run());
335
336 // --------- CROSSOVER / MUTATION ---------
337 auto off(es_.recombination.run(parents));
338
339 // --------- REPLACEMENT --------
340 const auto before(stats_.best.score.fitness);
341 es_.replacement.run(parents, off, &stats_);
342
343 if (stats_.best.score.fitness != before)
344 print_progress(k, run_count, true, &from_last_msg);
345 }
346
347 stats_.elapsed = measure.elapsed();
348
349 es_.after_generation(); // hook for strategy-specific bookkeeping
350 if (after_generation_callback_)
351 after_generation_callback_(pop_, stats_);
352 }
353
354 vitaINFO << "Elapsed time: "
355 << std::chrono::duration<double>(stats_.elapsed).count()
356 << "s" << std::string(10, ' ');
357
358 term::reset();
359 return stats_;
360}
361
362///
363/// A shortcut to call the `run` method without a shake function.
364///
365template<class T, template<class> class ES>
366const summary<T> &evolution<T, ES>::run(unsigned run_count)
367{
368 return run(run_count, [](unsigned) { return false; });
369}
370
371///
372/// \return `true` if object passes the internal consistency check
373///
374template<class T, template<class> class ES>
375bool evolution<T, ES>::is_valid() const
376{
377 return true;
378}
379#endif // include guard