Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generate Chromeleon program files #17

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 4 additions & 98 deletions src/Autosampler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
150 changes: 150 additions & 0 deletions src/progfile.jl
Original file line number Diff line number Diff line change
@@ -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

97 changes: 97 additions & 0 deletions src/seqfile.jl
Original file line number Diff line number Diff line change
@@ -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