Skip to content
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
60 changes: 2 additions & 58 deletions SODM.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

addpath(genpath('./lib'));
addpath(genpath('./tasks/SODM'));
addpath('./tasks/MDM');
% Add subfolders we'll be using to path
% NOTE: genpath gets the directory and all its subdirectories

%% Load config
config = SODM_blockDefaults();
Expand Down Expand Up @@ -37,63 +35,9 @@
end
end

% Disambiguate config here
monConfig = SODM_monetaryConfig(config);
medConfig = SODM_medicalConfig(config);

%% Generate trials if not generated already
if ~isfield(Data, 'blocks') || isempty(Data.blocks)
% NOTE: Generating each one separately with two repeats, so that there isn't
% a cluster of high values in self vs. other
medSelfBlocks = generateBlocks(medConfig, medConfig.trial.generate.catchTrial, ...
medConfig.trial.generate.catchIdx);
medOtherBlocks = generateBlocks(medConfig, medConfig.trial.generate.catchTrial, ...
medConfig.trial.generate.catchIdx);
monSelfBlocks = generateBlocks(monConfig, monConfig.trial.generate.catchTrial, ...
monConfig.trial.generate.catchIdx);
monOtherBlocks = generateBlocks(monConfig, monConfig.trial.generate.catchTrial, ...
monConfig.trial.generate.catchIdx);

medBlocks = [medSelfBlocks; medOtherBlocks];
monBlocks = [monSelfBlocks; monOtherBlocks];

sortOrder = mod(Data.subjectId, 4);
selfIdx = [1 0 1 0]; % 0 is friend, 1 is self
medIdx = [1 1 0 0]; % 0 is monetary, 1 is medical

switch sortOrder
case 0
% Keep order
case 1
selfIdx = 1 - selfIdx;
case 2
medIdx = 1 - medIdx;
case 3
selfIdx = 1 - selfIdx;
medIdx = 1 - medIdx;
end
% Repeat the order for the second round
selfIdx = repmat(selfIdx, 1, 2);
medIdx = repmat(medIdx, 1, 2);
beneficiaryLookup = {'Friend', 'Self'};

% Logic: Do mon/med blocks first, pass self/other to them depending on selfIdx
numBlocks = length(selfIdx);
Data.numFinishedBlocks = 0;
for blockIdx = 1:numBlocks
blockKind = medIdx(blockIdx);
beneficiaryKind = selfIdx(blockIdx);
beneficiaryStr = beneficiaryLookup{beneficiaryKind + 1};
withinKindIdx = sum(medIdx(1 : blockIdx) == blockKind);

if blockKind == 1
medConfig.runSetup.conditions.beneficiary = beneficiaryStr;
Data = addGeneratedBlock(Data, medBlocks{withinKindIdx}, medConfig);
else
monConfig.runSetup.conditions.beneficiary = beneficiaryStr;
Data = addGeneratedBlock(Data, monBlocks{withinKindIdx}, monConfig);
end
end
Data.blocks = SODM_generateBlocks(subjectId, config);
end

% Select which blocks to run
Expand All @@ -114,5 +58,5 @@
end

% Close window
unloadPTB(monConfig, medConfig);
unloadPTB(config);
end
21 changes: 21 additions & 0 deletions lib/helpers/addDispersedColumns.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
function [ tbl ] = addDispersedColumns(tbl, values)
% For each field in `values`, spread its contents in a new column in `tbl`
%
% Args:
% tbl: a `table` with some rows
% values: a `struct` in which each field is a matrix of values that you'd
% like distributed evenly across rows of `tbl`
%
% Returns:
% tbl with additional columns, one for each field in `values`.
%
% Note:
% To fit each arbitrarily long field in `values` into an arbitrary number of
% rows, the function uses `cutArrayToSize`. If there's fewer
% elements than rows, the values will be repeated; if there's more values
% than rows, the first n terms in each field will be used.

props = structfun(@(x) cutArrayToSize(x, height(tbl)), ...
values, 'UniformOutput', false);
tbl = [tbl, struct2table(props)];
end
29 changes: 29 additions & 0 deletions lib/helpers/generateValuesForMissing.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function tbl = generateValuesForMissing(tbl, levelConfig)
% If any of the columns in `tbl` have NaN in row, it will select a value
% randomly from the corresponding field of `levelConfig`.
%
% Args:
% tbl: An arbitrary table with some NaN values
% levelConfig: A struct that contains value options to replace NaN in the given columns
%
% Returns:
% tbl: A new table with missing values generated.
tblCols = tbl.Properties.VariableNames;
knownLevels = fieldnames(levelConfig);

for k = 1:numel(tblCols)
colName = tblCols{k};
if ~ismember(colName, knownLevels)
continue
end
col = tbl.(colName);
changable = isnan(col);
changableIdx = find(changable); % Get non-zero values
if sum(changable) > 0
numOptions = length(levelConfig.(colName));
for l = 1:length(changableIdx)
tbl(changableIdx(l), k) = {levelConfig.(colName)(randi(numOptions))};
end
end
end
end
22 changes: 22 additions & 0 deletions lib/helpers/injectInBlocks.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function [ blocksOut ] = injectInBlocks(blockArray, fields, insertion)
% Adds/changes the value of nested config structs within a block array
%
% Args:
% blockArray: cell array of block structs
% fields: a cell array that gives the "adress" for injection. If you wish to
% change `config.runSetup.authorName` in every block, you would use
% `{'runSetup', 'authorName'}`
% insertion: if it is a function handle, it will be called with the value of
% the target field as argument, and the output will replace that value. If
% it is any other variable kind, it will simply be set as the value of the
% target field.

if isFunction(insertion)
blocksOut = cellfun(@(block) setfield(s, fields{:}, ...
insertion(getfield(s, fields{:}))), ...
'UniformOutput', false);
else
blocksOut = cellfun(@(block) setfield(s, fields{:}, insertion), ...
'UniformOutput', false);
end
end
34 changes: 34 additions & 0 deletions lib/helpers/injectRowAtIndex.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
function tbl = injectRowAtIndex(tbl, row, rowIndex, levelConfig)
% Insert a tabular `row` into a table at all specified indices.
%
% If any of the named fields is NaN and levelConfig is provided,
% the function invokes `generateValuesForMissing` to generate a value from that
% field from levels.
%
% Args:
% tbl: The original table
% row: A (usually single-row) table to be inserted at specified indices
% rowIndex: A row index or a matrix of the row indices
% levelConfig (optional): A configuration level containing replacement
% information for use by `generateValuesForMissing`
%
% Returns:
% tbl: A new table with a row at every index.
%
% Warning:
% The function assumes that the row can be concatenated with the output of
% `tbl`. If the row doesn't have all the same columns as tbl, the injection
% will fail with a hard error.

if exist('levelConfig', 'var')
row = generateValuesForMissing(row, levelConfig);
end

rowIndex = sort(rowIndex);
for idx = 1:length(rowIndex)
tbl_pre = tbl(1:(rowIndex(idx) - 1), :);
tbl_post = tbl(rowIndex(idx):end, :);
% FIXME: Use join in case `row` orders the same variables differently
tbl = [tbl_pre; row; tbl_post];
end
end
60 changes: 0 additions & 60 deletions lib/setup/generateBlocks.m
Original file line number Diff line number Diff line change
Expand Up @@ -88,63 +88,3 @@
plannedBlocks{k} = trialTbl;
end
end

% Helper function
function tbl = injectRowAtIndex(tbl, row, rowIndex, levelConfig)
% Put a constant `row` at all indices in `rowIndex`.
%
% Assumes that the row can be concatenated with the output of `trialTbl`.
% If any of the named fields is NaN and levelConfig is provided,
% generate a value from that field from levels.
%
% Args:
% tbl: The original table
% row: An area in table where missing information has been filled in
% rowIndex: An array of the row indices
% levelConfig: A configuration level containing trial information
%
% Returns:
% tbl: A new table with a row at every index.
if exist('levelConfig', 'var')
row = generateValuesForMissing(row, levelConfig);
end

rowIndex = sort(rowIndex);
for idx = 1:length(rowIndex)
tbl_pre = tbl(1:(rowIndex(idx) - 1), :);
tbl_post = tbl(rowIndex(idx):end, :);
% FIXME: Use join in case `row` orders the same variables differently
tbl = [tbl_pre; row; tbl_post];
end
end

%Helper function
function tbl = generateValuesForMissing(tbl, levelConfig)
% If any of the columns in `tbl` have NaN in row,
% it will insert a random value from corresponding field of `levelConfig`.
%
% Args:
% tbl: An original table
% levelConfig: A configuration level containing trial information
%
% Returns:
% tbl: A new table with missing values generated.
tblCols = tbl.Properties.VariableNames;
knownLevels = fieldnames(levelConfig);

for k = 1:numel(tblCols)
colName = tblCols{k};
if ~ismember(colName, knownLevels)
continue
end
col = tbl.(colName);
changable = isnan(col);
changableIdx = find(changable); % Get non-zero values
if sum(changable) > 0
numOptions = length(levelConfig.(colName));
for l = 1:length(changableIdx)
tbl(changableIdx(l), k) = {levelConfig.(colName)(randi(numOptions))};
end
end
end
end
50 changes: 50 additions & 0 deletions lib/setup/generateCombinations.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function [ combinations ] = generateCombinations(generative, complementary, startTable)
% Generates chosen combinations of generative and complementary properties
%
% Args:
% generative: a `struct` in which each field is a matrix of numerical values
% that you'd like all possible combinations of
% complementary (optional): a `struct` in which each field is a matrix of numerical values
% that you'd like distributed evenly, once per combination
% startTable (optional, deprecated): a `table` to vertically concatenate the
% results with prior to the addition of complementary values
%
% Returns:
% a `table` with duly combined and distributed values
%
% Warning:
% Strings in `generative` will not work. If you need them, use placeholder
% values and substitute the strings in later.

if isstruct(generative) && ~isempty(generative) && length(fieldnames(generative)) > 0
values = struct2cell(generative);
names = fieldnames(generative);

% Trick to record an arbitrary # of outputs & pass an arbitrary # of inputs:
allComb = cell(1, numel(names));
[allComb{:}] = ndgrid(values{:});
allComb = cellfun(@(x) x(:), allComb, 'UniformOutput', false);
allGenerative = horzcat(allComb{:});

combinations = array2table(allGenerative, 'VariableNames', names);
else
combinations = table;
warning('No generative properties provided.');
end

if exist('startTable', 'var')
if istable(startTable)
combinations = vertcat(startTable, combinations);
else
warning('startTable must be type table; skipping.');
end
end

if isempty(combinations)
return;
end

if exist('complementary', 'var') && isstruct(complementary)
combinations = addDispersedColumns(combinations, complementary);
end
end
56 changes: 56 additions & 0 deletions lib/setup/orderBlocksAcrossConditions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function [ blocksArray ] = orderBlocksAcrossConditions(orderMatrix, varargin)
% Given an order pattern, order blocks for any number of conditions
%
% Args:
% orderMatrix: a 1-m matrix of values ranging from 1 to n, where m is the
% number of blocks in the task and n is the number of conditions in the
% task. orderMatrix{m} = n means that m-th element of the matrix will be
% assigned from condition n. For example, [1 2 2 1] would be an order
% matrix that places blocks from condition 1 at the start and end of the
% task, and blocks from condition 2 in the middle.
% condition1_blocks, condition2_blocks, ...: cell array of blocks, as
% produced e.g. by `splitTrialsIntoBlocks` (although technically, the
% function doesn't care about the object class the block is represented
% with)

% check that at least one condition was received
narginchk(2, Inf);

% check that length of all blocks is equal of the number of blocks
% TODO: fix so that "some multiple" works too
orderCount = length(orderMatrix);
blocksPerConditionCount = cellfun(@(x) length(x), varargin);
blockCount = sum(blocksPerConditionCount);

if blockCount ~= orderCount
error('orderMatrix doesn''t contain enough elements to account for all passed blocks')
end

% check that length of all blocks is some multiple of the number of blocks
if rem(blockCount, orderCount) > 0
error('Length of orderMatrix does not divide the overall number of blocks evenly.');
end

% check that all conditions are represented in orderMatrix
conditionCount = nargin - 1;
uniqueOrderElts = length(unique(orderMatrix(:)));
if uniqueOrderElts ~= conditionCount
error('orderMatrix does not represent all passed blocks.')
end

% check that each condition is represented with the right number of blocksArray
orderEltsPerCondition = histcounts(orderMatrix(:));
if blocksPerConditionCount ~= orderEltsPerCondition
warning('orderMatrix does not have the right number of blocks to represent actual block divisions');
end

%% Actual logic
blocksArray = cell(1, blockCount);
conditionCounts = zeros(1, conditionCount);
for k = 1:length(orderMatrix)
currentCondition = orderMatrix(k);
conditionCounts(currentCondition) = conditionCounts(currentCondition) + 1;
countInCondition = conditionCounts(currentCondition);
blocksArray{k} = varargin{currentCondition}{countInCondition};
end
end
Loading