When setting up for-loops, I want to be able to loop over an unknown number of parameters.
Through an input file, the user can set up as many or as few looping parameters as they desire, and I want to be able to loop over them regardless of the number of parameters they set up. Loop Input Example: (Note that my inputs can be lists of strings and number combinations as well)
Case 1:
- Weight 45000:5000:75000
- Altitude 10000
- Speed 0.2:0.1:0.9
Case 2:
- Weight 30000
- Altitude 1000:1000:10000
- Flaps 10, 20
- Temperature -10:1:10
The length of the list can differ, and can include anywhere from 0-15 variables. I know the workaround, but it's really messy to use a bunch of nested for-loops to do this. I'm looking for a way to set up a method, maybe with Recursion, where the code will set up an appropriate for-loop system regardless of the number of parameters involved, and still keeping track of those variables.
The code-generation solution
Well, you already have a number of pretty good solutions. I'll just toss one in there that involves code generation. MATLAB doesn't really have much in the way of those kind of tools, but you can fake it with a few loops and fprintf
. Here's my code generation script:
s = struct() ;
s.weight = 45000:5000:75000 ;
s.altitude = 10000 ;
s.engine = {'ge','rolsroyce'} ;
h = fopen('thisrun.m','w+') ;
mydisp = @(varargin)disp(transpose(varargin(:))) ; % dummy function body
vars = fields(s) ;
nv = numel(vars) ;
for ii = 1:nv
if isnumeric(s.(vars{ii}))
lb = '(' ;
rb = ')' ;
else
lb = '{' ;
rb = '}' ;
end
fprintf(h,'for i%g = 1:numel(s.(vars{%g})) \n',ii,ii) ;
fprintf(h,'i%gval = s.(vars{%g})%si%g%s ; \n',ii,ii,lb,ii,rb) ;
end
fprintf(h,'mydisp(') ;
for ii = 1:numel(vars)
fprintf(h,'i%gval',ii) ;
if ii<nv
fprintf(h,',') ;
end
end
fprintf(h,') ; \n') ;
for ii = 1:nv
fprintf(h,'end \n') ;
end
fclose(h) ;
run thisrun.m
Generated code (thisrun.m
):
for i1 = 1:numel(s.(vars{1}))
i1val = s.(vars{1})(i1) ;
for i2 = 1:numel(s.(vars{2}))
i2val = s.(vars{2})(i2) ;
for i3 = 1:numel(s.(vars{3}))
i3val = s.(vars{3}){i3} ;
mydisp(i1val,i2val,i3val) ;
end
end
end
Result of running generated code:
>>
[45000] [10000] 'ge'
[45000] [10000] 'rolsroyce'
[50000] [10000] 'ge'
[50000] [10000] 'rolsroyce'
[55000] [10000] 'ge'
[55000] [10000] 'rolsroyce'
[60000] [10000] 'ge'
[60000] [10000] 'rolsroyce'
[65000] [10000] 'ge'
[65000] [10000] 'rolsroyce'
[70000] [10000] 'ge'
[70000] [10000] 'rolsroyce'
[75000] [10000] 'ge'
[75000] [10000] 'rolsroyce'
The code generation takes time, but if you need to run the file many times it might still be an efficient solution.
Recursion.
You haven't put any speed requirements on this. This answer is slow, uses memory poorly, but is the simplest possible implementation of this idea. Many better implementations exist that are more complicated, use much less memory, and are significantly faster. Depends on how tight you need the inner loops to be...
parms.weight = [45000:5000:75000];
parms.alt = 10000;
parms.Speed = 0.2:0.1:0.9;
Then, define your simulator, as something like:
function result = simulation(parms)
fieldNames = fieldnames(parms)
result = [];
for ix = 1 : numel(fieldNames)
if 1 < numel(parms.(fieldNames{ix}))
list = parms.(fieldNames{ix});
for jx = 1 : numel(list)
tmpParms = parms;
tmpParms.(fieldNames{ix}) = list(jx);
tmpResult = simulation(tmpParms);
result = [result; tmpResult];
end
return;
end
end
if 0 == numel(result)
% Do the real simulation here.
end
Doing nested for
-loops for all parameters is the simple solution, constant parameters will simply result in a single iteration for that specific loop.
You could also use ndgrid
to create a set of multi-dimensional arrays that are the parameters for each call of your computeFunction(...)
, and iterate over the ngrid's
output instead of building nested for loops for each potentially variable parameter, if you expect to change the number of parameters.
However, be aware that it might come at a performance cost, as depending on what exactly you do in which matlab version, it might not benefit from the builtin optimizations the 'simple' for-loop version gets.
Here's a recursive function that produces all possible sets of parameters as column vectors of an array:
function idx=dynloop(varargin)
if nargin==1
idx=varargin{1};
else
idx=dynloop(varargin{2:end});
idx=[repelem(varargin{1},size(idx,2));
repmat(idx,[1,length(varargin{1})])];
end
return
Example output: loop over 5:7
, then 8
, then [2,3,5,7]
:
>> idx = dynloop(5:7,8,[2,3,5,7])
idx =
5 5 5 5 6 6 6 6 7 7 7 7
8 8 8 8 8 8 8 8 8 8 8 8
2 3 5 7 2 3 5 7 2 3 5 7
The output is equivalent to that of ndgrid
, (except in my opinion more intuitively shaped). (Also some preliminary benchmarking showed this may be slightly faster than ndgrid
...?! To be determined)
Main loop: Now just use one loop to iterate through the columns of idx
. Note this solution concept is similar to this solution, as commented by @Daniel on the OP.
Weight = 45000:5000:75000;
Altitude = 10000;
Speed = 0.2:0.1:0.9;
idx = dynloop(Weight,Altitude,Speed);
for ii=1:size(idx,2)
params = idx(:,ii);
%// do stuff with params!
%// ...
end
EDIT: to handle both string and numeric inputs
A simple mod to the original recursive function I posted allows it to return indices, rather than actual elements, so string cells are fine:
function idx=dynloop(varargin)
if nargin==1
idx=1:length(varargin{1});
else
idx=dynloop(varargin{2:end});
idx=[repelem(1:length(varargin{1}),size(idx,2));
repmat(idx,[1,length(varargin{1})]);];
end
return
So altogether, your function would presumably operate like this:
function yourMainFcn(varargin)
idx=dynloop(varargin{:});
for ii=1:size(idx,2)
params = cellfun(@(x,k) numORcell(x,k),...
varargin,num2cell(idx(:,ii).'),... %//'
'UniformOutput',0);
%// do stuff with params!
%// ...
disp(params)
end
end
where the function numORcell
appropriately parses numeric versus cell data:
function y=numORcell(x,k)
if iscell(x)
y=x{k};
else
y=x(k);
end
end
Example with strings:
Weight = 45000:5000:75000;
Altitude = 10000;
Speed = 0.2:0.1:0.9;
Names = {'Foo','Bar'};
>> yourMainFcn(Names,Altitude,Weight)
'Foo' [10000] [45000]
'Foo' [10000] [50000]
'Foo' [10000] [55000]
'Foo' [10000] [60000]
'Foo' [10000] [65000]
'Foo' [10000] [70000]
'Foo' [10000] [75000]
'Bar' [10000] [45000]
'Bar' [10000] [50000]
'Bar' [10000] [55000]
'Bar' [10000] [60000]
'Bar' [10000] [65000]
'Bar' [10000] [70000]
'Bar' [10000] [75000]
or instead:
>> yourMainFcn(Names,Names,Speed,Names)
'Foo' 'Foo' [0.2] 'Foo'
'Foo' 'Foo' [0.2] 'Bar'
'Foo' 'Foo' [0.3] 'Foo'
'Foo' 'Foo' [0.3] 'Bar'
'Foo' 'Foo' [0.4] 'Foo'
...
'Foo' 'Foo' [0.9] 'Bar'
'Foo' 'Bar' [0.2] 'Foo'
...
'Foo' 'Bar' [0.9] 'Bar'
'Bar' 'Foo' [0.2] 'Foo'
...
'Bar' 'Foo' [0.9] 'Bar'
...
'Bar' 'Bar' [0.8] 'Foo'
'Bar' 'Bar' [0.8] 'Bar'
'Bar' 'Bar' [0.9] 'Foo'
'Bar' 'Bar' [0.9] 'Bar'
Exercise left to the reader: If storing all the indices in idx
is a memory issue, you must be doing some pretty heavy looping. Nonetheless, you can create a function that determines from the current index set the next lexicographical index set, meaning you would only have to store one set of indices and iterate at the end of each loop.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With