Commit e2e8f95a authored by Antoine Laudrain's avatar Antoine Laudrain
Browse files

Merge branch 'MSO_multiple_channels' into 'master'

MSO: enable waveform dump for several channels.

See merge request !20
parents 391f1c0f e46da30c
Pipeline #131993 passed with stages
in 4 minutes and 11 seconds
......@@ -17,13 +17,13 @@ python take_data data/cosmics --nevents 500 -c 4
```
The first argument (only positional one) is the path prefix to the output file.
It should not contain the extension, as the timestamp and extension are automatically added.
It should not contain the extension, as the timestamp, channel number, and extension are automatically added.
For example if `cosmics` is given, the following output files will be created:
- `data/cosmics.yymmdd_HHMMSS.raw`: a binary file containing the raw waveforms (byte-string flux from the scope).
- `data/cosmics.yymmdd_HHMMSS.CHAN4.raw`: a binary file containing the raw waveforms (byte-string flux from the scope).
The first two (`'#0'`) and the last characters (`'\n'`) are not part of the data.
All the waveform are concatenated (no event separation), the event length is deduced from the preamble.
- `data/cosmics.yymmdd_HHMMSS.dat`, plain text file containing the preamble on the first line, providing all necessary information to reconstruct the waveform length.
- `data/cosmics.yymmdd_HHMMSS.CHAN4.dat`, plain text file containing the preamble on the first line, providing all necessary information to reconstruct the waveform length.
This argument can be a path containing folders (eg. `data/200909/cosmics`).
The last directory in the tree is created if not existing (if the above one don't exist, the code will crash).
......@@ -31,14 +31,14 @@ The last directory in the tree is created if not existing (if the above one don'
A more complete example would look like:
```bash
python take_data.py /localscratch/data/210305_24403_cosmics/cosmics --nevents 500 --nruns 20 -c 1 --wait 1800 --log both
python take_data.py /localscratch/data/210305_24403_cosmics/cosmics --nevents 500 --nruns 20 -c 1 3 4 --wait 1800 --log both
```
### Run control options
The second argument (required option `-c <channel>`, `--channel <channel>`, `--source <channel>`, with `channel` = 1, 2, 3, 4)
specifies from which oscilloscope channel the waveform is to be retrieved (only one can be used).
specifies from which oscilloscope channels the waveform is to be retrieved (several can be used).
The number of events per run is set with the `--nevents <N>` option.
The number of runs is 1 by default but can be set using the `--nruns <N>` option.
......@@ -59,7 +59,7 @@ This log file concatenates the information from all runs.
The log files are in all cases appended, so that no information can be lost.
Moreover, the name of the 'single' log file can be explicitely specified using the `--logfile <path/to/logfile.log>` option.
Moreover, the name of the 'single' log file can be explicitly specified using the `--logfile <path/to/logfile.log>` option.
......@@ -72,7 +72,7 @@ If using python, one can simply
from helpers_wav import process_preamble, get_time_points, process_sequence
# First read and process preamble for metadata.
with open("path/to/data.yymmdd_HHMMSS.dat") as infile:
with open("path/to/data.yymmdd_HHMMSS.CHAN1.dat") as infile:
preamble = infile.read()
wav_format, wav_type, npoints, count, \
......@@ -85,7 +85,7 @@ xunit, yunit, bw_max, bw_min, segcount = process_preamble(preamble)
time_points = get_time_points(npoints, xinc, xorigin)
# Read the raw data.
with open("path/to/data.yymmdd_HHMMSS.raw", 'rb') as infile:
with open("path/to/data.yymmdd_HHMMSS.CHAN1.raw", 'rb') as infile:
raw_data = infile.read()
events = process_sequence(raw_data, npoints, yinc, yorigin)
......
......@@ -16,8 +16,8 @@ from argparse import ArgumentParser, RawDescriptionHelpFormatter
import visa
from helpers import try_mkdir
from helpers_print import info_channels, info_timebase
from helpers_print import info_trigger, info_acquire, info_waveform
from helpers_print import (process_query_answer,
info_channels, info_timebase, info_trigger, info_acquire, info_waveform)
from logger_cfg import getLogger, setup_logging, FORMATTER
logger = getLogger(__name__)
......@@ -34,9 +34,9 @@ def setup_datataking(instr, n_wvfm):
"""Setup ACQ to segmented."""
log_write(instr, ':ACQ:AVERage OFF')
# log_write(instr, ':ACQ:BANDwidth AUTO')
# log_write(instr, ':ACQ:INTerpolate OFF')
# log_write(instr, ':ACQ:POINts:ANALog AUTO')
# log_write(instr, ':ACQ:SRATe:ANALog AUTO')
log_write(instr, ':ACQ:INTerpolate OFF')
log_write(instr, ':ACQ:SRATe:ANALog 5e9')
log_write(instr, ':ACQ:POINts:ANALog AUTO')
log_write(instr, ':ACQ:MODE SEGMented')
log_write(instr, f':ACQ:SEGMented:COUNt {n_wvfm}')
......@@ -75,64 +75,75 @@ def setup_trigger(instr, channel, level=None, slope=None):
log_write(instr, f':TRIG:LEVel CHAN{channel},{level:.3e}')
return
def setup_readout(instr, channel):
"""Setup WAV readout: WORD, LSBF, STReaming, ALL segments for channel."""
def setup_readout(instr):
"""Setup WAV readout: WORD, LSBF, STReaming, ALL segments, view ALL."""
# DO NOT USE BYTE FORMAT:
# The scope is using a 10-bit ADC, encoded as 2 bytes. Using BYTE would
# truncate and only send the LSB.
log_write(instr, ':WAV:FORMat WORD')
log_write(instr, ':WAV:BYTeorder LSBFirst')
log_write(instr, ':WAV:STReaming ON') # Manual advices for STR
log_write(instr, f':WAV:SOURce CHAN{channel}')
log_write(instr, ':WAV:SEGMented:ALL 1')
log_write(instr, ':WAV:FORMat WORD')
log_write(instr, ':WAV:BYTeorder LSBFirst')
log_write(instr, ':WAV:STReaming ON') # Manual advices for STR
log_write(instr, ':WAV:SEGMented:ALL 1')
log_write(instr, ':WAV:VIEW ALL')
###############################################################################
def write_output(instr, fpath):
"""Retrieve the data and dump to file.
def write_output(instr, channel, outpath):
"""Retrieve the data for a given channel and dump it to file.
@param[in] instr: instrument
@param[in] fpath: string, see `main`
@param[in] channel: int,
Channel number to record data from.
@param[in] outpath: string, see `main`
"""
fpath_pre = fpath + ".dat"
logger.info("Writing output for CHAN%d", channel)
log_write(instr, f':WAV:SOURce CHAN{channel}')
outpath += f".CHAN{channel}"
outpath_pre = outpath + ".dat"
log_write(instr, 'WAV:PRE?')
preamble = instr.read_raw()
logger.info("Writing preamble file: %s", fpath_pre)
with open(fpath_pre, 'w') as outf:
logger.info("Writing preamble file: %s", outpath_pre)
with open(outpath_pre, 'w') as outf:
outf.write(preamble.decode('ascii'))
fpath_data = fpath + ".raw"
outpath_data = outpath + ".raw"
log_write(instr, 'WAV:DATA?')
data = instr.read_raw()
logger.info("Writing data file: %s", fpath_data)
with open(fpath_data, 'wb') as outf:
logger.info("Writing data file: %s", outpath_data)
with open(outpath_data, 'wb') as outf:
outf.write(data)
def main(fpath, nevents, nruns, read_channel, wait_seconds=60,
logfile_single=True, logfile_path=None, log_each_run=False,
debug=False):
"""Main callable.
ALLOWED_LOGTYPE = ['no', 'single', 'each', 'both']
@param[in] fpath: string
def main(outpath, nevents, nruns, read_channels, wait_seconds=60,
logtype="single", logfile_path=None, debug=False):
"""Main callable: do the setup and loop over runs.
@param[in] outpath: string
Path to output file, without extension (timestamp and ext
automatically added).
@param[in] nevents: int
Number of events per run.
@param[in] nruns: int
Number of runs.
@param[in] read_channel: int {1, 2, 3, 4}
Channel number to extract data from.
@param[in] read_channels: list of int {1, 2, 3, 4}
List of channel number to extract data from.
@param[in] wait_seconds: float, default: 60
Duration in seconds between two pollings for end of run.
@param[in] logfile_single: bool
Flag to enable logging to file for everything written on screen.
Default is True.
@param[in] logtype: string, one of ALLOWED_LOGTYPE, default: 'single'.
How logging to file is handled:
- 'no' -> None
- 'single' -> one single log file is created containing all the
info (mimic what is displayed on screen)
- 'each' -> each run gets its own log file (does not contain the
instrument info)
- 'both' -> the previous two at the same time.
@param[in] logfile_path: string or None
Path for the logfile replicating what is written on screen.
If None (default), the name of the run (`fpath`) is used.
If logfile_single is False, this value is forced to None.
If None (default), the name of the run (`outpath`) is used.
@param[in] log_each_run: bool
Flag to enable logging to file each run separately. These logs
don't include the instrument information printed in the
......@@ -141,14 +152,37 @@ def main(fpath, nevents, nruns, read_channel, wait_seconds=60,
@param[in] debug: bool
Turns ON debug printout.
"""
# First resolve what we should do for logging.
logtype = logtype.lower()
if not logtype in ALLOWED_LOGTYPE:
logger.error("logtype is not one of %s", ALLOWED_LOGTYPE)
raise KeyError(logtype)
log_single = logtype == "both" or logtype == "single"
log_each_run = logtype == "both" or logtype == "each"
# Then apply these settings.
if logfile_path is None:
logfile_path = fpath + ".log"
if not logfile_single:
logfile_path = outpath + ".log"
if not log_single:
# If not logging the global screen output to file,
# `setup_logging` must receive None.
logfile_path = None
setup_logging(logfile=logfile_path,
level=logging.DEBUG if debug else logging.INFO)
# Check that at least one channel is provided, make them unique, and check
# that they are available ({1,2,3,4}).
if not read_channels: # if empty
logger.error("At least one channel must be provided.")
sys.exit(1)
logger.debug("Channel list provided: %s", read_channels)
read_channels = sorted(set(read_channels))
logger.debug("Sorted unique list of channels: %s", read_channels)
if not all([channel in range(1,5) for channel in read_channels]):
logger.error("Some requested channels are not available: %s",
read_channels)
sys.exit(1)
# '@py' to use the pyvisa-py backend
resource_mgr = visa.ResourceManager("@py")
logger.info(resource_mgr.list_resources())
......@@ -182,32 +216,68 @@ def main(fpath, nevents, nruns, read_channel, wait_seconds=60,
# setup_channel(instr, channel=1, offset=-600e-3, scale=200e-3, skew=0)
# setup_trigger(instr, channel=1, level=-230e-3, slope='-')
# Settings for cosmics (new PCB, March 2021).
setup_timebase(instr, position=-20e-9, scale=20e-9)
# # Settings for cosmics (new PCB, March 2021).
# setup_timebase(instr, position=-20e-9, scale=20e-9)
# setup_channel(instr, channel=1, offset=-600e-3, scale=200e-3, skew=0)
# setup_channel(instr, channel=3, offset=0, scale=500e-3, skew=0)
# setup_trigger(instr, channel=3, level=-500e-3, slope='-')
# # Settings for cosmics with one PMT trigger only (new PCB, April 2021).
# setup_timebase(instr, position=0, scale=20e-9)
# setup_channel(instr, channel=1, offset=-600e-3, scale=200e-3, skew=0)
# setup_channel(instr, channel=3, offset=0, scale=500e-3, skew=0)
# setup_trigger(instr, channel=3, level=-500e-3, slope='-')
# Settings for longer timebase, SiPM trigger, all 3 readout (April 2021).
setup_timebase(instr, position=600e-9, scale=200e-9)
setup_channel(instr, channel=1, offset=-600e-3, scale=200e-3, skew=0)
setup_channel(instr, channel=3, offset=0, scale=500e-3, skew=0)
setup_trigger(instr, channel=3, level=-500e-3, slope='-')
setup_channel(instr, channel=3, offset=-100e-3, scale=50e-3, skew=0)
setup_channel(instr, channel=4, offset=-100e-3, scale=50e-3, skew=0)
setup_trigger(instr, channel=1, level=-230e-3, slope='-')
setup_datataking(instr, nevents)
setup_readout(instr, channel=read_channel)
setup_readout(instr)
# Setup one default waveform source (used to get how many waveform are
# recorded).
log_write(instr, f':WAVeform:SOURce CHAN{read_channels[0]}')
info_channels(instr)
info_timebase(instr)
info_trigger(instr)
info_acquire(instr)
info_waveform(instr)
dirname = os.path.normpath(os.path.dirname(fpath))
logger.info("Will readout channels: %s", read_channels)
# Check that number of points per waveform is as expected.
expected_points = int(process_query_answer(instr.query(':ACQuire:POINts?')))
actual_points = int(process_query_answer(instr.query(':WAVeform:POINts?')))
if actual_points != expected_points:
logger.warning("ACQuire:POINts was set to %d but WAVeform:POINts has "
"%d. If that's higher maybe an interpolation is ON? If "
"that's lower maybe too many events per run were "
"requested.", expected_points, actual_points)
# Check that the oscilloscope will indeed record as many events as
# requested. If different, the event loop will likely never reach the point
# where enough events have been recorded and will never end.
actual_nevents = int(process_query_answer(
instr.query(':ACQuire:SEGMented:COUNt?')))
if actual_nevents != nevents:
logger.error("Requested %d events but %d will be recorded. "
"The event loop will never end :(")
sys.exit(1)
dirname = os.path.normpath(os.path.dirname(outpath))
try_mkdir(dirname)
# A log file handler is created at the start of each run and deleted at
# the end.
for irun in range(nruns):
fpath_time = fpath + "." + strftime("%y%m%d_%H%M%S")
outpath_time = outpath + "." + strftime("%y%m%d_%H%M%S")
if log_each_run:
# Add log file for this run.
file_handler = logging.FileHandler(fpath_time + ".log")
file_handler = logging.FileHandler(outpath_time + ".log")
file_handler.setFormatter(FORMATTER)
logger.addHandler(file_handler)
......@@ -228,7 +298,8 @@ def main(fpath, nevents, nruns, read_channel, wait_seconds=60,
break
log_write(instr, ':STOP')
write_output(instr, fpath_time)
for channel in read_channels:
write_output(instr, channel, outpath_time)
if log_each_run:
# Remove log file for this run: new one will be added for next run.
......@@ -250,9 +321,9 @@ Data from channel 4 will be dumped to output files at the end of each run.
Each run outputs several files:
- The raw waveform (byte-string flux from the scope) is written in
`data/cosmics.yymmdd_HHMMSS.raw`.
`data/cosmics.yymmdd_HHMMSS.CHAN4.raw`.
- The metadata (preamble) as plaintex is written in
`data/cosmics.yymmdd_HHMMSS.dat
`data/cosmics.yymmdd_HHMMSS.CHAN4.dat
"""
def parse_args(argv):
......@@ -262,43 +333,42 @@ def parse_args(argv):
parser.add_argument("-d", "--debug", action="store_true",
help="Print debug information.")
parser.add_argument("outfile",
parser.add_argument("outpath",
help="Full path to the output file, without extension (automatically "
"added alongside with timestamp). "
"Last output directory level is created if not already existing.")
parser.add_argument("-c", "--channel", "--source",
type=int, choices=list(range(1,5)), required=True,
help="Channel source for the waveform recording.")
parser.add_argument("-c", "--channel", "--source", dest="read_channels",
type=int, choices=list(range(1,5)), required=True, nargs="+",
help="List of channels from which to record the waveform. "
"Only analog channels are implemented (CHAN<1-4>.")
parser.add_argument("--nevents",
type=int, required=True, metavar="N",
help="Acquire a sequence of this many events.")
parser.add_argument("--nruns",
type=int, default=1, metavar="N",
help="Repeat sequence acquisition N times.")
parser.add_argument("--wait",
parser.add_argument("--wait", dest="wait_seconds",
metavar="SECONDS", type=float, default=60,
help="Poll if acquisition finished every SECONDS seconds.")
help="Poll if acquisition finished every SECONDS seconds. "
"Default: 60 seconds.")
args_log = parser.add_argument_group("logging")
args_log.add_argument("--log",
choices=['no', 'single', 'each', 'both'], default='single',
args_log.add_argument("--log", dest="logtype",
choices=ALLOWED_LOGTYPE, default='single',
help="How logging to file is handled: 'no' -> None / 'single' -> one "
"single log file is created (mimic what is displayed on screen) / "
"'each' -> each run gets its own log file (does not contain the "
"instrument info) / 'both' -> the previous two at the same time. "
"Default is 'single'."
)
args_log.add_argument("--logfile",
args_log.add_argument("--logfile", dest="logfile_path",
help="Name of the 'single' logfile. If not precised, use the name of "
"the run."
)
args = parser.parse_args(argv)
return args.outfile, args.nevents, args.nruns, args.channel, args.wait, \
args.log == 'single' or args.log == 'both', args.logfile, \
args.log == 'each' or args.log == 'both'
return vars(parser.parse_args(argv))
if __name__ == "__main__":
main(*parse_args(sys.argv[1:]))
main(**parse_args(sys.argv[1:]))
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment