# calibration scripts
#
# in config file (.stick)
#
# makenv calib -driver zcalib
#
# see also: http://samenv.psi.ch:8080/sample_environment/83

namespace eval zcalib {
  variable buf
}

proc stdConfig::zcalib {} {
  controller syncedprot 30
  prop write stdSct::complete

  obj ZCALIB out -int
  default 3
  prop check zcalib::command
  prop label "calibration"
  prop enum running,pause,continue,stop
  prop buttons start/0/0-2:pause/1/1-3:continue/2/0,2:stop/3/3

  kids calib {
    node tlist out -text
    default "1.38 logstep0.04 310"
    prop check zcalib::set_tlist
    prop label "T list"
    prop help "a list of temperatures, logstep0.04 creates 25 logarithmic steps per decade in between values"
    prop width 80
    
    node tindex out -int
    default 0
    prop label "T index"
    prop write zcalib::set_tindex
    
    node n_temp upd -int
    prop label "number of T's"
    
    node mode out -int
    default 0
    prop enum waiting,measuring
    prop write zcalib::set_mode
    
    node phase out -int
    default 0
    prop write zcalib::set_phase
    
    node scanpos out -int
    default 0
    prop write zcalib::set_scanpos
    prop help "index within one measuring cycle: 0: reference, 1: 1st chan, 2: ref, 3: 2nd chan, ..."
    
    node channels par -text "3 1 2 4 5 6"
    prop help "first channel is calibrated one"
    prop width 20
    
    node basename par -text "/home/l_samenv/sea/calib_scripts/calib_data/calib%s_p%d_c%d.dat"
    prop width 40
    
    node startdate par -text ""
    
    node tolerance par -float 0.005
    prop help "do not measure when (T_Vti - T_set) > tolerance * T_set"
    
    node slopelimit par -float 0.005
    prop help "do not measure before maxslope < slopelimit"
    
    node maxslope upd
    prop help "max of dR/R/window over all channels"
    
    node window par -float 600
    prop help "window for slope calculation"

    node nmean par -int 10
    
  }
}

proc zcalib::get_temp {index} {
  set T 0
  set logN 0
  set mode single
  set tlist [sctval /calib/tlist]
  foreach item $tlist {
    if {[string match logstep* $item]} {
      if {$T == 0} {
        error "logstep must not appear at the beginning"
      }
      set logstep 0
      scan $item logstep%f logstep
      set mode logstep
    } else {
      if {$mode eq "logstep"} {
        if {$logstep == 0} {
          set logstep $item
          continue
        }
        set n [expr abs(round((log10($item) - log10($T)) / $logstep))]
        if {$index < $n - 1} {
          if {$item > $T} {
            set i [expr 1+$index]
          } else {
            set i [expr -1-$index]
          }
          set dig [expr round(2 - log10($logstep))]
          return [format %.${dig}g [expr $T * pow(10, $i * $logstep)]]
        } else {
          set mode single
          if {$n != 0} {
            incr index [expr 1-$n]
          }
        }
      }
      if {$index == 0} {
        return $item
      }
      set T $item
      incr index -1
    }
  }
  return -$index
}

proc zcalib::clear_buf {} {
  variable buf

  foreach c [hval /calib/channels] {
    set buf($c) [list]
    set buf(dif$c) 1
  }
}

proc zcalib::set_tlist {} {
  set n [get_temp 1000]
  incr n 1000
  updateval [sct parent]/n_temp $n
  sct update [sct target]
}

proc zcalib::activate_nv {} {
  if {[result tt tm] < 12 && [result tt set] < 3 && [result nv] == 0} {
    # do not switch control on when already fixed at low T
  } elseif {[result tt set] > [result tt tm] - 10} {
    nv 1
  } else {
    nv 2
  }
}

proc zcalib::set_tindex {} {
  set i [sct target]
  set T [get_temp $i]
  if {$T <= 0} {
    clientput "close valve and stop pump"
    hepump valve 1
    hepump running 0
    calib 3
    return idle
  }
  clear_buf
  sct update $i
  clientput "next_temp $T"
  tt set $T
  res autoscan 1
  activate_nv
  return idle
}

proc zcalib::set_mode {} {
  updateerror /calib/maxslope undefined
  if {[sct target] == 0} {
    res autoscan 1
    activate_nv
  } else {
    res autoscan 0
    nv 0
  }
  clear_buf
  sct update [sct target]
  return idle
}

proc zcalib::set_phase {} {
  sct update [sct target]
  hset [sct parent]/scanpos 0
  return idle
}

proc zcalib::set_scanpos {} {
  sct update [sct target]
  if {[sct target] % 2 != 0} {
    set i [expr [sct target] / 2 + 1]
    set c [lindex [hval /calib/channels] $i]
  } else {
    set c [lindex [hval /calib/channels] 0]
  }
  res s$c/active 1
  return idle
}

proc zcalib::update_chan {value} {
  variable buf

 catch {
  if {[hval /calib] == 1} {
    # how to treat a real pause?
    return
  }
  set time [format %.3f [expr [DoubleTime] - [hgetpropval /calib @basetime]]]
  set c [sct @channel]
  if {$c != [hval /res]} {
    return
  }
  set channels [hval /calib/channels]
  if {[hval /calib/mode] == 0} {
    lappend buf($c) $time
    lappend buf($c) $value
    set i 0
    set vmin 1e30
    set vmax 0
    set idx 0
    set full 0
      
    # determine average
    set sumtime 0
    set sumres 0
    set n 0
    set w [hval /calib/window]
    foreach {t v} $buf($c) {
      if {$t < $time - $w} {
        set idx $i
      } else {
        set sumtime [expr $sumtime + $t]
        set sumres [expr $sumres + $v]
        incr n
      }
      incr i 2
    }
    if {$idx > 0} {
      # cut down buf to window size
      set buf($c) [lrange $buf($c) $idx end]
    }
    if {$n < 2} {
      set dt 0
    } else {
      set meantime [expr $sumtime / double($n)]
      set meanres [expr $sumres / double($n)]
      
      # determine slope
      set sumdt 0
      set sumdr 0
      foreach {t v} $buf($c) {
        set dt [expr ($t - $meantime)]
        set sumdt [expr $sumdt + $dt * $dt]
        set sumdr [expr $sumdr + ($v - $meanres) * $dt]
      }
      # slope is relative R change within window (Ohm/Ohm/10 min.)
      set buf(slope$c) [expr $sumdr / $sumdt / $meanres * [hval /calib/window]]
      set maxslope 0
      foreach ch $channels {
        if {abs($buf(slope$ch)) > abs($maxslope)} {
          set maxslope $buf(slope$ch)
        }
      }
      set dt [expr round($time - [lindex $buf($c) 0])]
    }
    if {$dt > $w / 3} {
      # do not update maxslope before we have a third of the window
      updateval /calib/maxslope [expr abs($maxslope)]
    }
    if {$dt < $w || abs($maxslope) > [hval /calib/slopelimit]} {
      # do not finish waiting mode before we have the full window and the slope limit is reached
      return
    }
    calib phase [hval /calib/phase]
    # switch to measuring mode
    calib mode 1
  } else {
    if {abs([hval /tt] - [hval /tt/set]) / [hval /tt/set] > [hval /calib/tolerance] && [hval /tt] > 1.5} {
      # go to back to waiting mode when out of relative tolerance and not at base
      clientput "tt out of tolerance - WAIT"
      calib mode 0
      return
    }
    set phase [hval /calib/phase]
    if {$phase >= 3} {
      # after 3 measuring cycles go to next point
      calib phase 0
      calib tindex [expr [hval /calib/tindex] + 1]
      calib mode 0
      return
    }
    set scanpos [hval /calib/scanpos]
    set lim [hval /calib/nmean]
    if {$scanpos % 2 == 0} {
      if {$c != [lindex $channels 0]} {
        return
      }
    } else {
      if {$c != [lindex $channels [expr ($scanpos + 1)/ 2]]} {
        return
      }
    }
    lappend buf($c) $value
    if {$scanpos == 0} {
      set lim [expr $lim / 2]
    }
    set len [llength $buf($c)]
    if {$len < $lim} {
      return
    }
    set mean [expr [::tcl::mathop::+ {*}$buf($c)] / double($len)]
    set sum2 0
    set diflog [list]
    foreach r $buf($c) {
      set sum2 [expr $sum2 + pow($r - $mean, 2)]
      lappend diflog [format %.7g [expr $r - $mean]]
    }
    set sigma [expr sqrt($sum2) / double($len)]
    if {$scanpos == 0} {
    } elseif {$scanpos % 2 == 0} {
      set cf [lindex $channels [expr $scanpos/ 2]]
      set buf(ref$cf) $mean
      set buf(ref_sigma$cf) $sigma
      # keep last half of buffer for next time
      set buf($c) [lrange $buf($c) [expr $len / 2] end]
    } else {
      set buf(value$c) $mean
      set buf(sigma$c) $sigma
      set buf(time$c) $time
      set buf($c) [list]
    }
    incr scanpos
    if {$scanpos <= 2 * [llength $channels] - 2} {
      calib scanpos $scanpos
      return
    }
    catch {
        foreach channel $channels {
          if {$channel != [lindex $channels 0]} {
            set filnam [format [hval /calib/basename] [hval /calib/startdate] $phase $channel]
            set exists [file exists $filnam]
            set fil [open $filnam a]
            if {!$exists} {
              puts $fil "#zdat (zcalib by M.Zolliker 2018)"
              puts $fil "#time since midnight, R ref, R meas, sigma, Tset(nominal)"
            }
            puts $fil [format "%.2f	%.9g	%.9g	%.9g" $buf(time$channel) $buf(ref$channel) $buf(value$channel) $buf(sigma$channel) [result tt set]]
            close $fil
            set filnam "[lindex [split [hval /calib/basename] "%"] 0]_[hval /calib/startdate].txt"
            set exists [file exists $filnam]
            set fil [open $filnam a]
            if {!$exists} {
              puts $fil "#zcalib (format 2021 M.Zolliker)"
              puts $fil "#chan, time since midnight, R ref, R meas, sigma, Tset(nominal)"
            }
            puts $fil [format "%d	%.9g	%.9g	%.9g	%.2f" $channel $buf(ref$channel) $buf(value$channel) $buf(sigma$channel) [result tt set] $buf(time$channel)]
            close $fil
          }
        }
    } msg
    if {$msg ne ""} {
      clientput "save error: $msg"
    }
    clear_buf
    incr phase
    calib phase $phase
  }
 } msg
 if {$msg ne ""} {
   clientput "ERROR: $msg"
 }
}

proc zcalib::set_chan {channel} {
  _res updatescript /res/s$channel/raw zcalib::update_chan
  hsetprop /res/s$channel/raw count 10
  hsetprop /res/s$channel/raw dif 1
}

proc zcalib::set_calchan {channel} {
  _res updatescript /res/s$channel/raw zcalib::update_chan
  hsetprop /res/s$channel/raw count 5
  hsetprop /res/s$channel/raw dif 1
}

proc zcalib::install_scripts {} {
  set channels [hval [sct]/channels]
  foreach channel $channels {
    _res updatescript /res/s$channel/raw zcalib::update_chan
  }
  hsetprop /res @calchan [lindex $channels 0]
}

proc zcalib::command {} {
  switch [sct target] {
    0 { #run
      # update n_temp
      hset [sct]/tlist [hval [sct]/tlist]
      set today [clock format [clock seconds] -format "%Y-%m-%d"]
      hupdate [sct]/startdate $today
      hupdate [sct]/tindex 0
      sct @basetime [clock scan $today]
      install_scripts
      nv autoflow/getTemp zcalib::tmts
      hset [sct]/tindex 0
    }
    1 { #pause
      activate_nv
      res autoscan 1
      calib mode 0
    }
    2 { #continue
      nv 0
      calib tindex [hval /calib/tindex]
      calib mode 0
      sct update 0
      install_scripts
      return idle
    }
    3 { # stop
      nv autoflow/getTemp ccu4flow::tmts
      foreach channel [hval [sct]/channels] {
        _res killupdatescript /res/s$channel/raw zcalib::update_chan
      }
    }
  }
  sct update [sct target]
  return idle
}

proc zcalib::tmts {} {
  set tm [silent 1 result tt tm]
  set ts [silent $tm hval /res/s[hgetpropval /res @calchan]]
  if {$ts < $tm} {
    return $ts
  } else {
    return $tm
  }
}

