klabhub / neurostim

Design and run visual neuroscience experiments using Matlab and the Psychophysics Toolbox.
MIT License
4 stars 3 forks source link

adding new property "listMod" for MANUAL mode #176

Closed dshimaoka closed 2 years ago

dshimaoka commented 2 years ago

Followed the suggestion on my previous request https://github.com/klabhub/neurostim/pull/175#discussion_r709294389. Instead of changing the access to list, a new listMod property was added with SetAccess =public.

bartkrekelberg commented 2 years ago

This solution leaves the problem that someone has to set the manualSequence (which duplicates .list) but then also the mode. I was thinking of something like function shuffle(o,manualList) if nargin > 1 % List was provided. o.randomization = MANUAL o.list = manualList else % The old code end end

This requires no new internal members to keep track of the manual list.

dshimaoka commented 2 years ago

Thank you so much for the suggestion. I will rewrite shuffle with another input listManual. Now the question is how we deal with listManual. When we actually call shuffle, should we store this variable in RSVP struct:

addRSVP(s,design,varargin)
...
 p.addParameter('manualList', [], @(x) isnumeric(x));
 p.parse(design,varargin{:});
  flds = fieldnames(p.Results);
for i=1:numel(flds)
    s.rsvp.(flds{i}) = p.Results.(flds{i});
end

%Elaborate the factorial design into (sub)condition lists for RSVP
if ~isempty(p.Results.manualList)
    s.rsvp.design.shuffle(p.Results.manualList);
else
    s.rsvp.design.shuffle;
end

or should we not save listManual as in:

addRSVP(s,design,varargin)
...
manualList = [];
for i=1:numel(flds)
    if ~strcmp(flds{i}, 'manualList')
        s.rsvp.(flds{i}) = p.Results.(flds{i});
    else
        manualList = p.Results.(flds{i});
    end
end

If there is a way to retrieve o.list, I guess the latter is better to reduce overhead?

adammorrissirrommada commented 2 years ago

It's not clear to me how this approach can work. As it stands, shuffle() is called in several places (block, stimulus.rsvp) without any arguments and uses stored properties of design to build and re-build (when exhausted) the condition-by-trial list as the experiment progresses. The user doesn't call shuffle() directly.

Perhaps the different list-generator modes (sequential, randomwithreplacement, manual etc.) should be in separate functions and one of them is assigned as the "active" generator function depending on randomization (e.g. its handle is assigned to a new property named shuffleFun). For manual mode, the user can pass a handle to an arbitrary function that returns the list. That way, it can also support reactive changes in trial order based on results of earlier blocks.

dshimaoka commented 2 years ago

Thanks for the comment. The way the user pass a handle to an arbitrary function would be via addRSVP as: addRSVP(... 'manualList', list).

This is done once at the beginning of the experiment. When shuffle is subsequently called without any arguments in baseBeforeTrial or in updateRSVP, then o.list is recycled. The recycling of o.list in baseBeforeTrial is precisely what we aim to achieve. The recycling in updateRSVP would be what a user would expect, I suppose. My understanding of block is pretty shallow as I have not used block with rsvp. If you can suggest an example code, I can dig and think more deeply.

I guess a support for reactive changes in trial order based on results is bit too advanced at this stage, unless there is a demand for it.

I can upload another pull request implementing what I wrote and checked with my stimulus code.

adammorrissirrommada commented 2 years ago

The design object is meant to contain all the information about conditions, their repeats, and ordering etc. The same object, with the same code for specifying those details, is used whether you are defining a factorial design for an experiment (i.e. by using it to create a block and then calling c.run(myBlock), or for an RSVP stream by passing the design object to addRSVP(myDesign). The only difference between the two is whether it steps through the list across or within trials.

For this reason, any option for manual specification of condition order has to be stand-alone, built into design, not as an additional argument to addRSVP()

adammorrissirrommada commented 2 years ago

As I said above, I don't think the committed version is a good solution. It solves the problem you have now (manual rsvp) but not the one we will get asked about tomorrow (and have had in the past): "how can I set the order of trials in my experiment to an arbitrary order?".

Similar to @bartkrekelberg's suggestion, could it perhaps be achieved instead by a function in design.m named setConditionOrder(condOrder), which overrides value of o.randomization to manual and sets o.list. then, make shuffle() do nothing to o.list for that mode.

So list remains frozen until someone next calls setCondOrder() (if a new order is desired), which could be done in a beforeBlock() callback, for example.

This is essentially the same solution Bart suggested but it's wrapped in a more intuitive function name and would allow shuffle() to remain protected (is it?).

This allows you to set up the design, including order, and then hand that to addRSVP(). Cleaner because it solves both problems and keeps design functionality in design.m and out of stimulus.m

dshimaoka commented 2 years ago

Thanks, another pull request reflecting the Adam's suggestion will be uploaded.