Minsky
mdlReader.cc
Go to the documentation of this file.
1 /*
2  @copyright Steve Keen 2020
3  @author Russell Standish
4  This file is part of Minsky.
5 
6  Minsky is free software: you can redistribute it and/or modify it
7  under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  Minsky is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with Minsky. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "minsky.h"
21 #include "mdlReader.h"
22 #include "operation.h"
23 #include "userFunction.h"
24 #include "selection.h" //TODO why is this needed?
25 #include "minsky_epilogue.h"
26 
27 #include <boost/locale.hpp>
28 
29 using boost::locale::conv::utf_to_utf;
30 using namespace std;
31 namespace minsky
32 {
33  namespace
34  {
35  string readToken(istream& mdlFile, char delim, bool appendDelim=false)
36  {
37  string r;
38  string c;
39  while (mdlFile>>GetUtf8Char(c))
40  if (c[0]==delim || c[0]=='~' || c[0]=='|')
41  {
42  if (appendDelim)
43  r+=c[0];
44  break;
45  }
46  else if (c[0]=='{') /* inline comment - read up to close brace */
47  {
48  while (c[0]!='}' && mdlFile>>GetUtf8Char(c));
49  continue;
50  }
51  else if (c[0]=='\\')
52  // quoted end of line processing
53  if (mdlFile>>GetUtf8Char(c) && (c[0]=='\n'||c[0]=='\r'))
54  r+=c;
55  else
56  r+="\\"+c;
57  else if (c[0]>=' ') // strip out control characters
58  r+=c;
59 
60  return r;
61  }
62 
63  bool identifierChar(int c)
64  {return c>0x7f || isalnum(c) || c=='\'' || c=='$';}
65 
66  // collapse multiple whitespace characters, and if in the middle
67  // of an identifier (alphanum <space> alphanum, replace with
68  // underscore, and tolower the characters (case insensitive)
69  string collapseWS(const string& x)
70  {
71  // work in UTF-32 to simplify string analysis
72  auto xx=utf_to_utf<uint32_t>(x);
73  basic_string<uint32_t> result;
74  uint32_t lastNonWS=0;
75  bool quoted=false, lastWS=false, inIdentifier=false;
76  for (auto& i: xx)
77  {
78  if (i=='"' &&lastNonWS!='\\')
79  {
80  quoted=!quoted;
81  inIdentifier=quoted;
82  // identifiers not allowed to end in .
83  if (!inIdentifier && result.back()=='.')
84  result.erase(result.end()-1);
85  continue; // strip quotes
86  }
87 
88  if (!quoted && !inIdentifier && isalpha(i))
89  inIdentifier=true;
90  if (!quoted && inIdentifier && !identifierChar(i) && !isspace(i) && i!='_')
91  {
92  // identifiers not allowed to end in .
93  if (result.back()=='.')
94  result.erase(result.end()-1);
95  inIdentifier=false;
96  }
97 
98  if (!isspace(i) && i!='_')
99  {
100  // convert verboten characters into ones more friendly to exprtk
101  // we use . as an escape into a numerical unicode code uXXXX, terminated by another .
102  basic_string<uint32_t> exprTkGoodChar;
103  if (inIdentifier)
104  exprTkGoodChar=utf_to_utf<uint32_t>(".u"+to_string(i)+".");
105  else
106  exprTkGoodChar+=i;
107  // camelcase if collapsing whitespace in an identifier, ' ' otherwise
108  if (lastWS)
109  if (quoted || (identifierChar(i) && identifierChar(lastNonWS)))
110  if (isalnum(i))
111  result+=toupper(i);
112  else
113  result+=exprTkGoodChar;
114  else
115  {
116  result+=' ';
117  if (isalnum(i))
118  result+=tolower(i);
119  else
120  result+=exprTkGoodChar;
121  }
122  else
123  if (isalnum(i))
124  result+=tolower(i);
125  else
126  result+=exprTkGoodChar;
127  lastNonWS=i;
128  lastWS=false;
129  }
130  else
131  lastWS=true;
132  }
133  return utf_to_utf<char>(result);
134  }
135 
136  struct FunctionDef
137  {
138  std::string args;
139  std::string expression;
140  };
141 
142 
143  map<string, FunctionDef> venSimFunctions={
144  {"arccos",{"(x)","acos(x)"}},
145  {"arcsin",{"(x)","asin(x)"}},
146  {"arctan",{"(x)","atan(x)"}},
147  {"gammaLn",{"(x)","gammaLn(x)"}},
148  {"integer",{"(x)","floor(x)"}},
149  {"ifThenElse",{"(x,y,z)","x? y: z"}},
150  {"ln",{"(x)","log(x)"}},
151  {"log",{"(x,y)","log(x)/log(y)"}},
152  {"modulo",{"(x,y)","x%y"}},
153  {"power",{"(x,y)","x^y"}},
154  {"pulse",{"(x,y)","(time>=x)*(time<x+y)"}},
155  {"pulseTrain",{"(s,b,r,e)","tm:=time%r; (time<e)*(time>=s)*(tm<((s+b)%r))*(tm>=(s%r))"}},
156  {"quantum",{"(x,y)","floor(x/y)"}},
157  {"ramp",{"(s,a,b)","var t1:=min(a,b); var t2:=max(a,b); (clamp(t1,time,t2)-t1)*s"}},
158  {"step",{"(x,y)","y*(time>=x)"}},
159  {"xidz",{"(x,y,z)","var r:=y/z; isfinite(r)? r: x"}},
160  {"zidz",{"(x,y)","var r:=y/z; isfinite(r)? r:0"}}
161  };
162 
163  set<string> functionsAdded; // track user functions added to group
164  regex identifier(R"([A-Za-z][A-Za-z0-9._]*[A-Za-z0-9_])");
165 
166  void addDefinitionToPort(Group& group, const shared_ptr<Port>& port, const string& name, const string& definition)
167  {
168  smatch match;
169  if (regex_match(definition,match,identifier))
170  {
171  const VariablePtr rhs(VariableBase::flow, definition);
172  group.addItem(rhs);
173  if (port)
174  group.addWire(rhs->ports(0).lock(), port);
175  return;
176  }
177 
178  auto function=new UserFunction(name,definition);
179  group.addItem(function); //ownership passed
180  for (auto& i: function->symbolNames())
181  {
182  auto f=venSimFunctions.find(i);
183  if (f!=venSimFunctions.end())
184  {
185  if (functionsAdded.insert(f->first).second)
186  addDefinitionToPort(group,nullptr,f->first+f->second.args,f->second.expression);
187  }
188  }
189  if (port)
190  group.addWire(function->ports(0), port);
191  }
192 
193  void defineLookupFunction(Group& group, const std::string& name, const std::string& data)
194  {
195  const regex lookupPairsPattern(R"((\[[^\]]*\],)?(\(.*\)))");
196  smatch match;
197  map<double,double> xData;
198  if (regex_match(data,match,lookupPairsPattern))
199  {
200  const regex extractHead(R"(\(([^,]*),([^)]*)\)(,(\(.*\)))*)");
201  // note match[3] is the trailing data, match[4] strips the leading ,
202  for (auto d=match[2].str(); regex_match(d, match, extractHead); d=match[4])
203  xData[stod(match[1])]=stod(match[2]);
204  }
205  else
206  {
207  vector<double> xyData;
208  for (size_t offs=0; offs<data.size(); ++offs)
209  xyData.push_back(stod(data.substr(offs),&offs));
210  if (xyData.size()%2!=0)
211  throw runtime_error("Odd amount of data specified");
212  for (size_t i=0; i<xyData.size()/2; ++i)
213  xData[xyData[i]]=xyData[i+xyData.size()/2];
214  }
215  auto f=make_shared<Group>();
216  f->self=f;
217  f->title=name;
218  group.addItem(f);
219  const VariablePtr dataVar(VariableType::flow,"data");
220  f->addItem(dataVar);
221  dataVar->moveTo(f->x()-50,f->y()-20);
222  const OperationPtr gather(OperationType::gather);
223  f->addItem(gather);
224  gather->moveTo(f->x()+30,f->y()-10);
225  const VariablePtr inVar(VariableType::flow,"in"), outVar(VariableType::flow,"out");
226  f->addItem(inVar);
227  f->addItem(outVar);
228  f->inVariables.push_back(inVar);
229  f->outVariables.push_back(outVar);
230  f->addWire(*dataVar, *gather, 1);
231  f->addWire(*f->inVariables[0], *gather, 2);
232  f->addWire(*gather, *f->outVariables[0], 1);
233 
234  XVector xVals("0",{Dimension::value,""});
235  auto& tensorInit=dataVar->vValue()->tensorInit;
236  for (auto& i: xData)
237  xVals.push_back(i.first);
238  Hypercube hc; hc.xvectors.push_back(std::move(xVals));
239  tensorInit.hypercube(std::move(hc));
240 
241  assert(tensorInit.size()==xData.size());
242  auto j=tensorInit.begin();
243  for (auto& i: xData)
244  *j++=i.second;
245 
246  *dataVar->vValue()=tensorInit;
247  }
248  }
249 
250  void readMdl(Group& group, Simulation& simParms, istream& mdlFile)
251  {
252  set<string> integrationVariables;
253  const regex integ(R"(\s*integ\s*\(([^,]*),([^,]*)\))");
254  const regex number(R"(\d*\.?\d+[Ee]?\d*)");
255  const regex unitFieldPattern(R"(([^\[\]]*)(\[.*\])?)");
256  const regex sliderSpecPattern(R"(\[([^,]*),?([^,]*),?([^,]*)\])");
257  const regex lookupPattern(R"(([^(]*)\((.*)\))");
258  smatch match;
259  functionsAdded.clear();
260 
261  string c;
262  string currentMDLGroup;
263  while (mdlFile>>GetUtf8Char(c))
264  {
265  if (isspace(c[0])) continue;
266  switch (c[0])
267  {
268  case '{':
269  // check this is a UTF-8 file
270  if (trimWS(readToken(mdlFile,'}'))!="UTF-8")
271  throw runtime_error("only UTF-8 file encoding is supported");
272  continue;
273 
274  case '*':
275  // group leadin
276  for (; c[0]=='*'; mdlFile>>GetUtf8Char(c));
277  currentMDLGroup=trimWS(readToken(mdlFile,'*'));
278 
279  while (mdlFile>>GetUtf8Char(c))
280  if (c[0]=='|')
281  {
282  c.clear();
283  break; // end of group leadin
284  }
285  break;
286  }
287 
288  // macros?
289  // read variable name
290  string nameStr=readToken(mdlFile,'=',true /* append delimiter */);
291  const string name=collapseWS(trimWS(c+nameStr.substr(0,nameStr.length()-1)));
292  if (name.substr(0,9)==R"(\\\---///)")
293  break; // we don't parse the sketch information - not used in Minsky
294  string definition;
295  if (nameStr.back()=='=')
296  // only read definition if this was a variable definition
297  definition=collapseWS(trimWS(readToken(mdlFile,'~')));
298  switch (definition[0])
299  {
300  case '=': case ':': // for now, treat constant assignment and data assignment equally to numeric assignment
301  definition.erase(definition.begin());
302  break;
303  default:
304  break;
305  }
306  auto unitField=trimWS(readToken(mdlFile,'~'));
307  regex_match(unitField,match,unitFieldPattern);
308  const string units=trimWS(match[1]);
309  string sliderSpec;
310  if (match.size()>1)
311  sliderSpec=match[2];
312 
313  const string comments=trimWS(readToken(mdlFile,'|'));
314 
315 
316  if (currentMDLGroup==".Control")
317  {
318  if (name=="timeStep")
319  {
320  simParms.timeUnit=units;
321  if (regex_match(definition,match,number))
322  simParms.stepMax=stod(definition);
323  }
324  if (!regex_match(definition,match,number)) continue;
325  if (name=="initialTime") simParms.t0=stod(definition);
326  if (name=="finalTime") simParms.tmax=stod(definition);
327  continue;
328  }
329  if (regex_match(name,match,lookupPattern))
330  defineLookupFunction(group, match[1], match[2]);
331  else if (regex_match(definition,match,integ))
332  {
333  auto intOp=new IntOp;
334  group.addItem(intOp);
335  intOp->description(name);
336  intOp->detailedText(comments);
337  auto& v=intOp->intVar;
338  integrationVariables.insert(name);
339  auto integrand=match[1].str();
340  addDefinitionToPort(group, intOp->ports(1).lock(), "Integrand of "+name,integrand);
341 
342  auto init=match[2].str();
343  if (regex_match(init,match,identifier))
344  v->init(init);
345  else
346  {
347  // we need to add another variable, and attach it to a function block
348  addDefinitionToPort(group, intOp->ports(2).lock(), "Initial value of "+name, init);
349  }
350  try // absorb any errors in units - we have a chance to fix these later
351  {
352  v->setUnits(units);
353  }
354  catch (...) {}
355  v->detailedText(comments);
356 
357  }
358  else if (regex_match(definition,match,number))
359  {
360  const VariablePtr v(VariableBase::parameter, name);
361  group.addItem(v);
362  v->init(definition);
363  try // absorb any errors in units - we have a chance to fix these later
364  {
365  v->setUnits(units);
366  }
367  catch (...) {}
368  v->detailedText(comments);
369  if (auto vv=v->vValue())
370  {
371  if (regex_match(sliderSpec,match,sliderSpecPattern))
372  {
373  vector<string> spec;
374  for (size_t i=1; i<=match.size(); ++i) spec.push_back(match[i]);
375  if (!spec.empty() && regex_match(spec[0],match,number))
376  vv->sliderMin=stod(spec[0]);
377  else
378  vv->sliderMin=0.1*stod(definition);
379  if (spec.size()>1 && regex_match(spec[1],match,number))
380  vv->sliderMax=stod(spec[1]);
381  else
382  vv->sliderMax=10*stod(definition);
383  if (spec.size()>2 && regex_match(spec[2],match,number))
384  vv->sliderStep=stod(spec[2]);
385  }
386  vv->adjustSliderBounds();
387  }
388  }
389  else
390  {
391  const VariablePtr v(VariableBase::flow, name);
392  group.addItem(v);
393  addDefinitionToPort(group, v->ports(1).lock(), "Def: "+name, definition);
394  try // absorb any errors in units - we have a chance to fix these later
395  {
396  v->setUnits(units);
397  }
398  catch (...) {}
399  v->detailedText(comments);
400  }
401  }
402 
403  }
404 }
function f
Definition: canvas.m:1
shared_ptr class for polymorphic operation objects. Note, you may assume that this pointer is always ...
STL namespace.
string readToken(istream &mdlFile, char delim, bool appendDelim=false)
Definition: mdlReader.cc:35
map< string, FunctionDef > venSimFunctions
Definition: mdlReader.cc:143
struct TCLcmd::trap::init_t init
Creation and access to the minskyTCL_obj object, which has code to record whenever Minsky&#39;s state cha...
Definition: constMap.h:22
std::string timeUnit
Definition: simulation.h:35
void readMdl(Group &group, Simulation &simParms, istream &mdlFile)
import a Vensim mdl file into group, also populating simParms from the control block ...
Definition: mdlReader.cc:250
std::string trimWS(const std::string &s)
Definition: str.h:49
std::string str(T x)
utility function to create a string representation of a numeric type
Definition: str.h:33
void defineLookupFunction(Group &group, const std::string &name, const std::string &data)
Definition: mdlReader.cc:193
void addDefinitionToPort(Group &group, const shared_ptr< Port > &port, const string &name, const string &definition)
Definition: mdlReader.cc:166
string collapseWS(const string &x)
Definition: mdlReader.cc:69
regex identifier(R"([A-Za-z][A-Za-z0-9._]*[A-Za-z0-9_])")
Definition: group.tcl:84
string to_string(CONST84 char *x)
Definition: minskyTCLObj.h:33