modelflowib 2.73__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modelpattern.py ADDED
@@ -0,0 +1,428 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Mon Sep 02 19:32:22 2013
4
+
5
+ This module defines a number of pattern used in PYFS.
6
+ If a new function is intruduced in the model definition language it should added to the
7
+ function names in funkname
8
+
9
+ All functions in the module modeluserfunk will be added to the language and incorporated in the Business
10
+ Logic language
11
+
12
+ @author: Ib
13
+ """
14
+ import re
15
+ import inspect
16
+ from collections import namedtuple
17
+ from collections import defaultdict
18
+ from typing import NamedTuple, Optional
19
+
20
+ import modelmanipulation as mp
21
+ import modelBLfunk
22
+
23
+ class FrmlParts(NamedTuple):
24
+ """Parsed components of a FRML statement."""
25
+ whole: str # The full matched FRML line (up to and incl. $)
26
+ frml: str # Literal 'FRML' (case-insensitive)
27
+ frmlname: str # The FRML name (must include <...>)
28
+ expression: str # The RHS expression text including trailing '$'
29
+
30
+
31
+
32
+ # names and lags
33
+ namepat_ng = r'(?:[A-Za-z_{][A-Za-z_{}0-9]*)' # a name non grouped
34
+ namepat = r'(' + namepat_ng + ')' # a name grouped
35
+ lagpat = r'(?:\(([+-][0-9]+)\))?'
36
+
37
+ # comments
38
+ commentchar = '£'
39
+ commentpat = r'('+commentchar+r'.*)'
40
+
41
+ try: # import the names of functions defined in modeluserfunk
42
+ import modeluserfunk
43
+ userfunk = [o.upper() for o,t in inspect.getmembers(modeluserfunk) if not o.startswith('__')]
44
+ except:
45
+ userfunk = []
46
+
47
+ BLfunk = [o.upper() for o,t in inspect.getmembers(modelBLfunk) if not o.startswith('__') ]
48
+ classfunk = modelBLfunk.classfunk
49
+ # Operators
50
+ funkname = 'DLOG SUM_EXCEL DIFF MIN MAX FLOAT NORM.CDF NORM.PPF ABS MOVAVG PCT_GROWTH'.split() + BLfunk+ userfunk + classfunk
51
+ funkname2 = [i+r'(?=\()' for i in funkname] # a function is followed by a (
52
+ opname = r'\*\* != >= <= == [=+-/*@|()$><,.\]\[]'.split() # list of ordinary operators
53
+ oppat = '('+'|'.join(['(?:' + i + ')' for i in funkname2+opname])+')'
54
+
55
+ # Numbers
56
+ numpat = r'((?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]\d+)?)'
57
+ numpat = r'((?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)'
58
+
59
+
60
+ # Formulars
61
+ dollarpat = r'([$]'
62
+ upat = r'([^$]*\$)' # resten frem til $
63
+ frmlpat = r'(FRML [^$]*\$)' # A FORMULAR for splitting a model in formulars
64
+ optionpat = r'(?:[<][^>]*[>])?' # 0 eller en optioner omsluttet af <>
65
+ optionpat_req = r'(?:[<][^>]*[>])' # required <...> block
66
+
67
+ #White space
68
+ ws = r'[\s]+' # 0 or more white spaces
69
+ ws2 = r'[\s]*' # 1 or more white spaces
70
+
71
+ splitpat = namepat + ws + \
72
+ '(' + namepat_ng + '?' + ws2 + optionpat + ')' + ws + upat # Splits a formular
73
+ splitpat_reqopts = re.compile(
74
+ f"{namepat}{ws}" # 1) FRML (name token)
75
+ f"({optionpat_req})" # 2) frmlname MUST include <...>
76
+ f"{ws}{upat}" # 3) everything up to next $
77
+ , flags=re.IGNORECASE)
78
+
79
+
80
+
81
+
82
+
83
+ # for splitting a model in commands and values
84
+ statementpat = commentpat + '|' + namepat + ws2 + upat
85
+
86
+ #udtrykpat = commentpat + '|' + numpat + '|' + oppat + '|' + namepat + lagpat
87
+ udtrykpat = numpat + '|' + oppat + '|' + namepat + lagpat
88
+
89
+ udtrykre_old = re.compile(udtrykpat)
90
+ nterm = namedtuple('nterm', ['number', 'op', 'var', 'lag'])
91
+
92
+
93
+ def udtrykre(funks=[]):
94
+ global funkname
95
+ newfunks = [f.__name__.upper() for f in funks]
96
+ funkname = 'DLOG SUM_EXCEL DIFF MIN MAX FLOAT NORM.CDF NORM.PPF ABS MOVAVG PCT_GROWTH'.split() + BLfunk+ userfunk + classfunk + newfunks
97
+ # print(funkname)
98
+ funkname2 = [i+r'(?=\()' for i in funkname] # a function is followed by a (
99
+ opname = r'\*\* != >= <= == [=+-/*@|()$><,.\]\[]'.split() # list of ordinary operators
100
+ oppat = '('+'|'.join(['(?:' + i + ')' for i in funkname2+opname])+')'
101
+ # udtrykpat = commentpat + '|' + numpat + '|' + oppat + '|' + namepat + lagpat
102
+ udtrykpat = numpat + '|' + oppat + '|' + namepat + lagpat
103
+ return re.compile(udtrykpat)
104
+
105
+ #udtrykpatnew([f1,f2])
106
+
107
+
108
+ def find_frml(equations):
109
+ ''' Takes at modeltext and returns a list with where each element is
110
+ a string starting with FRML and ending with $
111
+ It do not check if it is a valid FRML statement '''
112
+ return re.findall(frmlpat, equations, flags=re.IGNORECASE)
113
+
114
+
115
+ def split_frml(frml):
116
+ ''' Splits a string with a frml into a tuple with 4 parts:
117
+
118
+ 0. The unsplit frml statement
119
+ 1. FRML
120
+ 2. <Frml name>
121
+ 3. <the frml expression>
122
+
123
+ '''
124
+ m = re.search(splitpat, frml)
125
+ if m:
126
+ return m.group(0), m.group(1), m.group(2), m.group(3)
127
+ else:
128
+ return frml
129
+
130
+
131
+ def split_frml_reqopts(frml: str) -> Optional[FrmlParts]:
132
+ """
133
+ Splits a FRML string where the FRML name MUST include <...> options.
134
+
135
+ Returns:
136
+ FrmlParts named tuple on match, else None.
137
+ """
138
+ m = re.search(splitpat_reqopts, frml)
139
+ if not m:
140
+ raise ValueError(
141
+ f" FRML does not match required pattern "
142
+ f"'FRML <...> ...$' → got: {frml!r}"
143
+ )
144
+ expr = m.group(3)
145
+ if expr.strip().endswith("$"):
146
+ expr = expr[:-1].strip()
147
+
148
+ return FrmlParts(
149
+ whole=m.group(0),
150
+ frml=m.group(1),
151
+ frmlname=m.group(2),
152
+ expression=expr
153
+ )
154
+
155
+ def find_statements(a_model):
156
+ ''' splits a modeltest into comments and statements
157
+
158
+ * a *comment* starts with ! and ends at lineend
159
+ * a *statement* starts with a name and ends with a $ all characters between are considerd part of the statement
160
+
161
+ The statement is not chekked for meaningfulness
162
+ returns a list of tuppels (comment,command,<rest of statement>)
163
+ '''
164
+ return(re.findall(statementpat, a_model))
165
+
166
+ def model_parse_old(equations,funks=[]):
167
+ '''Takes a model returns a list of tupels. Each tupel contains:
168
+ :the compleete formular:
169
+ :FRML:
170
+ :formular name:
171
+ :the expression:
172
+ :list of terms from the expression:
173
+
174
+ The purpose of this function is to make model analysis faster. this is 20 times faster than looping over espressions in a model
175
+ '''
176
+ fatoms = namedtuple('fatoms', 'whole, frml ,frmlname, expression')
177
+ # nterm = namedtuple('nterm', [ 'number', 'op', 'var', 'lag'])
178
+ expressionre= udtrykre(funks)
179
+ ibh = [(fatoms(*c),[nterm(*t) for t in expressionre.findall(c[3])]) for c in (split_frml(f) for f in find_frml(equations.upper()) )]
180
+ return ibh
181
+
182
+ def model_parse(equations,funks=[]):
183
+ '''Takes a model returns a list of tupels. Each tupel contains:
184
+ :the compleete formular:
185
+ :FRML:
186
+ :formular name:
187
+ :the expression:
188
+ :list of terms from the expression:
189
+
190
+ The purpose of this function is to make model analysis faster. this is 20 times faster than looping over espressions in a model
191
+
192
+ This new model_parse handels lags of -0 or +0 which ocours in some models from world bank.
193
+ '''
194
+ fatoms = namedtuple('fatoms', 'whole, frml ,frmlname, expression')
195
+ # nterm = namedtuple('nterm', [ 'number', 'op', 'var', 'lag'])
196
+ expressionre= udtrykre(funks)
197
+ ibh = [(fatoms(*c),
198
+ [ nterm(t[0],t[1],t[2], '' if t[3] == '-0' or t[3]=='+0' else t[3]) for t in expressionre.findall(c[3])])
199
+ for c in (split_frml(f) for f in find_frml(equations.upper()) )]
200
+ return ibh
201
+
202
+
203
+ def list_extract(equations, silent=True, add_auto_sublists=True):
204
+ ''' creates lists used in a model
205
+
206
+ returns a dictonary with the lists
207
+ if a list is defined several times, the first definition is used'''
208
+ liste_dict = {}
209
+
210
+ for comment, command, value in find_statements(equations):
211
+ if command.upper() == 'LIST':
212
+ stripvalue = ' '.join(value.upper().splitlines())
213
+ list_name, list_value = stripvalue[:-1].split('=', 1)
214
+ list_name = list_name.strip()
215
+
216
+ if list_name in liste_dict:
217
+ if not silent:
218
+ print('Warning ', list_name, 'Defined 2 times')
219
+ print('Use ', list_name, liste_dict[list_name])
220
+ continue
221
+
222
+ this_dict = {}
223
+
224
+ for i in list_value.split('/'):
225
+ name, items = [part.strip() for part in i.split(':', 1)]
226
+
227
+ if '*' in items:
228
+ start, end = [x.strip() for x in items.split('*', 1)]
229
+ startitems = re.split(r'(^[A-Z0-9_]*[A-Z_])([0-9]+$)', start)
230
+ enditems = re.split(r'(^[A-Z0-9_]*[A-Z_])([0-9]+$)', end)
231
+
232
+ if len(startitems) != len(enditems):
233
+ raise Exception(f'Range of lists wrong {startitems=} {enditems=}')
234
+
235
+ if len(startitems) == 4:
236
+ if startitems[1] != enditems[1]:
237
+ raise Exception(f'Range prefix mismatch {start=} {end=}')
238
+ label = startitems[1]
239
+ startint = int(startitems[2])
240
+ endint = int(enditems[2])
241
+ elif len(startitems) == 1:
242
+ label = ''
243
+ startint = int(startitems[0])
244
+ endint = int(enditems[0])
245
+ else:
246
+ raise Exception(f'wrong range in list: {startitems=} {enditems=}')
247
+
248
+ itemlist = [label + str(i) for i in range(startint, endint)]
249
+ else:
250
+ itemlist = [t for t in re.split(r'[\s,]+', items) if t]
251
+
252
+ this_dict[name] = itemlist
253
+
254
+ if not this_dict:
255
+ raise Exception(f'Empty list definition for {list_name}')
256
+
257
+ first_sublist_name = list(this_dict.keys())[0]
258
+ first_sublist = this_dict[first_sublist_name]
259
+ list_len = len(first_sublist)
260
+
261
+ for sublist, values in this_dict.items():
262
+ if len(values) != list_len:
263
+ raise Exception(
264
+ f'In {list_name} the length of sublist {sublist} is {len(values)} should be {list_len}'
265
+ )
266
+
267
+ if add_auto_sublists:
268
+ # Only inject auto-sublists that don't collide with explicitly
269
+ # user-defined sublist names. A user list like
270
+ # LIST AGES = AGES : AGE_0 * AGE_5 / AGES_END : 1 0 0 0 0 0
271
+ # already has its own AGES_END sublist; silently overwriting it
272
+ # would be a footgun. We warn (when not silent) and keep the
273
+ # user's value.
274
+ auto_sublists = [
275
+ (first_sublist_name + '_END', ['0'] * (list_len - 1) + ['1']),
276
+ (first_sublist_name + '_NOEND', ['1'] * (list_len - 1) + ['0']),
277
+ (first_sublist_name + '_START', ['1'] + ['0'] * (list_len - 1)),
278
+ (first_sublist_name + '_NOSTART', ['0'] + ['1'] * (list_len - 1)),
279
+ ]
280
+ if list_len >= 3:
281
+ auto_sublists.append(
282
+ (first_sublist_name + '_MIDDLE', ['0'] + ['1'] * (list_len - 2) + ['0'])
283
+ )
284
+ auto_sublists.extend([
285
+ (first_sublist_name + '_BEFORE', ['0'] + first_sublist[:-1]),
286
+ (first_sublist_name + '_AFTER', first_sublist[1:] + ['0']),
287
+ ])
288
+
289
+ for sub_name, sub_values in auto_sublists:
290
+ if sub_name in this_dict:
291
+ if not silent:
292
+ print(
293
+ f'Warning: auto-sublist {sub_name!r} in list '
294
+ f'{list_name!r} would overwrite a user-defined '
295
+ f'sublist; keeping user value.'
296
+ )
297
+ continue
298
+ this_dict[sub_name] = sub_values
299
+
300
+ liste_dict[list_name] = this_dict
301
+
302
+ return liste_dict
303
+
304
+ def rebuild_list(list_dict):
305
+ """
306
+ Rebuild one LIST statement from the result of:
307
+
308
+ list_extract(block, add_auto_sublists=False)
309
+ """
310
+ if len(list_dict) != 1:
311
+ raise Exception(f'Expected exactly one list, got {list(list_dict.keys())}')
312
+
313
+ list_name, sublist_dict = next(iter(list_dict.items()))
314
+
315
+ if not sublist_dict:
316
+ return f'LIST {list_name} = $'
317
+
318
+ subitems = list(sublist_dict.items())
319
+
320
+ namewidth = max(len(subname) for subname, _ in subitems)
321
+ nval = len(subitems[0][1])
322
+
323
+ colwidths = [
324
+ max(len(values[i]) for _, values in subitems)
325
+ for i in range(nval)
326
+ ]
327
+
328
+ def format_values(values):
329
+ return ' '.join(
330
+ f'{v:>{w}}' for v, w in zip(values, colwidths)
331
+ ).rstrip()
332
+
333
+ parts = [
334
+ f"{subname:<{namewidth}} : {format_values(values)}"
335
+ for subname, values in subitems
336
+ ]
337
+
338
+ prefix = f"LIST {list_name} = "
339
+ indent = " " * len(prefix)
340
+
341
+ if len(parts) == 1:
342
+ return prefix + parts[0] + " $"
343
+
344
+ lines = [prefix + parts[0] + " /"]
345
+ for i, part in enumerate(parts[1:], start=1):
346
+ ending = " $" if i == len(parts) - 1 else " /"
347
+ lines.append(indent + part + ending)
348
+
349
+ return "\n".join(lines)
350
+
351
+ # def check_syntax_model(equations,test=True):
352
+ # ''' cheks if equations have syntax errors by calling the python compile.parse '''
353
+ # import ast
354
+ # ok =True
355
+ # try:
356
+ # for frml in find_frml(equations):
357
+ # a, fr, n, udtryk = split_frml(frml)
358
+ # ast.parse(re.sub(r'\n','',re.sub(' ','',udtryk[:-1])))
359
+ # except SyntaxError:
360
+ # print('Syntax error in:',frml)
361
+ # ok =False
362
+ # return ok
363
+
364
+ def get_expressions(equations):
365
+ ''' returns a generator of expressions in a model with frml <> expression $ '''
366
+ expressions = (split_frml(f)[3][:-1].replace(r'\n','').replace(r' ','') for f in find_frml(equations))
367
+ return expressions
368
+
369
+ def check_syntax_model(equations):
370
+ mp.check_syntax(get_expressions(equations))
371
+ return True
372
+
373
+
374
+ def udtryk_parse(udtryk,funks=[]):
375
+ '''returns a list of terms from an expression ie: lhs=rhs $
376
+ or just an expression like x+b '''
377
+ #nterm = namedtuple('nterm', ['comment', 'number', 'op', 'var', 'lag'])
378
+ temp=re.sub(r'\s+', '', udtryk.upper()) # remove all blanks
379
+ xxx = udtrykre(funks=funks).findall(temp) # the compiled re pattern is importet from pattern
380
+ # her laver vi det til en named tuple
381
+ ibh = [nterm(t[0],t[1],t[2], '' if t[3] == '-0' or t[3]=='+0' else t[3]) for t in xxx]
382
+ # ibh = [nterm._make(t) for t in xxx] # Easier to remember by using named tupels .
383
+ return ibh
384
+
385
+ def kw_frml_name(frml_name0, kw,default=None):
386
+ ''' find keywords and associated value from string '<kw=xxx,res=kdkdk>' '''
387
+ out = None
388
+ # frml_name=frml_name0.replace(' ','')
389
+ frml_name=frml_name0 # we want to enable values with blanks
390
+ if '<' in frml_name:
391
+ j = frml_name.find('<') # where is the <
392
+ for s in frml_name[j + 1:-1].split(','):
393
+ keyvalue = s.split('=',1)
394
+ if keyvalue[0].upper().strip() == kw.upper():
395
+ if len(keyvalue) == 2:
396
+ out = keyvalue[1].strip()
397
+ else:
398
+ out = True
399
+ if type(out) == type(None) and type(default)!=type(None):
400
+ out=default
401
+ return out
402
+
403
+ def f1():
404
+ return 42
405
+
406
+ def f2():
407
+ return 103
408
+
409
+ if __name__ == '__main__' and 1 :
410
+ #%% nterm = namedtuple('nterm', ['number', 'op', 'var', 'lag'])
411
+ if 0:
412
+ model_parse('frml <> a= b+c( + 0)+3.444 $')
413
+ xx = model_parse('frml <> a+b+b+b=x(-1)+y(-33) $ frml <> a= b+c(-0)+3.444 $')
414
+ for ((frml, fr, n, udtryk), nt) in xx:
415
+ print(f'{udtryk=}')
416
+ for t in nt:
417
+ print(f'{t=} ')
418
+
419
+ print(*udtryk_parse('frml <> a+b+b+b=x(+0)+y(-33) + b+c(-0)/ 1e4 +3.444 $'),sep=' \n')
420
+
421
+ list_extract('list bankdic = bank : Danske , Nordea / danske : yes , no $')
422
+ list_extract('list bankdic = bank : Danske , Nordea $')
423
+ list_extract('list agedic = age : age0 * age101 $')
424
+ list_extract('list yeardic = year : 2023 * 2031 / lag1 : 2022 * 2030 $')
425
+ t = list_extract('LIST AGES = AGES : AGE_0 * AGE_5 / A0T17 : 1 1 1 1 0 $')
426
+ print(rebuild_list(t))
427
+ t2 = list_extract('LIST AGES = AGES : AGE_0 * AGE_5 / A0T17 : 1 1 1 1 0 $',add_auto_sublists=False)
428
+ print(rebuild_list(t2))