function [model,name_map] = dsParseModelEquations(text,varargin)
%PARSEMODELEQUATIONS - parse equations and organize model data in DynaSim model structure
%
% Usage:
% model = dsParseModelEquations(STRING,'param',value,...)
%
% Inputs:
% - STRING (required): one of:
% - string with equations
% - string with name of file containing equations (.eqns or .mech)
% - options (using key/value pairs: 'option1',value1,...):
% - 'namespace': added as prefix to beginning of parameter/etc names (default: '')
% - 'delimiter': separates expressions on same line of model text (default: ';')
% - user-supplied parameter values: ('key',value): name (key) of parameters to
% be set and associated user-supplied values
% Outputs:
% - model: DynaSim model structure (see dsCheckModel for details)
% - name_map: useful for namespace-specific substitutions across multiple
% sub-models, see description in dsGenerateModel for more information {name,
% namespace_name, namespace, type}
%
% Notes:
% - NOTE 1: .eqns files contain fully self contained model equations; .mech
% files define (sub)models that depend on variables linked from elsewhere.
% However, this function does not distinguish between the two.
%
% Examples:
% model = dsParseModelEquations('dx/dt=3*a*x; x(0)=0','a',0);
% model = dsParseModelEquations('dx/dt=3*a*x, x(0)=0','a',0,'delimiter',',');
% model = dsParseModelEquations('CalciumPump.mech','namespace','HH');
% model = dsParseModelEquations('LIFneuron.eqns');
% model = dsParseModelEquations('a=2; b=2*a; f(x)=b; dx/dt=f(x); x(0)=0; if(x>1)(x=0); current->f(x); monitor f(x); % comments')
%
% - parsing individual sub-models from specification:
% equations=specification.populations(1).equations;
% [model,map] = dsParseModelEquations(equations,'namespace','pop')
% population_mechanism=specification.populations(1).mechanism_list{1};
% [model,map] = dsParseModelEquations(population_mechanism,'namespace','pop_mech')
% connection_mechanism=specification.connections(1).mechanism_list{1};
% [model,map] = dsParseModelEquations(connection_mechanism,'namespace','pop_pop_mech')
%
% See also: dsClassifyEquation, dsGenerateModel, dsLocateModelFiles
%
% Author: Jason Sherfey, PhD <jssherfey@gmail.com>
% Copyright (C) 2016 Jason Sherfey, Boston University, USA
%% auto_gen_test_data_flag argin
options = dsCheckOptions(varargin,{'auto_gen_test_data_flag',0,{0,1}},false);
if options.auto_gen_test_data_flag
varargs = varargin;
varargs{find(strcmp(varargs, 'auto_gen_test_data_flag'))+1} = 0;
varargs(end+1:end+2) = {'unit_test_flag',1};
argin = [{text}, varargs]; % specific to this function
end
model=[];
name_map={};
% organize optional user-supplied info
% key/value pairs
if nargin>2 % check for at least text input and one key/value pair
keys=varargin(1:2:end); % parameters to set
values=varargin(2:2:end); % values to use
else
keys=[];
values=[];
end
% set namespace
if ~isempty(keys) && ismember('namespace',keys) % check for user-supplied namespace (i.e., namespace)
namespace=values{ismember(keys,'namespace')}; % user-supplied namespace
if ~isempty(namespace)
namespace=[namespace '_'];
else
namespace='';
end
else
namespace='';
end
% set delimiter
if ~isempty(keys) && ismember('delimiter',keys) % check for user-supplied delimiter
delimiter = values(ismember(keys,'delimiter')); % user-supplied delimiter
else
delimiter=';';
end
% error handling for improper input format
if ~ischar(namespace)
error('model "namespace" must be a string.');
end
if ~ischar(delimiter)
error('expression "delimiter" must be a string.');
end
%% 1.0 convert text into cell array of strings (one string per line)
% check for DynaSim extensions if input is single \w+ string
if ischar(text) && isempty(regexp(text,'[^\w.]','once')) && ~any(which(text)) % isempty(regexp(text,'[^\w]','once'))
%if ischar(text) && ~any(which(text)) && isempty(regexp(text,'[^\w.]','once')) % isempty(regexp(text,'[^\w]','once'))
%if ischar(text) && ~exist(text,'file') && isempty(regexp(text,'[^\w.]','once')) % isempty(regexp(text,'[^\w]','once'))
[~,text]=dsLocateModelFiles(text);
if iscell(text) && ~isempty(text)
text=text{1};
end
end
% check if input is a filename
if ischar(text) && exist(text,'file')
text = dsReadText(text);
% [~,name,ext]=fileparts2(text);
% switch ext
% case '.m'
% model=feval(name); % evaluate model-creating function and return model
% return;
% case '.mat' % todo: uncomment once dsImportModel supports loading .mat
% %model=dsImportModel(text);
% %return;
% end
%
% % load equations from file
% [text,res]=readtext(text,'\n','%'); % text: cell array of strings, one element per line in text file
%
% % remove all lines without text
% text=text(res.stringMask);
%
% % remove leading/trailing white space
% text=strtrim(text);
%
% % end each line with semicolon
% for i=1:length(text)
% if ~isequal(text{i}(end),';')
% text{i}(end+1)=';';
% end
% end
%
% % concatenate into a single string
% text=[text{:}]; % concatenate text from all lines
end
% split string into cell array of lines delimited by semicolon
if ischar(text)
% remove end-line semicolon if present so split lines are free of all ';'
if text(end)==';'
text=text(1:end-1);
end
% account for the one exception where ';' does not delimit lines:
% conditional actions with multiple statements (expr1; expr2)
% approach: replace ';' by ',' here then reverse the replacement below
% when storing the action in model.conditionals
pattern='(if\([^;]+\)\s*\([^;\)]+);([^;]+\))'; % if(condiiton)(action1;action2)
replace='$1,$2';
text=regexprep(text,pattern,replace,'ignorecase');
% now split string into cell array of lines
text = strtrim(regexp(text,delimiter,'split'));
end
if ~iscellstr(text)
error('input not recognized. equations must be provided in single string, cell array of strings, or a text file');
end
%% 2.0 classify and parse lines; store info in model structure
% use empty struct for Octave compatibility
model.parameters=struct('');
model.fixed_variables=struct('');
model.functions=struct('');
model.monitors=struct('');
model.state_variables={};
model.ODEs=struct('');
model.ICs=struct('');
model.conditionals=struct('');
model.linkers=struct('');
model.comments={};
for index=1:length(text) % loop over lines of text
% organize model data in model structure
line=text{index}; % choose a single expression (lines with multiple expressions have already been split above using regexp-split)
[line,comment]=remove_comment(line); % remove comments
if isempty(line) % e.g., entire line was a comment, or there was nothing there originally
if ~isempty(comment)
model.comments{end+1}=comment;
end
continue;
end
switch dsClassifyEquation(line,delimiter) % classify
case 'parameter' % var=(string or number)
rhs=regexp(line,'=(.+)$','tokens','once');
lhs=regexp(line,'^([\w\.]+)\s*=','tokens','once');
lhs{1}=strrep(lhs{1},'.','_'); % e.g., Na.g --> Na_g
name=strtrim(lhs{1}); expression=rhs{1};
model.parameters(1).([namespace name]) = expression;
name_map(end+1,:) = {name,[namespace name],namespace,'parameters'};
if ~isempty(comment)
model.comments{end+1}=sprintf('%s (parameter): %s',[namespace name],comment);
end
case 'fixed_variable' % var=(expression with grouping or arithmetic), var(#), var([#]), var([# #]), var([#,#]), var(#:#), var(#:end), var([#:#]), var([#:end])
lhs=regexp(line,'^(\w+)\s*=','tokens','once');
if ~isempty(lhs)
rhs=regexp(line,'=(.+)$','tokens','once');
name=strtrim(lhs{1}); expression=rhs{1};
model.fixed_variables(1).([namespace name]) = expression;
name_map(end+1,:) = {name,[namespace name],namespace,'fixed_variables'};
else
% check for update to fixed variable that is already defined
lhs=regexp(line,'^(.+)\(.*\)\s*=','tokens','once');
name=strtrim(lhs{1});
if isfield(model.fixed_variables(1),[namespace name])
% add update to fixed variable definition
expression=[model.fixed_variables(1).([namespace name]) ';' line];
model.fixed_variables(1).([namespace name]) = expression;
else
warning('failed to set fixed variable.');
end
end
if ~isempty(comment)
model.comments{end+1}=sprintf('%s (fixed_variable): %s',[namespace name],comment);
end
case 'function' % f(vars)=exression
% if any(line=='@')
% line=strrep(line,'@','');
% %error('model specification error: delete the ''@'' character from all function definitions and try again.');
% end
name=regexp(line,'^(.+)\(.*\)\s*=','tokens','once');
vars=regexp(line,'\((.+)\)\s*=','tokens','once');
rhs=regexp(line,'=(.+)$','tokens','once');
name=strtrim(name{1});
expression=sprintf('@(%s)%s',vars{1},rhs{1});
model.functions(1).([namespace name]) = expression;
name_map(end+1,:) = {name,[namespace name],namespace,'functions'};
if ~isempty(comment)
model.comments{end+1}=sprintf('%s (function): %s',[namespace name],comment);
end
case 'ODE' % x'=expression or dx/dt=expression
var=regexp(line,'^d(\w+)/dt\s*=','tokens','once'); % x from dx/dt=
if isempty(var)
var=regexp(line,'^(\w+)''\s*=','tokens','once'); % x from x'=
end
rhs=regexp(line,'=(.+)$','tokens','once');
state_variable = strtrim(var{1}); expression=rhs{1};
model.ODEs(1).([namespace state_variable])=expression;
if ~ismember([namespace state_variable],model.state_variables)
name_map(end+1,:) = {state_variable,[namespace state_variable],namespace,'state_variables'};
model.state_variables{end+1}=[namespace state_variable];
end
if ~isempty(comment)
model.comments{end+1}=sprintf('d/dt %s (ODE): %s',[namespace state_variable],comment);
end
case 'IC' % x(0)=expression
var=regexp(line,'^(\w+)\(','tokens','once');
rhs=regexp(line,'=(.+)$','tokens','once');
state_variable = strtrim(var{1});
expression = rhs{1};
% convert scalars to vectors for when npop > 1 to permit compiler
if ~isnan(str2double(expression))
expression = [expression ' * ones(1,Npop)'];
end
model.ICs(1).([namespace state_variable])=expression;
if ~isempty(comment)
model.comments{end+1}=sprintf('%s(0) (IC): %s',[namespace state_variable],comment);
end
case 'monitor' % monitor f=(expression or function)
% split list of monitors
lines=strtrim(regexp(line,',','split'));
% combine elements with args for same monitor (e.g., v.spikes(0,5))
idx=~cellfun(@isempty,regexp(lines,'^\d'));
if any(idx)
tmp={};
for i=1:length(idx)
if idx(i)==0 || i==1
tmp{end+1}=lines{i};
else
tmp{end}=[tmp{end} ',' lines{i}];
end
end
lines=tmp;
end
% loop over monitors in list
for l=1:length(lines)
% process this monitor
line=lines{l};
% split left and right parts of monitor
lhs=regexp(line,'^monitor\s*([\w,@\s\.]+)','tokens','once');
if isempty(lhs)
lhs=regexp(line,'([\w,@\s\.]+)','tokens','once');
end
rhs=regexp(line,'=(.+)$','tokens','once');
% expand list of monitor names (e.g., monitor iNa.I, iK.I)
names = strtrim(regexp(lhs{1},',','split'));
for i=1:length(names) % loop over list of monitors on this line
name = names{i};
% process special monitors (those including '.', e.g., v.spikes(0))
% TODO: clean up or generalize this procedure...
if any(name=='.')
% check for numeric monitor argument
arg=regexp(line,[name '\(([-+]*\w+)\)'],'tokens','once');
% set argument as expression (see dsWriteDynaSimSolver() for usage as such)
if ~isempty(arg)
rhs=arg;
else
arg=regexp(line,[name '\(([-+\w,]+)\)'],'tokens','once');
if ~isempty(arg)
rhs={['(' arg{1} ')']};
end
end
end
% convert into valid monitor name
name = strrep(name,'.','_'); % index sub-namespace (monitor Na.I)
scopeName = [namespace name];
% determine expression (ie rhs)
if ~isempty(rhs)
expression = rhs{1};
elseif isempty(rhs) && isfield(model.functions,scopeName) % empty monitor RHS with LHS=function
% set expression if monitoring function referenced by name
rhs = regexp(model.functions.(scopeName),'@\([a-zA-Z][\w,]*\)\s*(.*)','tokens','once');
expression = rhs{1};
else
expression=[];
end
% store monitor
model.monitors(1).(scopeName) = expression;
name_map(end+1,:) = {name, scopeName, namespace, 'monitors'};
if ~isempty(comment)
model.comments{end+1}=sprintf('%s (monitor): %s', scopeName,comment);
end
end
end
case 'conditional' % if(conditions)(actions)
groups=regexp(line,'\)\(','split');
condition=regexp(groups{1},'^if\s*\((.*)','tokens','once');
if length(groups)==2
if groups{2}(end)==')'
groups{2}=groups{2}(1:end-1);
end
then_action=groups{2};
else_action=[];
elseif numel(groups==3)
if groups{3}(end)==')'
groups{3}=groups{3}(1:end-1);
end
then_action=groups{2};
else_action=groups{3};
end
model.conditionals(end+1).namespace=namespace;
model.conditionals(end).condition=condition{1};
model.conditionals(end).action=strrep(then_action,',',';'); % restore semicolon-delimited multiple actions like if(x>1)(x=0;y=0)
if length(groups)>2
model.conditionals(end).else=else_action;
else
model.conditionals(end).else=[];
end
if ~isempty(comment)
model.comments{end+1}=sprintf('%s conditional(%s): %s',namespace,condition,comment);
end
case 'linker' % [link ]? target operation expression (e.g., link target += f(x))
% viable options: ((\+=)|(-=)|(\*=)|(/=)|(=>))
line=regexprep(line,'^link (\s*\w)','$1');
lhs=regexp(line,'^([^\+\-*/=]+)','tokens','once'); % +=
rhs=regexp(line,'=>?(.+)$','tokens','once');
model.linkers(end+1).namespace=namespace;
if ~isempty(lhs), target=strtrim(lhs{1}); else target=[]; end
if ~isempty(rhs), expression=strtrim(rhs{1}); else expression=[]; end
if expression(end)==';', expression=expression(1:end-1); end
if isempty(target), target=expression; end % for sharing state var across mechanisms in same population
if isempty(expression), expression=target; end
model.linkers(end).target=target;
model.linkers(end).expression=expression;
model.linkers(end).operation='+=';
if ~isempty(comment)
model.comments{end+1}=sprintf('%s linkers(%s->%s): %s',namespace,target,expression,comment);
end
if ~isempty(comment)
model.linkers(end).comment=comment;
end
otherwise
warning('ignoring line, failed to classify :%s',line);
end
end
%% auto_gen_test_data_flag argout
if options.auto_gen_test_data_flag
argout = {model, name_map}; % specific to this function
dsUnitSaveAutoGenTestData(argin, argout);
end
end % main fn
%% local functions
function [line,comment]=remove_comment(line)
% purpose: split line into model content and comment
index=find(line=='%',1,'first');
if isempty(index)
% check other valid comment delimiters
index=find(line=='#',1,'first');
end
if isempty(index) % no comment found
comment='';
else % split line into comment and non-comment line sections
comment=line(index:end);
line=line(1:index-1);
end
end