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