From ce5feaed747809a74f3e30ce7de5b5ffe89f403c Mon Sep 17 00:00:00 2001 From: HForrest Date: Thu, 26 Sep 2019 17:34:11 +0100 Subject: [PATCH] added files. updated error message bug fix checking signalGen class made 'checkRewardValveCalibration' more general - have to test optimized logical indexing updated before creating mock objects for tests added todo for creating appropriate tests added test file --- +hw/calibrate.m | 25 +++-- +hw/checkRewardValveCalibration.m | 116 +++++++++++++++++++++++ tests/checkRewardValveCalibration_test.m | 60 ++++++++++++ 3 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 +hw/checkRewardValveCalibration.m create mode 100644 tests/checkRewardValveCalibration_test.m diff --git a/+hw/calibrate.m b/+hw/calibrate.m index 2630f065..01ebf96d 100644 --- a/+hw/calibrate.m +++ b/+hw/calibrate.m @@ -37,12 +37,16 @@ % Check inputs narginchk(5,15) -assert(~mod(length(varargin),2), 'Rigbox:hw:calibrate:partialPVpair', ... - 'Incorrect number of Name-Value pairs') +if ~all(cellfun(@ischar, (varargin(1:2:end)))) || mod(length(varargin),2) + error('Rigbox:hw:calibrate:nameValueMismatch', ... + ['If using input arguments, %s requires them to be constructed '... + 'in name-value pairs'], mfilename); +end % Check that the scale is initialized if isempty(scales.Port) || isempty(scales.readGrams) error('Rigbox:hw:calibrate:noscales', ... - 'Unable to communicate with scale. Scales object is not properly initialized') + ['Unable to communicate with scale. Scales object is not '... + 'properly initialized']) end % Parse Name-Value pairs @@ -55,10 +59,10 @@ inputs = cell2struct(varargin(2:2:end)', varargin(1:2:end)'); p = mergeStructs(inputs, defaults); -signalGen = rewardController.SignalGenerators(strcmp(rewardController.ChannelNames, channel)); +signalGen =... + rewardController.SignalGenerators(strcmp(rewardController.ChannelNames,... + channel)); t = meshgrid(linspace(tMin, tMax, p.nVolumes), zeros(1, p.nPerT)); -% t = [10 25 50 100 200 500 1000]/1000; -% t = [1000]/1000; n = repmat(p.delivPerSample, size(t)); dw = zeros(size(t)); @@ -78,13 +82,15 @@ newWeight = scales.readGrams; assert(newWeight > prevWeight + 0.02, ... 'Rigbox:hw:calibrate:deadscale',... - 'Error: Scale is not registering changes in weight. Confirm scale is properly connected and that water is landing into the dish') + ['Error: Scale is not registering changes in weight. Confirm scale is \n'... + 'properly connected and that water is landing into the dish']); prevWeight = newWeight; fprintf('Initial scale reading is %.2fg\n', prevWeight); startTime = GetSecs; -fprintf('Deliveries will take approximately %.0f minute(s)\n', ceil(approxTime/60)); +fprintf('Deliveries will take approximately %.0f minute(s)\n',... + ceil(approxTime/60)); try for j = 1:size(t,2) @@ -96,7 +102,8 @@ dw(i,j) = newWeight - prevWeight; prevWeight = newWeight; ml = dw(i,j)/n(i,j); - fprintf('Weight delta = %.2fg. Delivered %ful per %fms\n', dw(i,j), 1000*ml, 1000*t(i,j)); + fprintf('Weight delta = %.2fg. Delivered %ful per %fms\n',... + dw(i,j), 1000*ml, 1000*t(i,j)); end end catch ex diff --git a/+hw/checkRewardValveCalibration.m b/+hw/checkRewardValveCalibration.m new file mode 100644 index 00000000..0d2b0c58 --- /dev/null +++ b/+hw/checkRewardValveCalibration.m @@ -0,0 +1,116 @@ +function checkRewardValveCalibration(varargin) +% HW.CHECKREWARDVALVECALIBRATION delivers a set amount of reward to check reward valve calibration +% +% This function is used to deliver a set amount of reward via a reward +% valve controlled by a NI-DAQ: a user should check the amount of water +% delivered manually by catching the delivered reward via a falcon tube or +% similar container. +% +% Inputs: +% `amt` (numeric): The amount (in mL) of water to deliver. (default = +% 5.0) +% `chan` (string): The channel of the NI-DAQ associated with the reward +% valve. If not given, `chan` is found by getting the channel +% associated with the `hw.RewardValveControl` object in the rig's +% `hw.DaqController` object. +% +% Examples: +% - Check that 5 mL of water is delivered: +% `hw.checkRewardValveCalibration()` +% - Check that 10 mL of water is delivered via 'ao1': +% `hw.checkRewardValveCalibration('amt', 10.0, 'chan', 'ao1')` +% +% See also: `hw.calibrate`, `hw.RewardValveControl` +% +% Todo: Sanitize and integrate into `hw.RewardValveControl` +% Todo: Create appropriate tests using mock objects. + +%% Set input arg values and get `hw.RewardValveControl` object from which to deliver reward + +% If the name-value pairs don't match up, throw error. +if ~all(cellfun(@ischar, (varargin(1:2:end)))) || mod(length(varargin),2) + error('Rigbox:hw:checkRewardValveCalibration:nameValueMismatch', ... + ['If using input arguments, %s requires them to be constructed '... + 'in name-value pairs'], mfilename); +end + +% Get the `hw.DaqController` object from within the rig's hardware devices: +rig = hw.devices; +rigFields = fieldnames(rig); +% Get a cell array of each object in the `rig` struct. +rigObjs = cellfun(@(x) rig.(x), rigFields, 'UniformOutput', 0); +% Get the index of the `hw.DaqController` object in `rigObjs` +dcIdxInRig = cellfun(@(x) isa(x, 'hw.DaqController'),... + rigObjs, 'UniformOutput', 0); +try + % Get the `hw.DaqController` object from `rig` based on its fieldname. + dc = rig.(rigFields{[dcIdxInRig{:}]}); +catch + error('Rigbox:hw:checkRewardValveCalibration:dcNotFound',... + ['Could not find a ''hw.DaqController'' object in this rig''s \n'... + '''hardware.mat'' file.']); +end + +% Get the `hw.RewardValveControl` object from within the rig's +% `hw.DaqController` `SignalGenerators`: +sgClassNames = arrayfun(@class, dc.SignalGenerators,... + 'UniformOutput', false); +rvChanIdx = cellfun(@(x) strcmpi('hw.RewardValveControl', x),... + sgClassNames); +chan = dc.DaqChannelIds{rvChanIdx}; + +% Set default values for input args. +defaults = struct(... + 'amt', 5.0, ... + 'chan', chan); + +% Convert user-defined `varargin` into struct. +inputs = cell2struct(varargin(2:2:end)', varargin(1:2:end)'); + +% Merge `inputs` and `defaults`, making sure fields for `inputs` overwrites +% the same named fields for `defaults`. +argsStruct = mergeStructs(inputs, defaults); + +% De-struct input args +amt = argsStruct.amt; +chan = argsStruct.chan; + +% Get the `hw.RewardValveControl` object. +rv = dc.SignalGenerators(contains(dc.DaqChannelIds, chan)); +if isempty(rv) + error('Rigbox:hw:checkRewardValveCalibration:rvNotFound',... + ['Could not find a `hw.RewardValveControl` object in this rig''s \n'... + '`hw.DaqController` object, or an inappropriate `chan` input \n'... + 'argument was given.']); +end +%% Deliver reward + +% Get the most recent, largest delivery volume, `v`, and time, `t`, taken +% to deliver that volume, from the `rv` calibrations table. +v = rv.Calibrations(end).measuredDeliveries(end).volumeMicroLitres; +t = rv.Calibrations(end).measuredDeliveries(end).durationSecs; +% Get the number of pulses required to deliver a reward of amount `amt` in +% increments of `v`. +nPulses = ceil(amt*10e2 / v); + +% Set-up command, `cmd`, to send to `dc` to deliver reward. +interval = 0.2; % interval b/w pulses in s +cmd = [t; interval; nPulses]; % command to output to `dc` + +% Print to screen how long it will take to check this calibration: +totalTInMins = ((interval+t)*nPulses)/60; +fprintf('This calibration check will take approximately %0.2f mins\n',... + totalTInMins); + +% Change `ParamsFun` of `rv` to deliver pulses as +% [t, nPulses, frequency]. +origParamsFun = rv.ParamsFun; % we'll reset `ParamsFun` to this +rv.ParamsFun = @(cmd) deal(cmd(1), cmd(3), 1/(sum(cmd(1)+cmd(2)))); + +% Deliver command to `dc`. +dc.command(cmd, 'foreground'); + +% Reset `ParamsFun`. +rv.ParamsFun = origParamsFun; + +end diff --git a/tests/checkRewardValveCalibration_test.m b/tests/checkRewardValveCalibration_test.m new file mode 100644 index 00000000..e795be5b --- /dev/null +++ b/tests/checkRewardValveCalibration_test.m @@ -0,0 +1,60 @@ +% preconditions: + +% Get the `hw.DaqController` object from within the rig's hardware devices: +rig = hw.devices; +rigFields = fieldnames(rig); +% Get a cell array of each object in the `rig` struct. +rigObjs = cellfun(@(x) rig.(x), rigFields, 'UniformOutput', 0); +% Get the index of the `hw.DaqController` object in `rigObjs` +dcIdxInRig = cellfun(@(x) isa(x, 'hw.DaqController'),... + rigObjs, 'UniformOutput', 0); +try + % Get the `hw.DaqController` object from `rig` based on its fieldname. + dc = rig.(rigFields{[dcIdxInRig{:}]}); +catch + error('Rigbox:hw:checkRewardValveCalibration:dcNotFound',... + ['Could not find a ''hw.DaqController'' object in this rig''s \n'... + '''hardware.mat'' file.']); +end + +% Get the `hw.RewardValveControl` object from within the rig's +% `hw.DaqController` `SignalGenerators`: +sgClassNames = arrayfun(@class, dc.SignalGenerators,... + 'UniformOutput', false); +rvChanIdx = cellfun(@(x) strcmpi('hw.RewardValveControl', x),... + sgClassNames); + +chan = dc.DaqChannelIds{rvChanIdx}; % NI-DAQ channel we are outputting reward to +amt = 5.0; % volume of reward (in mL) + +%% Test 1: Ensure that the class of the Signal Generator output to is valid +chanIdx = contains(dc.DaqChannelIds, chan); + +% Get the `hw.ControlSignalGenerator` object from which to deliver reward. +signalGen = dc.SignalGenerators(chanIdx); + +assert(strcmpi(class(signalGen), 'hw.RewardValveControl'),... + ['The class of the Signal Generator is not a class which can '... + 'deliver reward']); + +%% Test 2: Ensure that proper name-value pair arguments are used +% Ensure rig hardware is freed up: +clearvars -except amt chan +daqreset; + +try + hw.checkRewardValveCalibration(amt, chan) +catch ex + errSep = strfind(ex.identifier, ':'); + errName = ex.identifier(errSep(end)+1:end); + assert(strcmpi(errName, 'nameValueMismatch')); +end + +try + daqreset; + hw.checkRewardValveCalibration('amt', 5.0, 'chan', 'invalidCh') +catch ex + errSep = strfind(ex.identifier, ':'); + errName = ex.identifier(errSep(end)+1:end); + assert(strcmpi(errName, 'rvNotFound')); +end \ No newline at end of file