diff --git a/src/Autosampler.jl b/src/Autosampler.jl index c23d488..ff194c8 100644 --- a/src/Autosampler.jl +++ b/src/Autosampler.jl @@ -4,103 +4,9 @@ using CSV, DataFrames, ImagineFormat, ImagineInterface using Random, Mmap export stim_randomize, update_imagine +export chromeleon_program -""" - stim_randomize(filein, trials::Int, fileout=insertfilename(filein, "_sequence")) +include("seqfile.jl") +include("progfile.jl") -Generate a randomize sequence of stimuli. `filein` is a string containing the name -of a CSV file that describes the unique stimuli (see format described in the README). -`trials` specifies the number of pseudo-random sequences used to generate the -final stimulus sequence. -`fileout` is an optional argument used to control the name of the output file; -the default is to insert "_sequence" right before the extension of `filein`. -""" -function stim_randomize(filein::AbstractString, trials::Int; fileout=insertfilename(filein, "_sequence"), kwargs...) - open(fileout, "w") do io - stim_randomize(io, filein, trials; kwargs...) - end - return fileout -end - -function stim_randomize(io::IO, filein::AbstractString, trials::Int; header=false, kwargs...) - df = CSV.File(filein) |> DataFrame - println("Headers found: ", String.(names(df))) - n = size(df, 1) - println(n, " stimuli found") - # $ is a disallowed character since we will use it as a separator - for i = 1:n - occursin('\$', df[i,1]) && error("\$ is disallowed in stimulus names") - end - p = Int[] - for i = 1:trials - append!(p, randperm(n)) - end - CSV.write(io, df[p, :]; header=header, kwargs...) - flush(io) -end - -function update_imagine(imaginefile, sequencefile; um_per_pixel=nothing, aifile=replaceext(imaginefile, ".ai"), difile=replaceext(imaginefile, ".di"), updatedfile=imaginefile, csvheader=0, kwargs...) - header = ImagineFormat.parse_header(imaginefile) - haskey(header, "stimulus sequence") && error(imaginefile, " already has a `stimulus sequence` entry") - if um_per_pixel === nothing - um_per_pixel = header["um per pixel"] - end - um_per_pixel < 0 && error("must specify um_per_pixel") - header["um per pixel"] = um_per_pixel - - df = CSV.File(sequencefile; header=csvheader, kwargs...) |> DataFrame - n = size(df, 1) - - # Scan the AI file for stimulus triggers - ai = parse_ai(aifile, header) - aistim = getname(ai, "stimuli") - stimhi = find_pulse_starts(aistim; sampmap=:volts) - stimlo = find_pulse_stops(aistim; sampmap=:volts) - if occursin("camera frame TTL", header["label list"]) - aiframe = getname(ai, "camera frame TTL") - framestarts = find_pulse_starts(aiframe; sampmap=:volts) - elseif occursin("camera1 frame monitor", header["di label list"]) - di = parse_di(difile, header) - diframe = getname(di, "camera1 frame monitor") - framestarts = find_pulse_starts(diframe) #; sampmap=:volts) - end - - # Record as frameidx after the stimulus trigger - fps = header["frames per stack"] - framehi, framelo = Int[], Int[] - for (frameidxs, scanidxs) in ((framehi, stimhi), (framelo, stimlo)) - for i in scanidxs - idx = last(searchsorted(framestarts, i)) - push!(frameidxs, idx) - end - end - - # Add to header - header["stimulus sequence"] = join(df[:,1], '\$') - header["stimulus scan hi"] = stimhi - header["stimulus scan lo"] = stimlo - header["stimulus frame hi"] = framehi - header["stimulus frame lo"] = framelo - - if updatedfile == imaginefile - mv(imaginefile, imaginefile*".orig") - sleep(0.25) - isfile(imaginefile) && error("failed to move the old file") - end - ImagineFormat.save_header(updatedfile, header; misc=( - "stimulus sequence", "stimulus scan hi", "stimulus scan lo", - "stimulus frame hi", "stimulus frame lo")) - return header -end - -function insertfilename(filename, tail) - basename, ext = splitext(filename) - return basename*tail*ext -end - -function replaceext(filename, ext) - basename, _ = splitext(filename) - return basename*ext -end - -end # module +end \ No newline at end of file diff --git a/src/progfile.jl b/src/progfile.jl new file mode 100644 index 0000000..15ea8de --- /dev/null +++ b/src/progfile.jl @@ -0,0 +1,150 @@ +# These parameters were chosen from .pgm files used by former Holy lab members +# Most parameters were kept the same between users, with the exception of the flowrate "Flow" and wait time "Delay" +# Some of these parameters may be extraneous when using "InjectMode = UserProg". +const default_params = Dict{String,String}( + "Flow" => "0.540", # mL/min + "Delay" => "0.500", # min + + "PreflushVolume" => "5.0", # μL + + "TempCtrl" => "On", + "TemperatureNominal" => "30.0", # [°C] + "TemperatureLowerLimit" => "27.0", # [°C] + "TemperatureUpperLimit" => "33.0", # [°C] + "ReadyTempDelta" => "2.0", # [°C] + "PressureLowerLimit" => "0", # [bar] + "PressureUpperLimit" => "350", # [bar] + "MaximumFlowRampDown" => "6.000", # [ml/min²] + "MaximumFlowRampUp" => "6.000", # [ml/min²] + "%A.Equate" => "\"%A\"", + "DrawSpeed" => "10.000", # [µl/s] + "DrawDelay" => "1000", # [ms] + "DispSpeed" => "20.000", # [µl/s] + "DispenseDelay" => "0", + "WasteSpeed" => "20.000", # [µl/s] + "SampleHeight" => "2.000", # [mm] + "InjectWash" => "AfterDraw", + "WashVolume" => "100.000", # [µl] + "WashSpeed" => "30.000", # [µl/s] + "LoopWashFactor" => "1.000", + "PunctureOffset" => "0.0", # [mm] + "PumpDevice" => "\"Pump\"", + "SyncWithPump" => "Off", + "Pump_Pressure.Step" => "0.01", # [s] + "Pump_Pressure.Average" => "Off", + "Curve" => "5", +) + +""" + chromeleon_program(fileout::AbstractString, params::Dict{String,String}=default_params) + +Generate a Chromeleon program file (.pgm) to be used for timing injections performed by the autosampler +with an external TTL input (e.g. one from Imagine). +`fileout` is a string containing the name of the program file to be written. +`params` specifies the parameters to be used in the program file. Default settings based on settings used +by current and former Holy lab members are provided in `default_params`. +For typical use, only the flowrate `params["Flow"]` and wait time `params["Delay"]` need to be changed between experiments. +""" +function chromeleon_program(fileout::AbstractString, params::Dict{String,String}=default_params) + split(fileout, ".")[end] == "pgm" || error("fileout must have .pgm extension") + open(fileout, "w") do io + chromeleon_program(io, params) + end + return fileout +end + +function chromeleon_program(io::IO, params=default_params) + # Newlines use the \r\n convention for compatibility with Windows/Notepad + write(io, + "\tTempCtrl =\t$(params["TempCtrl"])\r\n", + "\tTemperature.Nominal =\t$(params["TemperatureNominal"]) [°C]\r\n", + "\tTemperature.LowerLimit =\t$(params["TemperatureLowerLimit"]) [°C]\r\n", + "\tTemperature.UpperLimit =\t$(params["TemperatureUpperLimit"]) [°C]\r\n", + "\tReadyTempDelta =\t$(params["ReadyTempDelta"]) [°C]\r\n", + "\tPressure.LowerLimit =\t$(params["PressureLowerLimit"]) [bar]\r\n", + "\tPressure.UpperLimit =\t$(params["PressureUpperLimit"]) [bar]\r\n", + "\tMaximumFlowRampDown =\t$(params["MaximumFlowRampDown"]) [ml/min²]\r\n", + "\tMaximumFlowRampUp =\t$(params["MaximumFlowRampUp"]) [ml/min²]\r\n", + "\t%A.Equate =\t$(params["%A.Equate"])\r\n", + "\tDrawSpeed =\t$(params["DrawSpeed"]) [µl/s]\r\n", + "\tDrawDelay =\t$(params["DrawDelay"]) [ms]\r\n", + "\tDispSpeed =\t$(params["DispSpeed"]) [µl/s]\r\n", + "\tDispenseDelay =\t$(params["DispenseDelay"]) [ms]\r\n", + "\tWasteSpeed =\t$(params["WasteSpeed"]) [µl/s]\r\n", + "\tSampleHeight =\t$(params["SampleHeight"]) [mm]\r\n", + "\tInjectWash =\t$(params["InjectWash"])\r\n", + "\tWashVolume =\t$(params["WashVolume"]) [µl]\r\n", + "\tWashSpeed =\t$(params["WashSpeed"]) [µl/s]\r\n", + "\tLoopWashFactor =\t$(params["LoopWashFactor"])\r\n", + "\tPunctureOffset =\t$(params["PunctureOffset"]) [mm]\r\n", + "\tPumpDevice =\t$(params["PumpDevice"])\r\n", + "\tSyncWithPump =\t$(params["SyncWithPump"])\r\n", + "\tPump_Pressure.Step =\t$(params["Pump_Pressure.Step"]) [s]\r\n", + "\tPump_Pressure.Average =\t$(params["Pump_Pressure.Average"])\r\n", + "\tCurve =\t$(params["Curve"])\r\n\r\n", + + "\tFlow =\t$(params["Flow"]) [ml/min]\r\n\r\n", + + "\t; Wait for the Ready signals from the pump and autsosampler\r\n", + " 0.000\tWait\tPump.Ready and Sampler.Ready and PumpModule.Ready\r\n\r\n", + + "\tInjectMode =\tUserProg\r\n\r\n", + + "\t; The following section of code (containing all beginning with 'Udp') is not evaluated by Chromeleon as it is read.\r\n", + "\t; Instead, the commands are carried out when the 'Inject' command is read.\r\n\r\n", + + "\t; USER DEFINED INJECTION STARTS HERE\r\n\r\n", + + "\t; Wait for the stimulus input to have a Low signal, to prevent trigger of two injections from a single signal\r\n", + "\tUdpWaitInput\tInput=Inp1, State=Low\r\n\r\n", + + "\t; Preflush the injection needle\r\n", + "\tUdpInjectValve\tPosition=Inject\r\n", + "\tUdpSyringeValve\tPosition=Needle\r\n", + "\tUdpDraw\tFrom=SampleVial, Volume=$(params["PreflushVolume"]), SyringeSpeed=GlobalSpeed, SampleHeight=Globalheight\r\n", + "\tUdpMixWait\tDuration=$(parse(Float64, params["DrawDelay"])/1000) ; Pause to avoid air intake from aspirating the sample too quickly\r\n\r\n", + + "\t; Fill the Sample Loop with the plate position and volume specified by the sequence file\r\n", + "\tUdpInjectValve\tPosition=Load\r\n", + "\tUdpDraw\tFrom=SampleVial, Volume=Volume, SyringeSpeed=GlobalSpeed, SampleHeight=Globalheight\r\n", + "\tUdpMixWait\tDuration=$(parse(Float64, params["DrawDelay"])/1000) ; Pause to avoid air intake from aspirating the sample too quickly\r\n\r\n", + + "\t; Wait for the signal from Image before performing the injection\r\n", + "\tUdpWaitInput\tInput=Inp1, State=High\r\n\r\n", + + "\t; Inject the sample and signal to Chromeleon that the injection has been performed\r\n", + "\tUdpInjectValve\tPosition=Inject\r\n", + "\tUdpInjectMarker\r\n\r\n", + + "\t; Wash the needle\r\n", + "\tUdpSyringeValve\tPosition=Waste\r\n", + "\tUdpMoveSyringeHome\tSyringeSpeed=GlobalSpeed\r\n", + "\tUdpMixNeedleWash\tVolume=$(params["WashVolume"])\r\n\r\n", + + "\t; USER DEFINED INJECTION ENDS HERE\r\n\r\n", + + "\t; Perform the user-defined injection (see above)\r\n", + "\tInject\r\n\r\n", + + "\t; Send timestamp signal to imagine\r\n", + "\tRelay_4.State\tOn\r\n\r\n", + + "\t; Start recording the pump pressure (not used for optical records, but I'm not sure if it is safe to omit this step)\r\n", + "\tPump_Pressure.AcqOn\r\n\r\n", + + " $(params["Delay"])\tPump_Pressure.AcqOff ; Stop pressure acquisition\r\n", + "\t; Note the time of the above command (in minutes).\r\n", + "\t; This time interval may be important for fully emptying/washing the Sample Loop.\r\n", + "\t; If set too long, it might also cause Chromeleon to \"miss\" a signal from Imagine.\r\n\r\n", + + "\t; Check your flowrate for the pump (\"Flow\" above), sample volume in your sequence file,\r\n", + "\t; and inter-trial delay for your Imagine waveforms to avoid issues.\r\n\r\n", + + "\t; Turn off Relay 4\r\n", + "\tRelay_4.State\tOff\r\n\r\n", + + "\tEnd\r\n", + ) + flush(io) +end + diff --git a/src/seqfile.jl b/src/seqfile.jl new file mode 100644 index 0000000..15ce01d --- /dev/null +++ b/src/seqfile.jl @@ -0,0 +1,97 @@ +""" + stim_randomize(filein, trials::Int, fileout=insertfilename(filein, "_sequence")) + +Generate a randomize sequence of stimuli. `filein` is a string containing the name +of a CSV file that describes the unique stimuli (see format described in the README). +`trials` specifies the number of pseudo-random sequences used to generate the +final stimulus sequence. +`fileout` is an optional argument used to control the name of the output file; +the default is to insert "_sequence" right before the extension of `filein`. +""" +function stim_randomize(filein::AbstractString, trials::Int; fileout=insertfilename(filein, "_sequence"), kwargs...) + open(fileout, "w") do io + stim_randomize(io, filein, trials; kwargs...) + end + return fileout +end + +function stim_randomize(io::IO, filein::AbstractString, trials::Int; header=false, kwargs...) + df = CSV.File(filein) |> DataFrame + println("Headers found: ", String.(names(df))) + n = size(df, 1) + println(n, " stimuli found") + # $ is a disallowed character since we will use it as a separator + for i = 1:n + occursin('\$', df[i,1]) && error("\$ is disallowed in stimulus names") + end + p = Int[] + for i = 1:trials + append!(p, randperm(n)) + end + CSV.write(io, df[p, :]; header=header, kwargs...) + flush(io) +end + +function update_imagine(imaginefile, sequencefile; um_per_pixel=nothing, aifile=replaceext(imaginefile, ".ai"), difile=replaceext(imaginefile, ".di"), updatedfile=imaginefile, csvheader=0, kwargs...) + header = ImagineFormat.parse_header(imaginefile) + haskey(header, "stimulus sequence") && error(imaginefile, " already has a `stimulus sequence` entry") + if um_per_pixel === nothing + um_per_pixel = header["um per pixel"] + end + um_per_pixel < 0 && error("must specify um_per_pixel") + header["um per pixel"] = um_per_pixel + + df = CSV.File(sequencefile; header=csvheader, kwargs...) |> DataFrame + n = size(df, 1) + + # Scan the AI file for stimulus triggers + ai = parse_ai(aifile, header) + aistim = getname(ai, "stimuli") + stimhi = find_pulse_starts(aistim; sampmap=:volts) + stimlo = find_pulse_stops(aistim; sampmap=:volts) + if occursin("camera frame TTL", header["label list"]) + aiframe = getname(ai, "camera frame TTL") + framestarts = find_pulse_starts(aiframe; sampmap=:volts) + elseif occursin("camera1 frame monitor", header["di label list"]) + di = parse_di(difile, header) + diframe = getname(di, "camera1 frame monitor") + framestarts = find_pulse_starts(diframe) #; sampmap=:volts) + end + + # Record as frameidx after the stimulus trigger + fps = header["frames per stack"] + framehi, framelo = Int[], Int[] + for (frameidxs, scanidxs) in ((framehi, stimhi), (framelo, stimlo)) + for i in scanidxs + idx = last(searchsorted(framestarts, i)) + push!(frameidxs, idx) + end + end + + # Add to header + header["stimulus sequence"] = join(df[:,1], '\$') + header["stimulus scan hi"] = stimhi + header["stimulus scan lo"] = stimlo + header["stimulus frame hi"] = framehi + header["stimulus frame lo"] = framelo + + if updatedfile == imaginefile + mv(imaginefile, imaginefile*".orig") + sleep(0.25) + isfile(imaginefile) && error("failed to move the old file") + end + ImagineFormat.save_header(updatedfile, header; misc=( + "stimulus sequence", "stimulus scan hi", "stimulus scan lo", + "stimulus frame hi", "stimulus frame lo")) + return header +end + +function insertfilename(filename, tail) + basename, ext = splitext(filename) + return basename*tail*ext +end + +function replaceext(filename, ext) + basename, _ = splitext(filename) + return basename*ext +end