1## Copyright (C) 2015 Daniel Kraft <d@domob.eu> 2## GNU Octave level-set package. 3## 4## This program is free software: you can redistribute it and/or modify 5## it under the terms of the GNU General Public License as published by 6## the Free Software Foundation, either version 3 of the License, or 7## (at your option) any later version. 8## 9## This program is distributed in the hope that it will be useful, 10## but WITHOUT ANY WARRANTY; without even the implied warranty of 11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12## GNU General Public License for more details. 13## 14## You should have received a copy of the GNU General Public License 15## along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17## -*- texinfo -*- 18## @deftypefn {Function File} {[@var{s}, @var{log}] =} so_replay_descent (@var{file}, @var{init}) 19## @deftypefnx {Function File} {[@var{s}, @var{log}] =} so_replay_descent (@var{file}, @var{init}, @var{nSteps}) 20## 21## Replay a descent file saved by @code{so_save_descent}. @var{file} should 22## be either an already open file descriptor or a file name to be opened 23## in @qcode{"rb"} mode. 24## 25## @var{init} must be a function that can be called with the ``header'' 26## saved in the descent log. It should return the @var{data} structure 27## to use for the descent run. The handlers defined in 28## @code{@var{data}.handler} will be called in the same way as they 29## would by @code{so_run_descent}. Also similar log chatter will be printed 30## if @code{@var{data}.p.verbose} is @code{true}. 31## 32## If the state structs were compressed when saving with @code{so_save_descent}, 33## a routine should be provided to ``uncompress'' them as necessary. This 34## function should be defined in @code{@var{data}.compress.load.state} 35## and should return the uncompressed state struct when called 36## with the arguments @code{(@var{s}, @var{data})} where @var{s} 37## is the compressed struct. 38## 39## If @var{nSteps} is not given, the file is read until EOF is encountered. 40## If @var{nSteps} is present, the descent will be run until either 41## this number is reached or @code{@var{data}.cb.check_stop} returns true. 42## If the descent log does not contain enough data for this condition to be 43## fulfilled, more steps will be performed according to @code{so_run_descent}. 44## In this case, the callbacks in @code{@var{data}.cb} must be set in the 45## same way as for @code{so_run_descent}. 46## 47## Returned are the final state and the produced log structure, 48## in the same way as by @code{so_run_descent}. 49## 50## This routine requires @code{fload} from the @code{parallel} package 51## to be available. 52## 53## @seealso{so_explore_descent, so_run_descent, so_save_descent, fload} 54## @end deftypefn 55 56function [s, descentLog] = so_replay_descent (file, init, nSteps) 57 if (nargin () < 2 || nargin () > 3) 58 print_usage (); 59 endif 60 61 if (nargin () == 3) 62 forceSteps = true; 63 if (!isscalar (nSteps) || nSteps != round (nSteps) || nSteps < 1) 64 print_usage (); 65 endif 66 else 67 forceSteps = false; 68 endif 69 70 openFile = ischar (file); 71 if (!openFile && (!isnumeric (file) || !isscalar (file))) 72 error ("FILE must be a string or file ID"); 73 endif 74 75 hasFload = exist ("fload"); 76 if (hasFload != 2 && hasFload != 3) 77 error ("'fload' is not available"); 78 endif 79 80 % Open file and read header. 81 if (openFile) 82 fd = fopen (file, "rb"); 83 if (fd == -1) 84 error ("failed to open file '%s' for reading the descent log", file); 85 endif 86 else 87 fd = file; 88 endif 89 header = fload (fd); 90 91 % Call init method and load initial state. 92 data = init (header); 93 data.s = so_load_compressed (fd, "state", data); 94 95 % Add some (possibly) missing stuff with defaults. 96 if (!isfield (data, "log")) 97 data.log = struct (); 98 endif 99 if (!isfield (data, "handler")) 100 data.handler = struct (); 101 endif 102 if (!isfield (data, "cb")) 103 data.cb = struct (); 104 endif 105 if (!isfield (data.cb, "check_stop")) 106 data.cb.check_stop = @() false; 107 endif 108 109 % Call "initialised" handler and save initial state struct. 110 data.log.s0 = data.s; 111 if (isfield (data.handler, "initialised")) 112 data.log = data.handler.initialised (data); 113 endif 114 115 % Do the iteration. 116 costs = [data.s.cost]; 117 k = 0; 118 while (true) 119 if (ferror (fd)) 120 error ("error reading descent log"); 121 endif 122 123 % Honour stopping condition if nSteps is given. 124 if (forceSteps && (k >= nSteps || data.cb.check_stop (data.s))) 125 break; 126 endif 127 128 % Read next data and check for EOF. 129 try 130 f = fload (fd); 131 catch 132 if (feof (fd)) 133 break; 134 endif 135 error ("error reading descent log"); 136 end_try_catch 137 dJ = fload (fd); 138 step = fload (fd); 139 newS = so_load_compressed (fd, "state", data); 140 if (ferror (fd)) 141 error ("error reading descent log"); 142 endif 143 144 % Call handlers. 145 ++k; 146 if (data.p.verbose) 147 printf ("\nDescent iteration %d...\n", k); 148 printf ("Starting cost: %.6f\n", data.s.cost); 149 printf ("Directional derivative: %.6f\n", dJ); 150 printf ("Armijo step %.6f: cost = %.6f\n", step, newS.cost); 151 endif 152 if (isfield (data.handler, "before_step")) 153 data.log = data.handler.before_step (k, data); 154 endif 155 if (isfield (data.handler, "direction")) 156 data.log = data.handler.direction (k, f, dJ, data); 157 endif 158 if (isfield (data.handler, "after_step")) 159 data.log = data.handler.after_step (k, step, newS, data); 160 endif 161 162 % Update cost tracking and switch to new state. 163 costs(k + 1) = newS.cost; 164 data.s = newS; 165 endwhile 166 167 % Make sure to close the file now. Record EOF condition 168 % before we do so. 169 haveEOF = feof (fd); 170 if (openFile) 171 fclose (fd); 172 endif 173 174 % If we have an EOF condition, see if we need more steps. 175 if (haveEOF && forceSteps) 176 assert (k < nSteps && !data.cb.check_stop (data.s)); 177 178 if (k == 0) 179 error ("no data in descent log"); 180 endif 181 182 % Call through to so_run_descent, in a special "continuation" 183 % mode (defined internally for this purpose). 184 data._descentContinuation = struct ("k", k, "step", step, "costs", costs); 185 [s, descentLog] = so_run_descent (nSteps, [], data); 186 return; 187 endif 188 189 % Finish everything. 190 data.log.steps = k; 191 assert (length (costs) == k + 1); 192 data.log.costs = costs; 193 if (isfield (data.handler, "finished")) 194 data.log = data.handler.finished (data); 195 endif 196 197 % Fill return values. 198 s = data.s; 199 descentLog = data.log; 200endfunction 201 202%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 203% Tests. 204 205% Test for errors. 206%!error <Invalid call to> 207%! so_replay_descent (1); 208%!error <Invalid call to> 209%! so_replay_descent (1, 2, 3, 4); 210%!error <Invalid call to> 211%! so_replay_descent (1, 2, 0); 212%!error <Invalid call to> 213%! so_replay_descent (1, 2, 1.5); 214%!error <Invalid call to> 215%! so_replay_descent (1, 2, [2, 3]); 216%!error <FILE must be a string or file ID> 217%! so_replay_descent (struct (), 2); 218%!error <FILE must be a string or file ID> 219%! so_replay_descent ([2, 3], 2); 220%!error <'fload' is not available> 221%! pkg unload parallel; 222%! so_replay_descent ("foo", 2); 223 224% Define compression / uncompression functions. They do not really 225% "compress", but instead manipulate the data in ways such that it 226% is later possible to check that they have been called correctly. 227% 228%!function x = compressionCheckStruct (s, data) 229%! x = struct ("weight", data.p.weight, "cost", s.cost); 230%!endfunction 231% 232%!function checkCompression (s, data, field) 233%! assert (isfield (s, field)); 234%! assert (getfield (s, field), compressionCheckStruct (s, data)); 235%!endfunction 236% 237%!function s = compressState (s, data) 238%! s.compressed = compressionCheckStruct (s, data); 239%!endfunction 240% 241%!function s = uncompressState (s, data) 242%! checkCompression (s, data, "compressed"); 243%! s.uncompressed = compressionCheckStruct (s, data); 244%!endfunction 245 246% Sample handlers that just build up "some" array from all the data 247% they are passed. This is used to compare replayed and computed runs. 248% 249%!function log = initialised (data) 250%! log = data.log; 251%! log.mashUp = [data.s.a, data.s.b]; 252%!endfunction 253% 254%!function log = beforeStep (k, data) 255%! log = data.log; 256%! log.mashUp = [log.mashUp, k, data.s.vol]; 257%! 258%! if (data.checkUncompression) 259%! checkCompression (data.s, data, "uncompressed"); 260%! endif 261%!endfunction 262% 263%!function log = direction (k, f, dJ, data) 264%! log = data.log; 265%! log.mashUp = [log.mashUp, k, mean(f), sum(f), dJ, data.s.integ]; 266%!endfunction 267% 268%!function log = afterStep (k, t, s, data) 269%! log = data.log; 270%! log.mashUp = [log.mashUp, k, t, s.a, s.b, s.cost, data.s.cost]; 271%! 272%! if (data.checkUncompression) 273%! checkCompression (s, data, "uncompressed"); 274%! endif 275%!endfunction 276% 277%!function log = finished (data) 278%! log = data.log; 279%! log.mashUp = [log.mashUp, data.s.cost, log.steps, log.costs]; 280%!endfunction 281 282% Define a function that initialises the problem data. 283%!function data = getData () 284%! data = struct (); 285%! data.p = so_init_params (false); 286%! data.p.vol = 10; 287%! data.p.weight = 50; 288%! 289%! n = 100; 290%! x = linspace (-10, 10, n); 291%! h = x(2) - x(1); 292%! data.g = struct ("x", x, "h", h); 293%! 294%! data = so_example_problem (data); 295%! data.handler = struct ("initialised", @initialised, ... 296%! "before_step", @beforeStep, ... 297%! "direction", @direction, ... 298%! "after_step", @afterStep, ... 299%! "finished", @finished); 300%! 301%! % Unless explicitly set, do not expect compressed states. 302%! data.checkUncompression = false; 303%! 304%! data.phi0 = ls_genbasic (data.g.x, "box", -3, 5); 305%!endfunction 306 307% Get data and enable compression. 308%!function data = getDataForCompression (uncompress) 309%! data = getData (); 310%! data.compress = struct (); 311%! data.compress.save = struct ("state", @compressState); 312%! data.compress.load = struct ("state", @uncompressState); 313%! data.checkUncompression = uncompress; 314%!endfunction 315 316% Compare results (s, log) two runs and check if the mash-ups match. 317% We cannot fully compare the log structs, though, since they contain 318% private data used for saving. 319%!function compareResults (s1, l1, s2, l2) 320%! assert (s2, s1); 321%! assert (l2.mashUp, l1.mashUp); 322%!endfunction 323 324% Create a basic log and replay it without forcing steps. This tests 325% both saving to a file name and file descriptor. 326%!test 327%! pkg load parallel; 328%! 329%! nSteps = 10; 330%! baseData = getData (); 331%! 332%! f = tempname (); 333%! data = so_save_descent (f, struct (), baseData); 334%! [s1, l1] = so_run_descent (nSteps, data.phi0, data); 335%! [s2, l2] = so_replay_descent (f, @getData); 336%! unlink (f); 337%! compareResults (s1, l1, s2, l2); 338%! 339%! f = tmpfile (); 340%! data = so_save_descent (f, struct (), baseData); 341%! [s1, l1] = so_run_descent (nSteps, data.phi0, data); 342%! frewind (f); 343%! [s2, l2] = so_replay_descent (f, @getData); 344%! fclose (f); 345%! compareResults (s1, l1, s2, l2); 346%! 347%! [pr, pw] = pipe (); 348%! data = so_save_descent (pw, struct (), baseData); 349%! [s1, l1] = so_run_descent (nSteps, data.phi0, data); 350%! fclose (pw); 351%! [s2, l2] = so_replay_descent (pr, @getData); 352%! fclose (pr); 353%! compareResults (s1, l1, s2, l2); 354 355% Check for "no data" error if we need to compute more steps 356% but *none* are saved. Also check that it *is* ok to have 357% zero steps if the stopping criterion stops immediately. 358% 359%!test 360%! pkg load parallel; 361%! 362%! baseData = getData (); 363%! [pr, pw] = pipe (); 364%! data = so_save_descent (pw, struct (), baseData); 365%! data.cb.check_stop = @() true; 366%! [s1, l1] = so_run_descent (1, data.phi0, data); 367%! fclose (pw); 368%! assert (l1.steps, 0); 369%! [s2, l2] = so_replay_descent (pr, @getData); 370%! fclose (pr); 371%! compareResults (s1, l1, s2, l2); 372%! 373%! baseData = getData (); 374%! [pr, pw] = pipe (); 375%! data = so_save_descent (pw, struct (), baseData); 376%! [s1, l1] = so_run_descent (1, data.phi0, data); 377%! fclose (pw); 378%! [s2, l2] = so_replay_descent (pr, @getData, 1); 379%! fclose (pr); 380%! compareResults (s1, l1, s2, l2); 381% 382%!error <no data in descent log> 383%! pkg load parallel; 384%! 385%! baseData = getData (); 386%! [pr, pw] = pipe (); 387%! data = so_save_descent (pw, struct (), baseData); 388%! data.cb.check_stop = @() true; 389%! [s1, l1] = so_run_descent (1, data.phi0, data); 390%! fclose (pw); 391%! [s2, l2] = so_replay_descent (pr, @getData, 1); 392%! fclose (pr); 393%! compareResults (s1, l1, s2, l2); 394 395% Check forcing the steps to be less or to require computation. 396%!test 397%! pkg load parallel; 398%! 399%! nStepsShort = 5; 400%! nStepsLong = 10; 401%! 402%! baseData = getData (); 403%! [prl, pwl] = pipe (); 404%! data = so_save_descent (pwl, struct (), baseData); 405%! [sl, ll] = so_run_descent (nStepsLong, data.phi0, data); 406%! fclose (pwl); 407%! 408%! baseData = getData (); 409%! [prs, pws] = pipe (); 410%! data = so_save_descent (pws, struct (), baseData); 411%! [ss, ls] = so_run_descent (nStepsShort, data.phi0, data); 412%! fclose (pws); 413%! 414%! [s, l] = so_replay_descent (prl, @getData, nStepsShort); 415%! fclose (prl); 416%! compareResults (s, l, ss, ls); 417%! 418%! [s, l] = so_replay_descent (prs, @getData, nStepsLong); 419%! fclose (prs); 420%! compareResults (s, l, sl, ll); 421 422% Check compression / uncompression feature. 423%!test 424%! pkg load parallel; 425%! 426%! nSteps = 10; 427%! [pr, pw] = pipe (); 428%! 429%! data = getDataForCompression (false); 430%! data = so_save_descent (pw, struct (), data); 431%! [s1, l1] = so_run_descent (nSteps, data.phi0, data); 432%! fclose (pw); 433%! 434%! init = @(header) getDataForCompression (true); 435%! [s2, l2] = so_replay_descent (pr, init); 436%! fclose (pr); 437%! 438%! % No result comparison here, since the compress / uncompress 439%! % functions introduced changes to the state structs. 440 441%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 442% Demo. 443 444% This is the "same" demo as for so_run_descent. The difference is just 445% that the descent is first saved and only later replayed to produce 446% the actual plots. 447% 448%!demo 449%! pkg load parallel; 450%! 451%! data = struct (); 452%! data.p = so_init_params (false); 453%! data.p.vol = 10; 454%! data.p.weight = 50; 455%! 456%! n = 100; 457%! x = linspace (-10, 10, n); 458%! h = x(2) - x(1); 459%! data.g = struct ("x", x, "h", h); 460%! 461%! data = so_example_problem (data); 462%! phi0 = ls_genbasic (data.g.x, "box", -3, 7); 463%! 464%! printf ("Computing descent...\n"); 465%! [pr, pw] = pipe (); 466%! d = data; 467%! d.handler = struct (); 468%! d = so_save_descent (pw, struct (), d); 469%! s = so_run_descent (5, phi0, d); 470%! fclose (pw); 471%! printf ("Final cost: %.6d\n", s.cost); 472%! 473%! printf ("\nNow replaying...\n"); 474%! d = data; 475%! d.p.verbose = true; 476%! init = @() d; 477%! s = so_replay_descent (pr, init); 478%! fclose (pr); 479%! printf ("Final cost: %.6d\n", s.cost); 480