#!/usr/bin/env sh
# -*- wisp -*-
exec guile -L $(dirname $(dirname $(realpath "$0"))) --language=wisp -e '(@@ (examples benchmark) main)' -l $(dirname $(realpath "$0"))/cholesky.w -l $(dirname $(realpath "$0"))/ensemble-estimation.w -s "$0" "$@"
; !#

define-module : examples benchmark

import : statprof
         ice-9 optargs
         ice-9 format
         srfi srfi-1
         srfi srfi-42 ; list-ec
         srfi srfi-43 ; vector-append
         ice-9 pretty-print
         system vm program



;; stddev from rosetta code: http://rosettacode.org/wiki/Standard_deviation#Scheme
define : stddev nums
    sqrt
        -
            / : apply + : map (lambda (i) (* i i)) nums
                length nums
            expt (/ (apply + nums) (length nums)) 2


define : stddev-unbiased-normal nums
    . "Approximated unbiased standard deviation for the normal distribution

    'for n = 3 the bias is equal to 1.3%, and for n = 9 the bias is already less than 0.1%.'
     - https://en.wikipedia.org/wiki/Standard_deviation#Unbiased_sample_standard_deviation
    "
    sqrt
        -
            / : apply + : map (lambda (i) (* i i)) nums
                - (length nums) 1.5
            expt (/ (apply + nums) (length nums)) 2


define : running-stddev nums
  define : running-stddev-2 num
      set! nums : cons num nums
      stddev nums
  . running-stddev-2

define* : benchmark-run-single fun #:key (min-seconds 0.1)
  ;; trigger garbage collection before stats collection to avoid polluting the data
  gc
  let profiler : (loop-num 4)
    let : : t : get-internal-real-time
      with-output-to-string
        lambda ()
          let lp : (i loop-num)
            : λ () : fun
            when (> i 0)
              lp (- i 1)
      let*
        : dt : - (get-internal-real-time) t
          seconds : / (exact->inexact dt) internal-time-units-per-second
        ;; pretty-print : list dt seconds loop-num
        if {seconds > min-seconds}
            /  seconds loop-num ;; this wastes less than {(4 * ((4^(i-1)) - 1)) / 4^i} fractional data but gains big in simplicity
            profiler (* 4 loop-num) ;; for fast functions I need to go up rapidly, for slow ones I need to avoid overshooting

;; Define targets for the data aquisition
define max-relative-uncertainty 0.3 ;; 3 sigma from 0
define max-absolute-uncertainty-seconds 1.e-3 ;; 1ms, required to ensure that the model uses the higher values (else they would have huge uncertainties). If you find you need more, use a smaller test case.
define min-aggregated-runtime-seconds 1.e-5 ;; 10μs ~ 30k cycles
define max-iterations 128 ;; at most 128 samples, currently corresponding to at least 1ms each, so a benchmark of a fast function should take at most 0.1 seconds.

define* : benchmark-run fun
    ;; pretty-print fun
    let lp : (min-seconds min-aggregated-runtime-seconds) (sampling-steps 8) ;; start with at least 3 sampling steps to make the approximations in stddev-unbiased-normal good enough
        let*
          : res : list-ec (: i sampling-steps) : benchmark-run-single fun #:min-seconds min-seconds
            std : stddev-unbiased-normal res
            mean : / (apply + res) sampling-steps
           ;; pretty-print : list mean '± std min-seconds sampling-steps
           if : or {sampling-steps > max-iterations} : and {std < {mean * max-relative-uncertainty}} {std < max-absolute-uncertainty-seconds}
              . mean
              lp (* 2 min-seconds) (* 2 sampling-steps) ;; should decrease σ by factor 2 or √2 (for slow functions)

define loopcost
  benchmark-run (λ() #f)


;; TODO: Simplify #:key setup -> . setup
define* : benchmark-fun fun #:key setup
  when setup
    setup
  - : benchmark-run fun
    . loopcost

define-syntax benchmark
  ;; one single benchmark
  lambda : x
    syntax-case x (:let :setup)
      : _ thunk :setup setup-thunk :let let-thunk args ...
        #' benchmark thunk :let let-thunk :setup setup-thunk args ... 
      : _ thunk :let let-thunk :setup setup-thunk args ...
        #' benchmark thunk :let let-thunk #:setup (lambda () setup-thunk) args ... 
      : _ thunk :setup setup-thunk args ...
        #' benchmark thunk #:setup (lambda () setup-thunk) args ... 
      : _ thunk :let let-thunk args ...
        #' let let-thunk
           benchmark thunk args ... 
      : _ thunk args ...
        #' benchmark-fun
         . (lambda () thunk) args ...

define : logiota steps start stepsize
    . "Create numbers evenly spread in log space"
    let*
        : logstart : log (+ start 1)
          logstep : / (- (log (+ start (* stepsize (- steps 1)))) logstart) (- steps 1)
        map inexact->exact : map round : map exp : iota steps logstart logstep 


;; interesting functions to benchmark:
;; - TODO: add to set/alist/hashmap
;; - TODO: retrieve from alist/hashmap
;; - TODO: sort
;; - ... see https://wiki.python.org/moin/TimeComplexity

;; operation benchmarks
;; - TODO: or #f #t
;; - TODO: and #t #f
;; - TODO: math

;; List benchmarks:
;; - list-copy (py-copy)
;; - cons (py-push / py-append)
;; - car (py-pop)
;; - list-ref (py-get-item)
;; - list-set! (py-set-item)
;; - take + drop (py-get-slice)
;; - take-right + drop-right (py-get-slice)
;; - last
;; - append (py-extend)
;; - delete (py-delete-item)
;; - min (py-min)
;; - max (py-max)
;; - member (py-in)
;; - reverse (py-reversed)
;; - length (py-len)
define : bench-append param-list
  . "Test (append a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (append a b) :let ((a (iota N))(b (iota m)))
  zip param-list : map f param-list

define : bench-ref param-list
  . "Test (list-ref a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (list-ref a b) :let ((a (iota (max N m)))(b (- m 1)))
  zip param-list : map f param-list

define : bench-car param-list
  . "Test (coar a b) with element A and list B of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0))
            benchmark (car b) :let ((b (iota N)))
  zip param-list : map f param-list

define : bench-cdr param-list
  . "Test (cdr a b) with element A and list B of lengths from the param-list (note: this is really, really fast)."
  define : f x
     let : (N (list-ref x 0))
            benchmark (cdr b) :let ((b (iota N)))
  zip param-list : map f param-list

define : bench-sort param-list
  . "Test (sort a <) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0))
            benchmark (sort a <) :let ((a (iota N)))
  zip param-list : map f param-list

define : bench-cons param-list
  . "Test (cons a b) with element A and list B of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (cons b a) :let ((a (iota N))(b m))
  zip param-list : map f param-list

define : bench-copy param-list
  . "Test (cons a b) with element A and list B of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0))
            benchmark (list-copy a) :let ((a (iota N)))
  zip param-list : map f param-list

define : bench-set param-list
  . "Test (cons a b) with element A and list B of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (list-set! a b #t) :let ((a (iota (max N m)))(b (- m 1)))
  zip param-list : map f param-list

define : bench-copy param-list
  . "Copy a list of length N."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (set! b (list-copy a)) :let ((a (iota N))(b #f))
  zip param-list : map f param-list

define : bench-getslice-left param-list
  . "Get a slice from left."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (take (drop a b) b) :let ((a (iota (max N (* 2 m))))(b m))
  zip param-list : map f param-list

define : bench-getslice-right param-list
  . "Get a slice from right."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (take-right (drop-right a b) b) :let ((a (iota (max N (* 2 m))))(b m))
  zip param-list : map f param-list

define : bench-length param-list
  . "Get the length of a list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (length a) :let ((a (iota N))(b m))
  zip param-list : map f param-list

define : bench-reverse param-list
  . "Reverse a list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (reverse a) :let ((a (iota N))(b m))
  zip param-list : map f param-list

define : bench-member param-list
  . "Check (member b a)."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (member b a) :let ((a (iota N)) (b (- m 1)))
  zip param-list : map f param-list

define : bench-last param-list
  . "Get the last element of a list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (last a) :let ((a (iota N))(b m))
  zip param-list : map f param-list

define : bench-max param-list
  . "Get the maximum value of a list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (apply max a) :let ((a (iota N))(b m))
  zip param-list : map f param-list

define : bench-min param-list
  . "Get the minimum value of a list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (apply min a) :let ((a (iota N))(b m))
  zip param-list : map f param-list

define : bench-delete param-list
  . "Check (member b a)."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (delete b a) :let ((a (iota N)) (b (- m 1)))
  zip param-list : map f param-list

define : bench-delete-smaller param-list
  . "Check (delete b a <)."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (delete b a <) :let ((a (iota N)) (b (- m 1)))
  zip param-list : map f param-list

define : bench-delete-larger param-list
  . "Check (delete b a >)."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (delete b a >) :let ((a (iota N)) (b (- m 1)))
  zip param-list : map f param-list

define : bench-delete! param-list
  . "Check (member b a)."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (delete! b a) :let ((a (iota N)) (b (- m 1)))
  zip param-list : map f param-list


;; TODO: VList benchmarks


;; String benchmarks
define : bench-append-string param-list
  . "Test (string-append a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (string-append a b) :let ((a (make-string N))(b (make-string m)))
  zip param-list : map f param-list

define : bench-append-string/shared param-list
  . "Test (string-append/shared a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (string-append/shared a b) :let ((a (make-string N))(b (make-string m)))
  zip param-list : map f param-list

;; Vector benchmarks
define : bench-append-vector param-list
  . "Test (vector-append a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (vector-append a b) :let ((a (make-vector N 1))(b (make-vector m 1)))
  zip param-list : map f param-list

;; Map/set benchmarks
define : bench-assoc param-list
  . "Test (assoc a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (assoc a b) :let ((a m)(b (reverse (fold (λ (x y z) (acons x y z)) '() (iota N 1) (iota N 1)))))
  zip param-list : map f param-list


;; opertations
define : bench-operation-+ param-list
  . "Test (assoc a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (+ a b) :let ((a N)(b m)(c 1)(d 2))
  zip param-list : map f param-list

define : bench-operation-+++ param-list
  . "Test (assoc a b) with lists of lengths from the param-list."
  define : f x
     let : (N (list-ref x 0)) (m (list-ref x 1))
            benchmark (+ (+ (+ a b) c) d) :let ((a N)(b m)(c 50)(d 100))
  zip param-list : map f param-list


;; prepare a multi-function fit
import 
    only : examples ensemble-estimation
         . EnSRF make-covariance-matrix-with-offdiagonals-using-stds 
         . standard-deviation-from-deviations x-deviations->y-deviations
         . x^steps
    only : ice-9 popen
         . open-output-pipe close-pipe

define-syntax-rule : or0 test c ...
    if test : begin c ...
            . 0

define-syntax-rule : oror0 test1 test2 c ...
    if (or test1 test2) : begin c ...
                        . 0

define-syntax-rule : define-quoted sym val
    ;; set the value to true using eval to break the symbol->variable barrier
    primitive-eval `(define ,sym val)

define* 
       H-N-m x pos #:key all const OlogN OsqrtN ON ONlogN ON²
                                 . Ologm Osqrtm Om Omlogm Om²
                                 . OlogNm ONlogm OmlogN ONm
                                 . ONlogNlogN Omlogmlogm
       . "Observation operator. It generates modelled observations from the input.

x are parameters to be optimized, pos is another input which is not optimized. For plain functions it could be the position of the measurement on the x-axis. We currently assume absolute knowledge about the position.
"
       let : (N (first pos)) (m (second pos))
           +
             oror0 all const : list-ref x 0 ; constant value
             ;; pure N
             oror0 all OlogN  : * (list-ref x 1) : log (+ 1 N) ; avoid breakage at pos 0
             oror0 all OsqrtN : * (list-ref x 2) : sqrt N
             oror0 all ON     : * (list-ref x 3) N
             oror0 all ONlogN : * (list-ref x 4) : * N : log (+ 1 N)
             oror0 all ON²    : * (list-ref x 5) : expt N 2
             ;; pure m
             oror0 all Ologm  : * (list-ref x 6) : log (+ 1 m)
             oror0 all Osqrtm : * (list-ref x 7) : sqrt m
             oror0 all Om     : * (list-ref x 8) m
             oror0 all Omlogm : * (list-ref x 9) : * m : log (+ 1 m)
             oror0 all Om²    : * (list-ref x 10) : expt m 2
             ;; mixed terms
             oror0 all OlogNm : * (list-ref x 11) : log (+ 1 N m)
             oror0 all ONlogm : * (list-ref x 12) : * N : log (+ 1 m)
             oror0 all OmlogN : * (list-ref x 13) : * m : log (+ 1 N)
             oror0 all ONm    : * (list-ref x 14) : * N m
             oror0 all ONlogNlogN   : * (list-ref x 15) : * (log (+ 1 N)) (log (+ 1 N)) N
             oror0 all Omlogmlogm   : * (list-ref x 16) : * (log (+ 1 m)) (log (+ 1 m)) m


define : interleave lx lz
  cond
    (null? lx) lz
    else
      cons : car lx
             interleave lz : cdr lx


define : print-fit x σ
    . "Print the big-O parameters which are larger than 2.6σ (2.6 times their standard deviation, roughly 99% / p 0.01)."
    ;; see https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule#/media/File:Standard_score_and_prediction_interval.png
    let : : number-format "~,1,,,,,'ee±~,1,,,,,'ee"
      let big-O
        : names : list "" "log(N)" "sqrt(N)" "N log(N)" "N^2" "log(m)" "sqrt(m)" "m" "m log(m)" "m^2" "log(N + m)" "N log(m)" "m log(N)" "N m" "N log(N)^2" "m^2 N"
          x x
          σ σ
        cond
          : or (null? names) (null? x) (null? σ)
            newline
          : > (abs (car x)) (* 2.6 (car σ)) ;; 99%: 2.6 times standard deviation as significance level
            format #t : string-append number-format " " (car names) "  "
                      . (car x) (car σ)
            big-O (cdr names) (cdr x) (cdr σ)
          else
            big-O (cdr names) (cdr x) (cdr σ)


define : flatten li
         append-ec (: i li) i

;; TODO: add filename and title and fix the units
define* : plot-benchmark-result bench H #:key filename title
     let*
        : ensemble-member-count 32
          ensemble-member-plot-skip 8 ;; must not be zero!
          iterations 4
          y_0 : apply min : map car : map cdr bench
          y_m : * 0.25 : apply max : map car : map cdr bench
          nb : apply max : interleave (map car (map car bench)) (map car (map cdr (map car bench)))
          ;; "const" "log(N)" "sqrt(N)" "N" "N^2" "N^3" "log(m)" "sqrt(m)" "m" "m^2" "m^3" "log(N + m)" "N log(m)" "m log(N)" "N m" "N^2 m" "m^2 N"
          x^b : list y_0 (/ y_m (log nb)) (/ y_m (sqrt nb)) (/ y_m nb) (/ y_m nb nb) (/ y_m nb nb nb) (/ y_m (log nb)) (/ y_m (sqrt nb)) (/ y_m nb) (/ y_m nb nb) (/ y_m nb nb nb) (/ y_m nb nb) (/ y_m nb nb) (/ y_m nb nb nb) (/ y_m nb nb nb) (/ y_m nb nb nb nb) (/ y_m nb nb nb nb)  ; inital guess: constant starting at the first result
          x^b-std : list-ec (: i x^b) (* 2 i) ; inital guess: 200% uncertainty
          P : make-covariance-matrix-with-offdiagonals-using-stds x^b-std
          y⁰-pos : map car bench
          y⁰ : append-map cdr bench
          ;; several iterations to better cope with non-linearity, following http://journals.ametsoc.org/doi/abs/10.1175/MWR-D-11-00176.1 (but globally)
          y⁰-stds : list-ec (: i y⁰) : * (sqrt iterations) : min max-absolute-uncertainty-seconds {max-relative-uncertainty * i} ; enforcing 20% max std in benchmark-run
          R : make-covariance-matrix-with-offdiagonals-using-stds y⁰-stds
          optimized ;; iterate N times
              let lp : (N iterations) (x^b x^b) (P P)
                  let : : optimized : EnSRF H x^b P y⁰ R y⁰-pos ensemble-member-count
                      cond
                         : <= N 1
                            . optimized
                         else
                            let*
                              : x-opt : list-ref optimized 0
                                x-deviations : list-ref optimized 1
                                x-std ;; re-create the ensemble with the new std
                                    list-ec (: i (length x-opt))
                                        apply standard-deviation-from-deviations
                                            list-ec (: j x-deviations) : list-ref j i
                                P : make-covariance-matrix-with-offdiagonals-using-stds x-std
                              lp (- N 1) x-opt P
          x-opt : list-ref optimized 0
          x-deviations : list-ref optimized 1
          x-std 
                list-ec (: i (length x-opt))
                      apply standard-deviation-from-deviations : list-ec (: j x-deviations) : list-ref j i
          y-deviations : x-deviations->y-deviations H x-opt x-deviations y⁰-pos
          y-stds : list-ec (: i y-deviations) : apply standard-deviation-from-deviations i
          y-opt : map (λ (x) (H x-opt x)) y⁰-pos
          x^b-deviations-approx
              list-ec (: i ensemble-member-count)
                   list-ec (: j (length x^b))
                       * : random:normal
                           sqrt : list-ref (list-ref P j) j ; only for diagonal P!
          y^b-deviations : x-deviations->y-deviations H x^b x^b-deviations-approx y⁰-pos
          y-std
             apply standard-deviation-from-deviations
                flatten y-deviations
          y-stds : list-ec (: i y-deviations) : apply standard-deviation-from-deviations i
          y^b-stds : list-ec (: i y^b-deviations) : apply standard-deviation-from-deviations i
 
        ;; print-fit x-std
        when title
            display title
            newline
        print-fit x-opt x-std
        ;; TODO: minimize y-mismatch * y-uncertainty
        format #t "Model standard deviation (uncertainty): ~,4e\n" y-std
        newline
        ; now plot the result
        let : : port : open-output-pipe "python2"
          format port "# encoding: utf-8\n"
          format port "from __future__ import unicode_literals\n"
          format port "import pylab as pl\nimport matplotlib as mpl\n"
          format port "y0 = [float(i) for i in '~A'[1:-1].split(' ')]\n" y⁰
          format port "ystds = [float(i) for i in '~A'[1:-1].split(' ')]\n" y⁰-stds
          format port "ypos1 = [float(i) for i in '~A'[1:-1].split(' ')]\n" : list-ec (: i y⁰-pos) : first i
          format port "ypos2 = [float(i) for i in '~A'[1:-1].split(' ')]\n" : list-ec (: i y⁰-pos) : second i
          format port "yinit = [float(i) for i in '~A'[1:-1].split(' ')]\n" : list-ec (: i y⁰-pos) : H x^b i
          format port "yinitstds = [float(i) for i in '~A'[1:-1].split(' ')]\n" y^b-stds
          format port "yopt = [float(i) for i in '~A'[1:-1].split(' ')]\n" : list-ec (: i y⁰-pos) : H x-opt i
          format port "yoptstds = [float(i) for i in '~A'[1:-1].split(' ')]\n" y-stds
          ;; format port "pl.errorbar(*zip(*sorted(zip(ypos1, yinit))), yerr=zip(*sorted(zip(ypos1, yinitstds)))[1], label='prior vs N')\n"
          format port "pl.errorbar(*zip(*sorted(zip(ypos1, yopt))), yerr=zip(*sorted(zip(ypos1, yoptstds)))[1], marker='H', mew=1, ms=10, linewidth=0.1, label='optimized vs N')\n"
          format port "eb=pl.errorbar(*zip(*sorted(zip(ypos1, y0))), yerr=ystds, alpha=0.6, marker='x', mew=2, ms=10, linewidth=0, label='measurements vs N')\neb[-1][0].set_linewidth(1)\n"
          ;; format port "pl.errorbar(*zip(*sorted(zip(ypos2, yinit))), yerr=zip(*sorted(zip(ypos2, yinitstds)))[1], label='prior vs. m')\n"
          format port "pl.errorbar(*zip(*sorted(zip(ypos2, yopt))), yerr=zip(*sorted(zip(ypos2, yoptstds)))[1], marker='h', mew=1, ms=10, linewidth=0.1, label='optimized vs. m')\n"
          format port "eb=pl.errorbar(*zip(*sorted(zip(ypos2, y0))), yerr=ystds, alpha=0.6, marker='x', mew=2, ms=10, linewidth=0, label='measurements vs. m')\neb[-1][0].set_linewidth(1)\n"
          format port "pl.plot(sorted(ypos1+ypos2), pl.log(sorted(ypos1+ypos2))*(max(y0) / pl.log(max(ypos1+ypos2))), label='log(x)')\n"
          format port "pl.plot(sorted(ypos1+ypos2), pl.sqrt(sorted(ypos1+ypos2))*(max(y0) / pl.sqrt(max(ypos1+ypos2))), label='sqrt(x)')\n"
          format port "pl.plot(sorted(ypos1+ypos2), pl.multiply(sorted(ypos1+ypos2), max(y0) / max(ypos1+ypos2)), label='x')\n"
          list-ec (: step 0 (length x^steps) 4)
               let : : members : list-ref x^steps (- (length x^steps) step 1)
                  list-ec (: member-idx 0 (length members) ensemble-member-plot-skip) ; reversed
                     let : : member : list-ref members member-idx
                       format port "paired = pl.get_cmap('Paired')
cNorm = mpl.colors.Normalize(vmin=~A, vmax=~A)
scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=paired)\n" 0 (length member)
                       list-ec (: param-idx 0 (length member) 4) ; step = 4
                          ;; plot parameter 0
                          let : (offset (/ (apply max (append y⁰ y-opt)) 2)) (spreading (/ (apply max (append y⁰ y-opt)) (- (apply max member) (apply min member)) 2))
                              format port "pl.plot(~A, ~A, marker='.', color=scalarMap.to_rgba(~A), linewidth=0, label='', alpha=0.6, zorder=-1)\n"
                                          . (/ step 1) (+ offset (* spreading (list-ref member param-idx))) param-idx
          format port "pl.legend(loc='upper left', fancybox=True, framealpha=0.5)\n"
          format port "pl.xlabel('position (N or m) / arbitrary units')\n"
          format port "pl.ylabel('time / s')\n"
          format port "pl.title('''~A''')\n" : or title "Operation scaling behaviour"
          format port "pl.text(1, 0, '''fit: ~A''', horizontalalignment='right', verticalalignment='center', transform=pl.gca().transAxes)\n" : with-output-to-string : λ() : print-fit x-opt x-std
          format port "pl.xscale('log')\n"
          ;; format port "pl.yscale('log')\n"
          if filename
              format port "pl.savefig('~A', bbox_inches='tight')\n" filename
              format port "pl.show()\n"
          format port "exit()\n"
          close-pipe port


define : main args
 let : : steps 25
   when : member "--quick" args
       set! max-relative-uncertainty 1.5
       set! max-absolute-uncertainty-seconds 1.e-1
       set! min-aggregated-runtime-seconds 1.e-7
       set! max-iterations 16
       set! steps 5
   let*
      : H : lambda (x pos) (H-N-m x pos #:all #t)
        H-const : lambda (x pos) (H-N-m x pos #:const #t)
        H-log : lambda (x pos) (H-N-m x pos #:const #t #:OlogN #t #:Ologm #t)
        H-lin : lambda (x pos) (H-N-m x pos #:const #t #:ON #t #:Om #t #:ONlogN #t #:Omlogm #t #:ONlogNlogN #t #:Omlogmlogm #t )
        pbr plot-benchmark-result
      let lp
        : N-start `(1    1    1    100   ,(* steps 1000))
          N-step  '(1000 1000 0    0     0)
          m-start '(1    100  1    1     1)
          m-step  '(0    0    1000 1000  1000)
        cond
          : null? N-start
            . #t
          else
            let*
              : N : car N-start
                dN : car N-step
                m : car m-start
                dm : car m-step
                param-list : zip (logiota steps N dN) (logiota steps m dm)
              define : title description
                  string-append description
                    format #f ", ~a ~a"
                      if (equal? dN 0) N "N"
                      if (equal? dm 0) m "m"
              define : filename identifier
                  format #f "/tmp/benchmark-~a-~a-~a.png"
                      . identifier
                      if (equal? dN 0) N "N"
                      if (equal? dm 0) m "m"
              pbr (bench-operation-+ param-list) H
                  . #:title : title "+ N m"
                  . #:filename : filename "operation-plus"
              pbr (bench-operation-+++ param-list) H
                  . #:title : title "+ (+ (+ N m) 50) 100"
                  . #:filename : filename "operation-plus3"
              pbr (bench-ref param-list) H-const
                  . #:title : title "list-ref (iota (max m N)) (- m 1)"
                  . #:filename : filename "list-ref"
              when : equal? dm 0 ;; only over N
                  pbr (bench-car param-list) H-const
                      . #:title : title "car (iota N)"
                      . #:filename : filename "car"
                  pbr (bench-cdr param-list) H-const
                      . #:title : title "cdr (iota N)"
                      . #:filename : filename "cdr"
                  pbr (bench-sort param-list) H-lin
                      . #:title : title "sort (iota N)"
                      . #:filename : filename "sort"
                  pbr (bench-copy param-list) H
                      . #:title : title "list-copy (iota N)"
                      . #:filename : filename "copy"
                  pbr (bench-length param-list) H
                      . #:title : title "length (iota N)"
                      . #:filename : filename "length"
                  pbr (bench-reverse param-list) H
                      . #:title : title "reverse (iota N)"
                      . #:filename : filename "reverse"
                  pbr (bench-last param-list) H
                      . #:title : title "last (iota N)"
                      . #:filename : filename "last"
                  pbr (bench-max param-list) H
                      . #:title : title "apply max (iota N)"
                      . #:filename : filename "max"
                  pbr (bench-min param-list) H
                      . #:title : title "apply min (iota N)"
                      . #:filename : filename "min"
              pbr (bench-append param-list) H-lin
                  . #:title : title "append (iota N) (iota m)"
                  . #:filename : filename "list-append"
              pbr (bench-set param-list) H
                  . #:title : title "list-set! (iota (max N m)) m #t"
                  . #:filename : filename "list-set"
              pbr (bench-getslice-left param-list) H
                  . #:title : title "take (drop (iota (max N (* 2 m))) m) m"
                  . #:filename : filename "list-getslice-left"
              pbr (bench-getslice-right param-list) H
                  . #:title : title "take-right (drop-right (iota (max N (* 2 m))) m) m"
                  . #:filename : filename "list-getslice-right"
              pbr (bench-member param-list) H
                  . #:title : title "member (- m 1) (iota N)"
                  . #:filename : filename "member"
              pbr (bench-delete param-list) H
                  . #:title : title "delete (- m 1) (iota N)"
                  . #:filename : filename "delete"
              pbr (bench-delete-smaller param-list) H
                  . #:title : title "delete (- m 1) (iota N) <"
                  . #:filename : filename "delete-smaller"
              pbr (bench-delete-larger param-list) H
                  . #:title : title "delete (- m 1) (iota N) >"
                  . #:filename : filename "delete-larger"
              pbr (bench-delete! param-list) H
                  . #:title : title "delete! (- m 1) (iota N)"
                  . #:filename : filename "delete-bang"
              ;; strings
              pbr (bench-append-string param-list) H-lin
                  . #:title : title "string-append (make-string N) (make-string m)"
                  . #:filename : filename "string-append"
              pbr (bench-append-string/shared param-list) H-lin
                  . #:title : title "string-append/shared (make-string N) (make-string m)"
                  . #:filename : filename "string-append-shared"
              ;; vectors
              pbr (bench-append-vector param-list) H-lin
                  . #:title : title "vector-append (make-vector N 1) (make-vector m 1)"
                  . #:filename : filename "vector-append"
              ;; maps
              pbr (bench-assoc param-list) H 
                  . #:title : title "assoc m '((1 . 1) (2 . 2) ... (N . N))"
                  . #:filename : filename "assoc"
              pbr (bench-cons param-list) H
                  . #:title : title "cons m (iota N)"
                  . #:filename : filename "cons"
            lp
                cdr N-start
                cdr N-step
                cdr m-start
                cdr m-step