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.
- modelBLfunk.py +180 -0
- model_Excel.py +332 -0
- model_cvx.py +139 -0
- model_dynare.py +173 -0
- model_financial_stability.py +88 -0
- model_latex.py +497 -0
- model_latex_class.py +808 -0
- model_parquet_mixin.py +424 -0
- modelclass.py +9828 -0
- modelconstruct.py +1496 -0
- modelconstruct_estimation.py +2872 -0
- modeldash.py +265 -0
- modeldashboot.py +202 -0
- modeldashsidebar.py +456 -0
- modeldekom.py +651 -0
- modeldiff.py +561 -0
- modeldisplay.py +550 -0
- modelestimation.py +1776 -0
- modelestimator_new.py +2613 -0
- modelflowib-2.73.dist-info/METADATA +156 -0
- modelflowib-2.73.dist-info/RECORD +44 -0
- modelflowib-2.73.dist-info/WHEEL +5 -0
- modelflowib-2.73.dist-info/licenses/license.md +10 -0
- modelflowib-2.73.dist-info/top_level.txt +39 -0
- modelgrab.py +318 -0
- modelgrabgdx.py +584 -0
- modelgrabwf2.py +1107 -0
- modelhelp.py +543 -0
- modelhtml.py +606 -0
- modelinvert.py +250 -0
- modeljupyter.py +824 -0
- modeljupytermagic.py +813 -0
- modelmacrograb.py +98 -0
- modelmanipulation.py +1461 -0
- modelmf.py +349 -0
- modelnet.py +114 -0
- modelnewton.py +2178 -0
- modelnormalize.py +430 -0
- modelpattern.py +428 -0
- modelreport.py +2187 -0
- modeluserfunk.py +97 -0
- modelvis.py +1038 -0
- modelwidget.py +718 -0
- modelwidget_input.py +1933 -0
modelconstruct.py
ADDED
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on Thu Oct 16 09:43:04 2025
|
|
4
|
+
|
|
5
|
+
@author: ibhan
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# next generation
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field, fields
|
|
11
|
+
from typing import List, Optional, Any , Union
|
|
12
|
+
import re
|
|
13
|
+
from IPython.display import display, Math, Latex, Markdown , Image, SVG, display_svg,IFrame
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from pprint import pformat
|
|
17
|
+
import textwrap
|
|
18
|
+
|
|
19
|
+
from modelmanipulation import tofrml,dounloop, sumunroll
|
|
20
|
+
from modelpattern import find_statements,split_frml,find_frml,list_extract,udtryk_parse,kw_frml_name,commentchar,split_frml_reqopts,rebuild_list
|
|
21
|
+
from modelpattern import namepat, check_syntax_model
|
|
22
|
+
from modelhelp import debug_var
|
|
23
|
+
from model_latex_class import a_latex_model,a_latex_equation,defrack, depower,debrace,defunk
|
|
24
|
+
from modelreport import LatexRepo
|
|
25
|
+
|
|
26
|
+
class ModelSpecificationError(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def clean_expressions(original_statements: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Pipeline
|
|
33
|
+
--------
|
|
34
|
+
1. Uppercase the DSL.
|
|
35
|
+
2. Collapse LIST blocks to one line, preserving whether they ended with '$'.
|
|
36
|
+
3. Run tofrml().
|
|
37
|
+
4. Rebuild LIST blocks through:
|
|
38
|
+
list_extract(..., add_auto_sublists=False)
|
|
39
|
+
rebuild_list(...)
|
|
40
|
+
"""
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
stmt_start = re.compile(r'^\s*(LIST|TLIST|FRML|DO|ENDDO|DOABLE)\b')
|
|
44
|
+
|
|
45
|
+
def is_list_start(line: str) -> bool:
|
|
46
|
+
# Both LIST and TLIST start a list block. TLIST is the transposed
|
|
47
|
+
# form: the second "row" gives sublist names and values run down
|
|
48
|
+
# the columns. TLIST is converted to standard LIST during
|
|
49
|
+
# normalize_lists() so downstream code only ever sees LIST.
|
|
50
|
+
return bool(re.match(r'^\s*(LIST|TLIST)\b', line))
|
|
51
|
+
|
|
52
|
+
def is_tlist_start(line: str) -> bool:
|
|
53
|
+
return bool(re.match(r'^\s*TLIST\b', line))
|
|
54
|
+
|
|
55
|
+
def collect_list_block(lines, start_index):
|
|
56
|
+
"""
|
|
57
|
+
Collect one LIST block starting at start_index.
|
|
58
|
+
|
|
59
|
+
A block continues while the current line ends with '/'.
|
|
60
|
+
A block ends when the current line ends with '$', or when
|
|
61
|
+
the current line does not end with '/'.
|
|
62
|
+
"""
|
|
63
|
+
block_lines = [lines[start_index].rstrip()]
|
|
64
|
+
j = start_index + 1
|
|
65
|
+
|
|
66
|
+
while j < len(lines):
|
|
67
|
+
prev = block_lines[-1].rstrip()
|
|
68
|
+
|
|
69
|
+
if prev.endswith('$'):
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if prev.endswith('/'):
|
|
73
|
+
block_lines.append(lines[j].rstrip())
|
|
74
|
+
j += 1
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
return '\n'.join(block_lines), j
|
|
80
|
+
|
|
81
|
+
def _convert_tlist_to_list(flat_no_dollar: str) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Convert a flattened TLIST single-line form to standard LIST form.
|
|
84
|
+
|
|
85
|
+
Input (flat, no trailing '$'):
|
|
86
|
+
TLIST <name> = h1 h2 h3 / v11 v12 v13 / v21 v22 v23 / v31 v32 v33
|
|
87
|
+
|
|
88
|
+
Output:
|
|
89
|
+
LIST <name> = h1 : v11 v21 v31 / h2 : v12 v22 v32 / h3 : v13 v23 v33
|
|
90
|
+
|
|
91
|
+
The first '/'-separated chunk after '=' is the header row giving
|
|
92
|
+
sublist names. Each subsequent chunk is one tuple of values, one
|
|
93
|
+
value per sublist. Data therefore runs *down* the columns, hence
|
|
94
|
+
'transposed list'.
|
|
95
|
+
"""
|
|
96
|
+
# strip the leading TLIST keyword
|
|
97
|
+
m = re.match(r'^\s*TLIST\b\s*(.*)$', flat_no_dollar, flags=re.IGNORECASE)
|
|
98
|
+
if not m:
|
|
99
|
+
raise ModelSpecificationError(
|
|
100
|
+
f"Internal: _convert_tlist_to_list called on non-TLIST: {flat_no_dollar!r}"
|
|
101
|
+
)
|
|
102
|
+
body = m.group(1).strip()
|
|
103
|
+
|
|
104
|
+
if '=' not in body:
|
|
105
|
+
raise ModelSpecificationError(
|
|
106
|
+
f"TLIST is missing '=' between name and contents: {flat_no_dollar!r}"
|
|
107
|
+
)
|
|
108
|
+
list_name, rhs = body.split('=', 1)
|
|
109
|
+
list_name = list_name.strip()
|
|
110
|
+
if not list_name:
|
|
111
|
+
raise ModelSpecificationError(
|
|
112
|
+
f"TLIST has empty name: {flat_no_dollar!r}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# split rows on '/'
|
|
116
|
+
raw_rows = [r.strip() for r in rhs.split('/')]
|
|
117
|
+
raw_rows = [r for r in raw_rows if r != '']
|
|
118
|
+
if len(raw_rows) < 2:
|
|
119
|
+
raise ModelSpecificationError(
|
|
120
|
+
f"TLIST {list_name} needs a header row and at least one value row "
|
|
121
|
+
f"(rows separated by '/'); got: {flat_no_dollar!r}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def tokens(row: str):
|
|
125
|
+
# split on whitespace or commas, drop empties
|
|
126
|
+
return [t for t in re.split(r'[\s,]+', row.strip()) if t != '']
|
|
127
|
+
|
|
128
|
+
header = tokens(raw_rows[0])
|
|
129
|
+
value_rows = [tokens(r) for r in raw_rows[1:]]
|
|
130
|
+
|
|
131
|
+
if not header:
|
|
132
|
+
raise ModelSpecificationError(
|
|
133
|
+
f"TLIST {list_name} has empty header row"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
ncols = len(header)
|
|
137
|
+
for k, vr in enumerate(value_rows, 1):
|
|
138
|
+
if len(vr) != ncols:
|
|
139
|
+
raise ModelSpecificationError(
|
|
140
|
+
f"TLIST {list_name}: value row {k} has {len(vr)} entries "
|
|
141
|
+
f"but header has {ncols} ({header})"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# transpose: column j across all value rows becomes the j-th sublist
|
|
145
|
+
sublists = []
|
|
146
|
+
for j, sublist_name in enumerate(header):
|
|
147
|
+
col = [vr[j] for vr in value_rows]
|
|
148
|
+
sublists.append(f"{sublist_name} : {' '.join(col)}")
|
|
149
|
+
|
|
150
|
+
return f"LIST {list_name} = " + ' / '.join(sublists)
|
|
151
|
+
|
|
152
|
+
def normalize_list_block(block: str) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Flatten one LIST or TLIST block to a single line.
|
|
155
|
+
Preserve a trailing '$' if present in the original block.
|
|
156
|
+
|
|
157
|
+
TLIST blocks are transposed into the canonical LIST form here, so
|
|
158
|
+
every later stage (tofrml, list_extract, rebuild_list, ...) only
|
|
159
|
+
ever sees LIST.
|
|
160
|
+
"""
|
|
161
|
+
saw_dollar = bool(re.search(r'\$\s*$', block))
|
|
162
|
+
is_tlist = bool(re.match(r'^\s*TLIST\b', block))
|
|
163
|
+
|
|
164
|
+
flat = ' '.join(line.strip() for line in block.splitlines())
|
|
165
|
+
flat = re.sub(r'\s+', ' ', flat).strip()
|
|
166
|
+
|
|
167
|
+
# remove one trailing '$' and one trailing '/'
|
|
168
|
+
flat = re.sub(r'\s*\$\s*$', '', flat)
|
|
169
|
+
flat = re.sub(r'\s*/\s*$', '', flat)
|
|
170
|
+
|
|
171
|
+
if is_tlist:
|
|
172
|
+
flat = _convert_tlist_to_list(flat)
|
|
173
|
+
|
|
174
|
+
if saw_dollar:
|
|
175
|
+
flat = flat + ' $'
|
|
176
|
+
|
|
177
|
+
return flat
|
|
178
|
+
|
|
179
|
+
def normalize_lists(text: str) -> str:
|
|
180
|
+
lines = text.splitlines()
|
|
181
|
+
out = []
|
|
182
|
+
i = 0
|
|
183
|
+
|
|
184
|
+
while i < len(lines):
|
|
185
|
+
if not is_list_start(lines[i]):
|
|
186
|
+
out.append(lines[i])
|
|
187
|
+
i += 1
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
block, next_i = collect_list_block(lines, i)
|
|
191
|
+
out.append(normalize_list_block(block))
|
|
192
|
+
i = next_i
|
|
193
|
+
|
|
194
|
+
return '\n'.join(out)
|
|
195
|
+
|
|
196
|
+
def restore_list_block(block: str) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Rebuild one LIST block through list_extract() + rebuild_list().
|
|
199
|
+
"""
|
|
200
|
+
saw_dollar = bool(re.search(r'\$\s*$', block))
|
|
201
|
+
|
|
202
|
+
parse_block = block if saw_dollar else block.rstrip() + ' $'
|
|
203
|
+
list_dict = list_extract(parse_block, add_auto_sublists=False)
|
|
204
|
+
rebuilt = rebuild_list(list_dict)
|
|
205
|
+
|
|
206
|
+
if not saw_dollar:
|
|
207
|
+
rebuilt = re.sub(r'\s*\$\s*$', '', rebuilt)
|
|
208
|
+
|
|
209
|
+
return rebuilt
|
|
210
|
+
|
|
211
|
+
def restore_lists(text: str) -> str:
|
|
212
|
+
lines = text.splitlines()
|
|
213
|
+
out = []
|
|
214
|
+
i = 0
|
|
215
|
+
|
|
216
|
+
while i < len(lines):
|
|
217
|
+
if not is_list_start(lines[i]):
|
|
218
|
+
out.append(lines[i])
|
|
219
|
+
i += 1
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# after normalize_lists(), LIST blocks are normally single-line,
|
|
223
|
+
# but we keep block collection here for robustness
|
|
224
|
+
block_lines = [lines[i].rstrip()]
|
|
225
|
+
j = i + 1
|
|
226
|
+
|
|
227
|
+
while j < len(lines):
|
|
228
|
+
prev = block_lines[-1].rstrip()
|
|
229
|
+
|
|
230
|
+
if prev.endswith('$'):
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
if prev.endswith('/'):
|
|
234
|
+
block_lines.append(lines[j].rstrip())
|
|
235
|
+
j += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if stmt_start.match(lines[j]):
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
block = '\n'.join(block_lines)
|
|
244
|
+
out.append(restore_list_block(block))
|
|
245
|
+
i = j
|
|
246
|
+
|
|
247
|
+
return '\n'.join(out)
|
|
248
|
+
|
|
249
|
+
up = original_statements.upper()
|
|
250
|
+
flattened = normalize_lists(up)
|
|
251
|
+
frml_added = tofrml(flattened)
|
|
252
|
+
return restore_lists(frml_added)
|
|
253
|
+
|
|
254
|
+
def doable_unroll(in_equations,funks=[]):
|
|
255
|
+
''' expands all sum(list,'expression') in a model
|
|
256
|
+
returns a new model'''
|
|
257
|
+
import model_latex_class as ml
|
|
258
|
+
nymodel = []
|
|
259
|
+
equations = in_equations[:].upper() # we want do change the e
|
|
260
|
+
# debug_var(equations)
|
|
261
|
+
|
|
262
|
+
for comment, command, value in find_statements(equations):
|
|
263
|
+
# print('>>',comment,'<',command,'>',value)
|
|
264
|
+
# debug_var(comment,command,value)
|
|
265
|
+
if comment:
|
|
266
|
+
nymodel.append(comment)
|
|
267
|
+
else:
|
|
268
|
+
if command == 'DOABLE':
|
|
269
|
+
unrolled = ml.doable(value)
|
|
270
|
+
nymodel.append(unrolled + ' ')
|
|
271
|
+
# debug_var(unrolled)
|
|
272
|
+
|
|
273
|
+
else:
|
|
274
|
+
nymodel.append(command + ' ' + value)
|
|
275
|
+
equations = '\n'.join(nymodel)
|
|
276
|
+
# debug_var(equations)
|
|
277
|
+
return equations
|
|
278
|
+
|
|
279
|
+
# Simple extractor
|
|
280
|
+
def extract_model_from_markdown(md_text: str) -> str:
|
|
281
|
+
"""Extracts model lines (starting with '>') from Markdown."""
|
|
282
|
+
return "\n".join(
|
|
283
|
+
re.findall(r'^[>]\s?(.*)', md_text, flags=re.MULTILINE)
|
|
284
|
+
).strip()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
TAG_RE = re.compile(r'^%\s*@<([^>]+)>')
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def extract_latex_equation_block(lines, start):
|
|
292
|
+
block = []
|
|
293
|
+
i = start + 1
|
|
294
|
+
|
|
295
|
+
while i < len(lines) and r"\end{equation}" not in lines[i]:
|
|
296
|
+
block.append(lines[i])
|
|
297
|
+
i += 1
|
|
298
|
+
|
|
299
|
+
return block, i + 1
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def extract_model_from_markdown(md_text: str, list_position: str = "front") -> str:
|
|
304
|
+
"""
|
|
305
|
+
Extract ModelFlow model equations from markdown.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
md_text : str
|
|
310
|
+
Markdown text containing model equations and LaTeX.
|
|
311
|
+
list_position : {"front", "end"}
|
|
312
|
+
Where extracted LIST definitions should be placed.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
if list_position not in {"front", "end"}:
|
|
316
|
+
raise ValueError("list_position must be 'front' or 'end'")
|
|
317
|
+
|
|
318
|
+
model_lines = []
|
|
319
|
+
latex_buffer = [] # collect ALL LaTeX for LIST extraction
|
|
320
|
+
|
|
321
|
+
lines = md_text.splitlines()
|
|
322
|
+
current = ""
|
|
323
|
+
i = 0
|
|
324
|
+
n = len(lines)
|
|
325
|
+
|
|
326
|
+
in_dollars = False
|
|
327
|
+
|
|
328
|
+
while i < n:
|
|
329
|
+
line = lines[i]
|
|
330
|
+
stripped = line.lstrip()
|
|
331
|
+
|
|
332
|
+
# ----------------------------------------------------------
|
|
333
|
+
# Track $$ display math
|
|
334
|
+
# ----------------------------------------------------------
|
|
335
|
+
if stripped.startswith("$$"):
|
|
336
|
+
in_dollars = not in_dollars
|
|
337
|
+
latex_buffer.append(line)
|
|
338
|
+
i += 1
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
if in_dollars:
|
|
342
|
+
latex_buffer.append(line)
|
|
343
|
+
i += 1
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# ----------------------------------------------------------
|
|
347
|
+
# LaTeX equation environment (standalone)
|
|
348
|
+
# ----------------------------------------------------------
|
|
349
|
+
if stripped.startswith(r"\begin{equation}"):
|
|
350
|
+
block, i = extract_latex_equation_block(lines, i)
|
|
351
|
+
|
|
352
|
+
tag = None
|
|
353
|
+
label = None
|
|
354
|
+
body_lines = []
|
|
355
|
+
|
|
356
|
+
for l in block:
|
|
357
|
+
s = l.strip()
|
|
358
|
+
if not s:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
latex_buffer.append(s)
|
|
362
|
+
|
|
363
|
+
# % @<...>
|
|
364
|
+
if s.startswith('%'):
|
|
365
|
+
m = TAG_RE.match(s)
|
|
366
|
+
if m:
|
|
367
|
+
tag = m.group(1)
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# \label{eq:...}
|
|
371
|
+
if s.startswith(r"\label{eq:"):
|
|
372
|
+
label = s[len(r"\label{eq:"):-1]
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
body_lines.append(s)
|
|
376
|
+
|
|
377
|
+
if label and body_lines:
|
|
378
|
+
final_tag = f'<{tag}>' if tag else '<>'
|
|
379
|
+
body = " ".join(body_lines)
|
|
380
|
+
model_lines.append(
|
|
381
|
+
latex_to_doable(body, tag=final_tag)
|
|
382
|
+
)
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
# ----------------------------------------------------------
|
|
386
|
+
# Inline LaTeX (for LIST detection)
|
|
387
|
+
# ----------------------------------------------------------
|
|
388
|
+
if "$" in line:
|
|
389
|
+
latex_buffer.append(line)
|
|
390
|
+
|
|
391
|
+
# ----------------------------------------------------------
|
|
392
|
+
# Markdown continuation >>
|
|
393
|
+
# ----------------------------------------------------------
|
|
394
|
+
if stripped.startswith(">>"):
|
|
395
|
+
current += " " + stripped[2:].lstrip()
|
|
396
|
+
i += 1
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
# ----------------------------------------------------------
|
|
400
|
+
# Markdown model line >
|
|
401
|
+
# ----------------------------------------------------------
|
|
402
|
+
if stripped.startswith(">"):
|
|
403
|
+
if current.strip():
|
|
404
|
+
model_lines.append(current.strip())
|
|
405
|
+
current = stripped[1:].lstrip()
|
|
406
|
+
i += 1
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
# ----------------------------------------------------------
|
|
410
|
+
# Flush on non-model line
|
|
411
|
+
# ----------------------------------------------------------
|
|
412
|
+
if current.strip():
|
|
413
|
+
model_lines.append(current.strip())
|
|
414
|
+
current = ""
|
|
415
|
+
|
|
416
|
+
i += 1
|
|
417
|
+
|
|
418
|
+
# ----------------------------------------------------------
|
|
419
|
+
# Final flush
|
|
420
|
+
# ----------------------------------------------------------
|
|
421
|
+
if current.strip():
|
|
422
|
+
model_lines.append(current.strip())
|
|
423
|
+
|
|
424
|
+
# ----------------------------------------------------------
|
|
425
|
+
# GLOBAL LIST extraction (ONCE)
|
|
426
|
+
# ----------------------------------------------------------
|
|
427
|
+
lists = findlists_in_latex(md_text)
|
|
428
|
+
# debug_var(lists,model_lines)
|
|
429
|
+
if lists:
|
|
430
|
+
if list_position == "front":
|
|
431
|
+
model_lines = [lists] + model_lines
|
|
432
|
+
else: # "end"
|
|
433
|
+
model_lines = model_lines + [lists]
|
|
434
|
+
|
|
435
|
+
return "\n".join(model_lines)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# def findlists_in_latex(input):
|
|
440
|
+
# '''extracte list with sublist from latex'''
|
|
441
|
+
# relevant = re.findall(r'\$LIST\s*\\;\s*[^$]*\$',input.upper())
|
|
442
|
+
# # print(f'{relevant=}')
|
|
443
|
+
# temp1 = [l.replace('$','').replace('\\','')
|
|
444
|
+
# .replace(',',' ').replace(';',' ')
|
|
445
|
+
# .replace('{','').replace('}','').replace('\n','/ \n')
|
|
446
|
+
# for l in relevant]
|
|
447
|
+
# # print(f'\n{temp1=}\n')
|
|
448
|
+
# temp2 = ['LIST ' + l.split('=')[0][4:].strip() +' = '
|
|
449
|
+
# + l.split('=')[0][4:]
|
|
450
|
+
# +' : '+ l.split('=')[1] for l in temp1]
|
|
451
|
+
|
|
452
|
+
# return ('\n'.join(temp2)+'\n')
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
import re
|
|
456
|
+
|
|
457
|
+
import re
|
|
458
|
+
|
|
459
|
+
def clean_mathjax(s):
|
|
460
|
+
s = (
|
|
461
|
+
s.replace('$$\n','')
|
|
462
|
+
.replace('$','')
|
|
463
|
+
.replace('\\allowbreak',' ')
|
|
464
|
+
.replace('\\\\',' ')
|
|
465
|
+
.replace('\\begin{aligned}','')
|
|
466
|
+
.replace('\\end{aligned}','')
|
|
467
|
+
.replace(r'\{','')
|
|
468
|
+
.replace(r'\}','')
|
|
469
|
+
.replace(',',' ')
|
|
470
|
+
.replace(r'\;',' ')
|
|
471
|
+
.replace('\n','/ \n')
|
|
472
|
+
.replace(r'\_','_')
|
|
473
|
+
)
|
|
474
|
+
# remove line-start + blanks
|
|
475
|
+
return re.sub(r'(?m)^\s+', '', s)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def findlists_in_latex(input):
|
|
480
|
+
'''extract list with possible MathJax line breaks'''
|
|
481
|
+
|
|
482
|
+
relevant = re.findall(
|
|
483
|
+
r'\${1,2}\s*LIST\s*\\;\s*.*?\${1,2}',
|
|
484
|
+
input,
|
|
485
|
+
flags=re.IGNORECASE | re.DOTALL
|
|
486
|
+
)
|
|
487
|
+
temp0 = [l.upper() for l in relevant]
|
|
488
|
+
temp1 = [clean_mathjax(l.upper()) for l in relevant]
|
|
489
|
+
|
|
490
|
+
temp2 = [
|
|
491
|
+
'LIST ' + l.split('=')[0][4:].strip()
|
|
492
|
+
+ ' = ' + l.split('=')[0][4:]
|
|
493
|
+
+ ' : ' + l.split('=')[1]
|
|
494
|
+
for l in temp1
|
|
495
|
+
]
|
|
496
|
+
# debug_var(temp0,temp1,temp2)
|
|
497
|
+
result = '\n'.join(temp2) + '\n'
|
|
498
|
+
return result
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def latex_to_doable(temp,tag='<>'):
|
|
503
|
+
"""
|
|
504
|
+
Given a LaTeX equation string in `temp`, this function processes and converts it to a more standardized format.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
temp (str): A LaTeX equation string to be processed and standardized.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
str: A processed and standardized version of the input `temp` string.
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
None.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
if type(temp) == type(None):
|
|
517
|
+
return None
|
|
518
|
+
trans={r'\left':'',
|
|
519
|
+
r'\right':'',
|
|
520
|
+
# r'\min':'min',
|
|
521
|
+
# r'\max':'max',
|
|
522
|
+
r'\rho':'rho',
|
|
523
|
+
r'\alpha':'alpha',
|
|
524
|
+
r'\beta':'beta',
|
|
525
|
+
r'\tau':'tau',
|
|
526
|
+
r'\sigma':'sigma',
|
|
527
|
+
r'\exp':'exp',
|
|
528
|
+
r'&':'',
|
|
529
|
+
r'\\':'',
|
|
530
|
+
r'\nonumber' : '',
|
|
531
|
+
r'\_' : '_',
|
|
532
|
+
r'_{t}' : '',
|
|
533
|
+
r"_t(?![a-zA-Z0-9])" :'',
|
|
534
|
+
'logit^{-1}' : 'logit_inverse',
|
|
535
|
+
r'\{' : '{',
|
|
536
|
+
r'\}' : '}',
|
|
537
|
+
r'\begin{split}' : '',
|
|
538
|
+
'\n' :'',
|
|
539
|
+
r'\forall' :'',
|
|
540
|
+
r'\;' :'',
|
|
541
|
+
r'\:' :'',
|
|
542
|
+
r'\,' :'',
|
|
543
|
+
r'\big' :'',
|
|
544
|
+
r'\begin{aligned}' :'',
|
|
545
|
+
r'\end{aligned}' :'',
|
|
546
|
+
r' ' :' ',
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
}
|
|
550
|
+
ftrans = {
|
|
551
|
+
r'\sqrt':'sqrt',
|
|
552
|
+
r'\Delta':'diff',
|
|
553
|
+
r'\Phi':'NORM.CDF',
|
|
554
|
+
r'\Phi^{-1}':'NORM.PDF'
|
|
555
|
+
}
|
|
556
|
+
regtrans = {
|
|
557
|
+
r'\\Delta ([A-Za-z_][\w{},\^]*)':r'diff(\1)', # \Delta xy => diff(xy)
|
|
558
|
+
r'_{t-([1-9]+)}' : r'(-\1)', # _{t-x} => (-x)
|
|
559
|
+
r'_{t\+([1-9]+)}' : r'(+\1)', # _{t+x} => (+x)
|
|
560
|
+
|
|
561
|
+
# r'\^([\w])' : r'_\1', # ^x => _x
|
|
562
|
+
# r'\^\{([\w]+)\}(\w)' : r'_\1_\2', # ^{xx}y => _xx_y
|
|
563
|
+
r'\^{([\w+-]+)}' : r'__{\1}', # ^{xx} => _xx
|
|
564
|
+
r'\^{([\w+-]+),([\w+-]+)\}' : r'__{\1}__{\2}', # ^{xx,yy} => _xx_yy
|
|
565
|
+
r'\^{([\w+-]+),([\w+-]+),([\w+-]+)\}' : r'__{\1}__{\2}__{\3}', # ^{xx,yy} => _xx_yy
|
|
566
|
+
r'\^{([\w+-]+),([\w+-]+),([\w+-]+),([\w+-]+)\}' : r'__{\1}__{\2}__{\3}__{\4}', # ^{xx,yy} => _xx_yy
|
|
567
|
+
r'\s*\\times\s*':'*' ,
|
|
568
|
+
r'\s*\\cdot\s*':'*' ,
|
|
569
|
+
r'\\text{\[([\w+-,.]+)\]}' : r'[\1]',
|
|
570
|
+
r'\\sum_{('+namepat+r')}\(' : r'sum(\1,',
|
|
571
|
+
r"\\sum_{([a-zA-Z][a-zA-Z0-9_]*)=([a-zA-Z][a-zA-Z0-9_]*)}\(": r'sum(\1 \2=1,',
|
|
572
|
+
|
|
573
|
+
r'\\max_{('+namepat+r')}\(' : r'lmax(\1,',
|
|
574
|
+
r"\\max_{([a-zA-Z][a-zA-Z0-9_]*)=([a-zA-Z][a-zA-Z0-9_]*)}\(": r'lmax(\1 \2=1,',
|
|
575
|
+
r'\\min_{('+namepat+r')}\(' : r'lmin(\1,',
|
|
576
|
+
r"\\min_{([a-zA-Z][a-zA-Z0-9_]*)=([a-zA-Z][a-zA-Z0-9_]*)}\(": r'lmin(\1 \2=1,',
|
|
577
|
+
|
|
578
|
+
}
|
|
579
|
+
# breakpoint()
|
|
580
|
+
try:
|
|
581
|
+
for before,to in ftrans.items():
|
|
582
|
+
temp = defunk(before,to,temp)
|
|
583
|
+
except:
|
|
584
|
+
print(f'{before=} {to=} {temp=}')
|
|
585
|
+
# debug_var(temp)
|
|
586
|
+
|
|
587
|
+
for before,to in trans.items():
|
|
588
|
+
temp = temp.replace(before,to)
|
|
589
|
+
# debug_var(temp)
|
|
590
|
+
|
|
591
|
+
for before,to in regtrans.items():
|
|
592
|
+
temp = re.sub(before,to,temp)
|
|
593
|
+
# debug_var(temp)
|
|
594
|
+
temp = debrace(temp)
|
|
595
|
+
temp = defrack(temp)
|
|
596
|
+
temp = depower(temp)
|
|
597
|
+
temp = ' '.join(temp.split())
|
|
598
|
+
if '__' in temp:
|
|
599
|
+
res = f'doable {tag} {temp}'
|
|
600
|
+
else:
|
|
601
|
+
res = f'{tag} {temp}'
|
|
602
|
+
|
|
603
|
+
if '\\' in res:
|
|
604
|
+
raise ModelSpecificationError(
|
|
605
|
+
f"Some LaTeX has survived in the model (excerpt): {res[:200]}"
|
|
606
|
+
)
|
|
607
|
+
return res
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def apply_replacements(formulas: str, replacements: Union[tuple[str, str], list[tuple[str, str]]]) -> str:
|
|
612
|
+
"""
|
|
613
|
+
Apply one or more string replacements to a formula string.
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
formulas : str
|
|
618
|
+
The input string containing one or more formulas.
|
|
619
|
+
replacements : tuple[str, str] or list[tuple[str, str]]
|
|
620
|
+
Either a single (pattern, replacement) tuple or
|
|
621
|
+
a list of such tuples.
|
|
622
|
+
|
|
623
|
+
Example:
|
|
624
|
+
('__dim', '__{banks}__{country}__{ports}')
|
|
625
|
+
or:
|
|
626
|
+
[
|
|
627
|
+
('__dim', '__{banks}__{country}__{ports}'),
|
|
628
|
+
('GDP', 'GDP_REAL')
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
Returns
|
|
632
|
+
-------
|
|
633
|
+
str
|
|
634
|
+
The modified formula string after all replacements have been applied.
|
|
635
|
+
If no replacements are given or replacements is empty, the original
|
|
636
|
+
string is returned unchanged.
|
|
637
|
+
"""
|
|
638
|
+
if not replacements:
|
|
639
|
+
return formulas
|
|
640
|
+
|
|
641
|
+
# Ensure replacements is a list of tuples
|
|
642
|
+
if isinstance(replacements, tuple) and len(replacements) == 2 and isinstance(replacements[0], str):
|
|
643
|
+
replacements = [replacements]
|
|
644
|
+
|
|
645
|
+
updated = formulas
|
|
646
|
+
for old, new in replacements:
|
|
647
|
+
updated = updated.replace(old, new)
|
|
648
|
+
return updated
|
|
649
|
+
|
|
650
|
+
def mfmod_list_to_markdown(text: str) -> str:
|
|
651
|
+
"""
|
|
652
|
+
Convert MFMod-style list definitions (EViews/ModelFlow format) into
|
|
653
|
+
aligned Markdown tables, while keeping all other lines unchanged.
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
Input:
|
|
657
|
+
>list ages = ages : age_0 * age_11
|
|
658
|
+
>list sexes = sexes : female male /
|
|
659
|
+
> fertile : 1 0
|
|
660
|
+
other stuff
|
|
661
|
+
|
|
662
|
+
Output:
|
|
663
|
+
(Markdown tables for lists with blank line before each)
|
|
664
|
+
other stuff
|
|
665
|
+
|
|
666
|
+
Behavior:
|
|
667
|
+
- Detects lines starting with '>list' as list definitions.
|
|
668
|
+
- Parses sublists separated by '/' and items separated by whitespace.
|
|
669
|
+
- Renders Markdown tables with aligned columns and clear '|'.
|
|
670
|
+
- Leaves all non-list lines unchanged (including indentation).
|
|
671
|
+
- Adds one blank line before each rendered list.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
lines = text.splitlines()
|
|
675
|
+
output = []
|
|
676
|
+
|
|
677
|
+
# Temporary buffers for list parsing
|
|
678
|
+
current_list = None
|
|
679
|
+
current_entries = []
|
|
680
|
+
inside_list = False
|
|
681
|
+
raw_block = [] # keep original lines for fallback
|
|
682
|
+
|
|
683
|
+
def flush():
|
|
684
|
+
"""
|
|
685
|
+
Finalize the current list block:
|
|
686
|
+
- If successfully parsed → render as Markdown table.
|
|
687
|
+
- If invalid → output raw block unchanged.
|
|
688
|
+
- Always reset list state.
|
|
689
|
+
"""
|
|
690
|
+
nonlocal current_list, current_entries, inside_list, raw_block
|
|
691
|
+
if current_list and current_entries:
|
|
692
|
+
# Add a blank line before each table unless we’re at the top
|
|
693
|
+
if output and output[-1].strip():
|
|
694
|
+
output.append("")
|
|
695
|
+
output.append(render_list_table(current_list, current_entries))
|
|
696
|
+
output.append("") # 👈 ensures a blank line after the table
|
|
697
|
+
|
|
698
|
+
elif inside_list and raw_block:
|
|
699
|
+
# List start detected but not parsed → keep original text
|
|
700
|
+
if output and output[-1].strip():
|
|
701
|
+
output.append("")
|
|
702
|
+
output.extend(raw_block)
|
|
703
|
+
|
|
704
|
+
# Reset for next possible list
|
|
705
|
+
current_list, current_entries, inside_list, raw_block = None, [], False, []
|
|
706
|
+
|
|
707
|
+
# --- Main parsing loop ---
|
|
708
|
+
for line in lines:
|
|
709
|
+
stripped = line.strip()
|
|
710
|
+
|
|
711
|
+
# Case 1: Start of new list definition
|
|
712
|
+
if stripped.startswith(">list"):
|
|
713
|
+
flush() # finish any previous list
|
|
714
|
+
inside_list = True
|
|
715
|
+
raw_block = [line]
|
|
716
|
+
|
|
717
|
+
# Try to extract list name and right-hand content
|
|
718
|
+
match = re.match(r'>list\s+(\w+)\s*=\s*(.*)', stripped)
|
|
719
|
+
if match:
|
|
720
|
+
current_list, rest = match.groups()
|
|
721
|
+
current_entries.extend(parse_sublists(rest))
|
|
722
|
+
else:
|
|
723
|
+
# only name, content may follow later
|
|
724
|
+
current_list = stripped.replace('>list', '').strip()
|
|
725
|
+
|
|
726
|
+
# Case 2: Continuation lines (e.g., '> fertile : 1 0')
|
|
727
|
+
elif inside_list and stripped.startswith(">") and not stripped.startswith(">list"):
|
|
728
|
+
raw_block.append(line)
|
|
729
|
+
content = stripped[1:].strip()
|
|
730
|
+
current_entries.extend(parse_sublists(content))
|
|
731
|
+
|
|
732
|
+
# Case 3: A non-'>' line ends the list block
|
|
733
|
+
elif inside_list and not stripped.startswith(">"):
|
|
734
|
+
flush()
|
|
735
|
+
output.append(line)
|
|
736
|
+
|
|
737
|
+
# Case 4: Outside of any list
|
|
738
|
+
else:
|
|
739
|
+
if inside_list:
|
|
740
|
+
flush()
|
|
741
|
+
output.append(line)
|
|
742
|
+
|
|
743
|
+
# Handle any unfinished list at the end
|
|
744
|
+
flush()
|
|
745
|
+
|
|
746
|
+
return "\n".join(output)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def mfmod_list_to_codeblock(text: str) -> str:
|
|
750
|
+
"""
|
|
751
|
+
Wrap MFMod-style list definitions (lines starting with '>list' and following '>' lines)
|
|
752
|
+
inside triple backticks with 'text' language tag so they render as fixed-width blocks
|
|
753
|
+
in Jupyter Book. All non-list lines are left unchanged.
|
|
754
|
+
|
|
755
|
+
Example:
|
|
756
|
+
Input:
|
|
757
|
+
>list ages = ages : age_0 * age_11
|
|
758
|
+
>list sexes = sexes : female male /
|
|
759
|
+
> fertile : 1 0
|
|
760
|
+
other stuff
|
|
761
|
+
|
|
762
|
+
Output:
|
|
763
|
+
```text
|
|
764
|
+
>list ages = ages : age_0 * age_11
|
|
765
|
+
```
|
|
766
|
+
```text
|
|
767
|
+
>list sexes = sexes : female male /
|
|
768
|
+
> fertile : 1 0
|
|
769
|
+
```
|
|
770
|
+
other stuff
|
|
771
|
+
"""
|
|
772
|
+
lines = text.splitlines()
|
|
773
|
+
output = []
|
|
774
|
+
inside_block = False
|
|
775
|
+
|
|
776
|
+
for line in lines:
|
|
777
|
+
stripped = line.strip()
|
|
778
|
+
if stripped.startswith(">list"): # start of new block
|
|
779
|
+
if inside_block: # close previous
|
|
780
|
+
output.append("```")
|
|
781
|
+
inside_block = False
|
|
782
|
+
if output and output[-1].strip():
|
|
783
|
+
output.append("") # blank line before
|
|
784
|
+
output.append("```text")
|
|
785
|
+
output.append(line)
|
|
786
|
+
inside_block = True
|
|
787
|
+
elif inside_block and stripped.startswith(">"): # continuation
|
|
788
|
+
output.append(line)
|
|
789
|
+
elif inside_block: # end of block
|
|
790
|
+
output.append("```")
|
|
791
|
+
inside_block = False
|
|
792
|
+
output.append(line)
|
|
793
|
+
else:
|
|
794
|
+
output.append(line)
|
|
795
|
+
|
|
796
|
+
if inside_block: # close any open block
|
|
797
|
+
output.append("```")
|
|
798
|
+
|
|
799
|
+
return "\n".join(output)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def parse_sublists(text: str) -> list[tuple[str, list[str]]]:
|
|
803
|
+
"""
|
|
804
|
+
Parse a text fragment defining one or more sublists into a list of tuples.
|
|
805
|
+
|
|
806
|
+
Example:
|
|
807
|
+
'sexes : female male / fertile : 1 0'
|
|
808
|
+
→ [('sexes', ['female', 'male']), ('fertile', ['1', '0'])]
|
|
809
|
+
"""
|
|
810
|
+
result = []
|
|
811
|
+
for part in [p.strip() for p in text.split('/') if p.strip()]:
|
|
812
|
+
if ':' not in part:
|
|
813
|
+
continue
|
|
814
|
+
sub, vals = [p.strip() for p in part.split(':', 1)]
|
|
815
|
+
result.append((sub, vals.split()))
|
|
816
|
+
return result
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def render_list_table(list_name: str, entries: list[tuple[str, list[str]]]) -> str:
|
|
820
|
+
"""
|
|
821
|
+
Render one parsed list into a Markdown table, preserving structure.
|
|
822
|
+
|
|
823
|
+
Example:
|
|
824
|
+
[('ages', ['age_0', '*', 'age_11'])] →
|
|
825
|
+
> list
|
|
826
|
+
| **list name** | **sublist name** ||| |
|
|
827
|
+
|:------------------|:-------------------------------------------|-:|-:|-:|
|
|
828
|
+
| ages | ages | age_0 | * | age_11 |
|
|
829
|
+
"""
|
|
830
|
+
if not entries:
|
|
831
|
+
return ""
|
|
832
|
+
|
|
833
|
+
# Determine max number of value columns
|
|
834
|
+
max_cols = max(len(e[1]) for e in entries)
|
|
835
|
+
pipes = " |" * max_cols
|
|
836
|
+
|
|
837
|
+
# Header and alignment lines (exact Markdown structure)
|
|
838
|
+
header = f"\n| **list name** | **sublist name** {pipes} |\n"
|
|
839
|
+
header += (
|
|
840
|
+
f"|:------------------|:-------------------------------------------|"
|
|
841
|
+
+ "|".join(["---:" for _ in range(max_cols)])
|
|
842
|
+
+ "|\n"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# Align sublist names to the longest one for consistent look
|
|
846
|
+
max_sub_len = max(len(sub) for sub, _ in entries)
|
|
847
|
+
|
|
848
|
+
rows = []
|
|
849
|
+
first = True
|
|
850
|
+
for sublist, items in entries:
|
|
851
|
+
# Only show list name once
|
|
852
|
+
list_cell = list_name if first else ""
|
|
853
|
+
first = False
|
|
854
|
+
|
|
855
|
+
# Pad with blanks if some sublists have fewer entries
|
|
856
|
+
padded_items = items + [""] * (max_cols - len(items))
|
|
857
|
+
row = f"| {list_cell:<18}| {sublist:<{max_sub_len}} | " + " | ".join(padded_items) + " |"
|
|
858
|
+
rows.append(row)
|
|
859
|
+
|
|
860
|
+
return header + "\n".join(rows)
|
|
861
|
+
import re
|
|
862
|
+
|
|
863
|
+
def mfmod_list_to_codeblock(text: str) -> str:
|
|
864
|
+
"""
|
|
865
|
+
Wrap any consecutive lines starting with '>' (including '>list' definitions)
|
|
866
|
+
in fenced code blocks (```text) so they render in fixed-width font
|
|
867
|
+
in Jupyter Book. All other lines remain unchanged.
|
|
868
|
+
|
|
869
|
+
Example
|
|
870
|
+
-------
|
|
871
|
+
Input:
|
|
872
|
+
>list ages = ages : age_0 * age_11
|
|
873
|
+
>list sexes = sexes : female male /
|
|
874
|
+
> fertile : 1 0
|
|
875
|
+
other stuff
|
|
876
|
+
|
|
877
|
+
Output:
|
|
878
|
+
```text
|
|
879
|
+
>list ages = ages : age_0 * age_11
|
|
880
|
+
>list sexes = sexes : female male /
|
|
881
|
+
> fertile : 1 0
|
|
882
|
+
```
|
|
883
|
+
other stuff
|
|
884
|
+
"""
|
|
885
|
+
lines = text.splitlines()
|
|
886
|
+
output = []
|
|
887
|
+
inside_block = False
|
|
888
|
+
|
|
889
|
+
for line in lines:
|
|
890
|
+
stripped = line.strip()
|
|
891
|
+
|
|
892
|
+
# Lines starting with '>' belong to a code block
|
|
893
|
+
if stripped.startswith(">"):
|
|
894
|
+
if not inside_block:
|
|
895
|
+
# add a blank line before starting a block if previous line not blank
|
|
896
|
+
if output and output[-1].strip():
|
|
897
|
+
output.append("")
|
|
898
|
+
output.append("```text")
|
|
899
|
+
inside_block = True
|
|
900
|
+
output.append(line)
|
|
901
|
+
else:
|
|
902
|
+
# close any open block before non-'>' line
|
|
903
|
+
if inside_block:
|
|
904
|
+
output.append("```")
|
|
905
|
+
inside_block = False
|
|
906
|
+
output.append(line)
|
|
907
|
+
|
|
908
|
+
# close any unclosed block at end
|
|
909
|
+
if inside_block:
|
|
910
|
+
output.append("```")
|
|
911
|
+
|
|
912
|
+
return "\n".join(output)
|
|
913
|
+
|
|
914
|
+
import modelnormalize as nz
|
|
915
|
+
from functools import cached_property
|
|
916
|
+
|
|
917
|
+
def render_markdown_model(cell: str, style: str = "mixed") -> str:
|
|
918
|
+
"""
|
|
919
|
+
Prepare Markdown text for rendering.
|
|
920
|
+
|
|
921
|
+
Responsibilities:
|
|
922
|
+
- Protect model lines (starting with '>') by wrapping them in code fences
|
|
923
|
+
- Leave LaTeX blocks untouched
|
|
924
|
+
- Do NOT render anything
|
|
925
|
+
"""
|
|
926
|
+
|
|
927
|
+
if style == "plain":
|
|
928
|
+
return cell
|
|
929
|
+
|
|
930
|
+
lines = cell.splitlines()
|
|
931
|
+
out = []
|
|
932
|
+
|
|
933
|
+
in_code = False
|
|
934
|
+
in_fence = False
|
|
935
|
+
|
|
936
|
+
def start_code():
|
|
937
|
+
nonlocal in_code
|
|
938
|
+
if not in_code:
|
|
939
|
+
out.append("```text")
|
|
940
|
+
in_code = True
|
|
941
|
+
|
|
942
|
+
def end_code():
|
|
943
|
+
nonlocal in_code
|
|
944
|
+
if in_code:
|
|
945
|
+
out.append("```")
|
|
946
|
+
in_code = False
|
|
947
|
+
|
|
948
|
+
for line in lines:
|
|
949
|
+
stripped = line.lstrip()
|
|
950
|
+
|
|
951
|
+
# Preserve existing fenced blocks
|
|
952
|
+
if stripped.startswith("```"):
|
|
953
|
+
end_code()
|
|
954
|
+
out.append(stripped)
|
|
955
|
+
in_fence = not in_fence
|
|
956
|
+
continue
|
|
957
|
+
|
|
958
|
+
if in_fence:
|
|
959
|
+
out.append(line)
|
|
960
|
+
continue
|
|
961
|
+
|
|
962
|
+
# Model lines → code block
|
|
963
|
+
if stripped.startswith(">"):
|
|
964
|
+
start_code()
|
|
965
|
+
out.append(stripped)
|
|
966
|
+
continue
|
|
967
|
+
|
|
968
|
+
# Normal text
|
|
969
|
+
end_code()
|
|
970
|
+
out.append(line)
|
|
971
|
+
|
|
972
|
+
end_code()
|
|
973
|
+
# debug_var(out)
|
|
974
|
+
return "\n".join(out)
|
|
975
|
+
|
|
976
|
+
import re
|
|
977
|
+
from IPython.display import display, Markdown, Math
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
import re
|
|
981
|
+
from IPython.display import display, Markdown, Math
|
|
982
|
+
|
|
983
|
+
def display_model(cell: str, spec: str = "markdown"):
|
|
984
|
+
"""
|
|
985
|
+
Display a model specification.
|
|
986
|
+
|
|
987
|
+
spec = "markdown" | "latex"
|
|
988
|
+
"""
|
|
989
|
+
if spec == "markdown":
|
|
990
|
+
display_markdown_model(cell)
|
|
991
|
+
elif spec == "latex":
|
|
992
|
+
display_latex_model(cell)
|
|
993
|
+
else:
|
|
994
|
+
raise ValueError("spec must be 'markdown' or 'latex'")
|
|
995
|
+
|
|
996
|
+
def display_markdown_model(cell: str):
|
|
997
|
+
cell = render_markdown_model(cell)
|
|
998
|
+
display_mixed_markdown(cell)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def markdown_headings_to_latex(text: str) -> str:
|
|
1003
|
+
lines = []
|
|
1004
|
+
for line in text.splitlines():
|
|
1005
|
+
if line.startswith("### "):
|
|
1006
|
+
lines.append(r"\subsubsection{" + line[4:] + "}")
|
|
1007
|
+
elif line.startswith("## "):
|
|
1008
|
+
lines.append(r"\subsection{" + line[3:] + "}")
|
|
1009
|
+
elif line.startswith("# "):
|
|
1010
|
+
lines.append(r"\section{" + line[2:] + "}")
|
|
1011
|
+
else:
|
|
1012
|
+
lines.append(line)
|
|
1013
|
+
return "\n".join(lines)
|
|
1014
|
+
|
|
1015
|
+
def protect_model_code_latex(md: str) -> str:
|
|
1016
|
+
out = []
|
|
1017
|
+
in_code = False
|
|
1018
|
+
|
|
1019
|
+
for line in md.splitlines():
|
|
1020
|
+
if line.lstrip().startswith(">"):
|
|
1021
|
+
if not in_code:
|
|
1022
|
+
out.append(r"\begin{verbatim}")
|
|
1023
|
+
in_code = True
|
|
1024
|
+
out.append(line.lstrip()[1:])
|
|
1025
|
+
else:
|
|
1026
|
+
if in_code:
|
|
1027
|
+
out.append(r"\end{verbatim}")
|
|
1028
|
+
in_code = False
|
|
1029
|
+
out.append(line)
|
|
1030
|
+
|
|
1031
|
+
if in_code:
|
|
1032
|
+
out.append(r"\end{verbatim}")
|
|
1033
|
+
|
|
1034
|
+
return "\n".join(out)
|
|
1035
|
+
|
|
1036
|
+
from IPython.display import Latex, display
|
|
1037
|
+
|
|
1038
|
+
def display_latex_model(cell: str):
|
|
1039
|
+
# cell = protect_model_code_latex(cell)
|
|
1040
|
+
|
|
1041
|
+
display(Markdown(cell))
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def display_mixed_markdown(md: str):
|
|
1046
|
+
"""
|
|
1047
|
+
Display Markdown mixed with LaTeX safely in Jupyter.
|
|
1048
|
+
|
|
1049
|
+
Supports unlimited alternation of:
|
|
1050
|
+
- Markdown prose
|
|
1051
|
+
- LaTeX equation / align environments
|
|
1052
|
+
- $$ ... $$ blocks
|
|
1053
|
+
|
|
1054
|
+
Rendering rules:
|
|
1055
|
+
- Math() receives ONLY pure math (no environments, no labels, no tags)
|
|
1056
|
+
- Markdown() never sees LaTeX display environments
|
|
1057
|
+
"""
|
|
1058
|
+
|
|
1059
|
+
pattern = re.compile(
|
|
1060
|
+
r"\$\$(.*?)\$\$"
|
|
1061
|
+
r"|\\begin\{equation\}(.*?)\\end\{equation\}"
|
|
1062
|
+
r"|\\begin\{align\}(.*?)\\end\{align\}",
|
|
1063
|
+
flags=re.DOTALL,
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
pos = 0
|
|
1067
|
+
|
|
1068
|
+
def _clean_math_body(math: str) -> str:
|
|
1069
|
+
"""
|
|
1070
|
+
Remove non-math directives from LaTeX before MathJax rendering.
|
|
1071
|
+
"""
|
|
1072
|
+
LABEL_RE = re.compile(r"\\label\{[^}]*\}")
|
|
1073
|
+
TAG_RE = re.compile(r"%\s*@<[^>]*>")
|
|
1074
|
+
COMMENT_RE = re.compile(r"%.*?$", re.MULTILINE)
|
|
1075
|
+
|
|
1076
|
+
math = LABEL_RE.sub("", math)
|
|
1077
|
+
math = TAG_RE.sub("", math)
|
|
1078
|
+
math = COMMENT_RE.sub("", math)
|
|
1079
|
+
return math.strip()
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
for m in pattern.finditer(md):
|
|
1084
|
+
# ---- Markdown before math ----
|
|
1085
|
+
if m.start() > pos:
|
|
1086
|
+
text = md[pos:m.start()]
|
|
1087
|
+
if text.strip():
|
|
1088
|
+
display(Markdown(text))
|
|
1089
|
+
|
|
1090
|
+
# ---- Math block ----
|
|
1091
|
+
raw_math = next(g for g in m.groups() if g is not None)
|
|
1092
|
+
clean_math = _clean_math_body(raw_math)
|
|
1093
|
+
if clean_math:
|
|
1094
|
+
display(Math(clean_math))
|
|
1095
|
+
|
|
1096
|
+
pos = m.end()
|
|
1097
|
+
|
|
1098
|
+
# ---- Trailing Markdown ----
|
|
1099
|
+
rest = md[pos:]
|
|
1100
|
+
if rest.strip():
|
|
1101
|
+
display(Markdown(rest))
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
@dataclass
|
|
1106
|
+
class BaseExplode:
|
|
1107
|
+
"""Common parent for Mexplode and Lexplode."""
|
|
1108
|
+
original_statements : str = field(default="", metadata={"description": "Input expressions"})
|
|
1109
|
+
normal_frml : str = field(default="",init=False, metadata={"description": "Output normalized expressions"})
|
|
1110
|
+
markdown_model : str = field(default="",init=False, metadata={"description": "As markdown"})
|
|
1111
|
+
funks : List[Any] = field(default_factory=list, metadata={"description": "List of user specified functions to be used in model"})
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
@property
|
|
1116
|
+
def show(self):
|
|
1117
|
+
print (self.normal_frml.strip())
|
|
1118
|
+
|
|
1119
|
+
@property
|
|
1120
|
+
def draw(self):
|
|
1121
|
+
self.mmodel.drawmodel()
|
|
1122
|
+
|
|
1123
|
+
@cached_property
|
|
1124
|
+
def mmodel(self):
|
|
1125
|
+
from modelclass import model
|
|
1126
|
+
return model(self.normal_frml,funks=self.funks)
|
|
1127
|
+
|
|
1128
|
+
def __str__(self):
|
|
1129
|
+
return self.normal_frml.strip()
|
|
1130
|
+
|
|
1131
|
+
@property
|
|
1132
|
+
def render(self):
|
|
1133
|
+
# display(Markdown(self.markdown_model))
|
|
1134
|
+
|
|
1135
|
+
rendered_md = render_markdown_model(self.markdown_model)
|
|
1136
|
+
display_mixed_markdown(rendered_md)
|
|
1137
|
+
|
|
1138
|
+
@dataclass
|
|
1139
|
+
class Mexplode(BaseExplode):
|
|
1140
|
+
# original_statements : str = field(default="", metadata={"description": "Input expressions"})
|
|
1141
|
+
# normal_frml : str = field(default="", metadata={"description": "Output normalized expressions"})
|
|
1142
|
+
|
|
1143
|
+
normal_main : str = field(init=False, metadata={"description": "Normalized frmls"})
|
|
1144
|
+
normal_fit : str = field(init=False, metadata={"description": "Normalized frmls for fitted values"})
|
|
1145
|
+
normal_calc_add : str = field(init=False, metadata={"description": "Normalized frmls to calculate add factors"})
|
|
1146
|
+
replacements : Union[tuple[str, str], list[tuple[str, str]]] = field(default_factory=list, metadata={"description": "list of string tupels with string replacements"})
|
|
1147
|
+
list_defs : str = field(default="", metadata={"description": "Lists definitions"})
|
|
1148
|
+
modelname : str = field(default="", metadata={"description": "A optional name for this (sub) model"})
|
|
1149
|
+
|
|
1150
|
+
clean_frml_statements : str = field(init=False, metadata={"description": "With frml and nice lists"})
|
|
1151
|
+
post_doable : str = field(init=False, metadata={"description": "Expanded after doable"})
|
|
1152
|
+
post_do : str = field(init=False, metadata={"description": "Frmls after do expansion"})
|
|
1153
|
+
post_sum : str = field(init=False, metadata={"description": "Frmls after expanding sums"})
|
|
1154
|
+
expanded_frml : str = field(init=False, metadata={"description": "frmls after expanding "})
|
|
1155
|
+
normal_expressions : List[Any] = field(init=False, metadata={"description": "List of normal expressions"})
|
|
1156
|
+
list_specification : str = field(init=False, metadata={"description": "All list specifications in string "})
|
|
1157
|
+
modellist : str = field(init=False, metadata={"description": "The lists defined in string as a dictionary"})
|
|
1158
|
+
type_input : str = field(default= 'markdown' ,metadata={"description": "Originaal as type modelflow or markdown"})
|
|
1159
|
+
|
|
1160
|
+
def __post_init__(self):
|
|
1161
|
+
'''prepares a model from a model template.
|
|
1162
|
+
|
|
1163
|
+
Returns a expanded model which is ready to solve
|
|
1164
|
+
|
|
1165
|
+
Eksempel: model = udrul_model(MinModel.txt)'''
|
|
1166
|
+
|
|
1167
|
+
if self.type_input == 'markdown':
|
|
1168
|
+
extracted_frml = extract_model_from_markdown(self.list_defs+self.original_statements)
|
|
1169
|
+
self.markdown_model = (mfmod_list_to_codeblock(self.original_statements))
|
|
1170
|
+
|
|
1171
|
+
else:
|
|
1172
|
+
extracted_frml = self.list_defs+self.original_statements
|
|
1173
|
+
self.markdown_model = ''
|
|
1174
|
+
|
|
1175
|
+
self.clean_frml_statements = clean_expressions(
|
|
1176
|
+
apply_replacements(extracted_frml,self.replacements)
|
|
1177
|
+
).strip().upper()
|
|
1178
|
+
# debug_var(extract_model_from_markdown(self.original_statements))
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
self.post_doable = doable_unroll(self.clean_frml_statements ,self.funks)
|
|
1182
|
+
# assert 1==2
|
|
1183
|
+
self.post_do = dounloop(self.post_doable) # we unroll the do loops
|
|
1184
|
+
|
|
1185
|
+
self.modellist = list_extract(self.post_do)
|
|
1186
|
+
self.post_sum = sumunroll(self.post_do,listin=self.modellist) # then we unroll the sum
|
|
1187
|
+
|
|
1188
|
+
self.expanded_frml = self.post_sum
|
|
1189
|
+
self.expanded_frml_list = find_frml(self.expanded_frml)
|
|
1190
|
+
self.expanded_frml_split = [split_frml_reqopts(frml) for frml in self.expanded_frml_list]
|
|
1191
|
+
|
|
1192
|
+
# def normal(ind_o,the_endo='',add_add_factor=True,do_preprocess = True,add_suffix = '_A',endo_lhs = True, =False,make_fitted=False,eviews=''):
|
|
1193
|
+
self.normal = [(parts,
|
|
1194
|
+
nz.normal(parts.expression,
|
|
1195
|
+
add_add_factor= kw_frml_name(parts.frmlname, 'ADD') or kw_frml_name(parts.frmlname, 'STOC'),
|
|
1196
|
+
add_suffix = kw_frml_name(parts.frmlname, 'ADD_SUFFIX','_A'),
|
|
1197
|
+
make_fixable = kw_frml_name(parts.frmlname, 'EXO')or kw_frml_name(parts.frmlname, 'STOC'),
|
|
1198
|
+
make_fitted = kw_frml_name(parts.frmlname, 'FIT'),
|
|
1199
|
+
the_endo = kw_frml_name(parts.frmlname, 'ENDO'),
|
|
1200
|
+
endo_lhs = False if 'FALSE' == kw_frml_name(parts.frmlname, 'ENDO_LHS',default='1') else True,
|
|
1201
|
+
implicit = kw_frml_name(parts.frmlname, 'IMPLICIT')
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
for parts in self.expanded_frml_split]
|
|
1206
|
+
|
|
1207
|
+
self.normal_expressions = [n for p,n in self.normal ]
|
|
1208
|
+
|
|
1209
|
+
# udrullet = lagarray_unroll(udrullet,funks=funks )
|
|
1210
|
+
# udrullet = creatematrix(udrullet,listin=modellist)
|
|
1211
|
+
# udrullet = createarray(udrullet,listin=modellist)
|
|
1212
|
+
# udrullet = argunroll(udrullet,listin=modellist)
|
|
1213
|
+
self.normal_main = '\n'.join([f'FRML {parts.frmlname} {normal.normalized} $'
|
|
1214
|
+
for parts, normal in self.normal
|
|
1215
|
+
])
|
|
1216
|
+
self.normal_fit = '\n'.join([f'FRML <FIT> {normal.fitted } $ '
|
|
1217
|
+
for parts, normal in self.normal if len(normal.fitted)
|
|
1218
|
+
])
|
|
1219
|
+
self.normal_calc_add = '\n'.join([f'FRML <CALC_ADD_FACTOR> {normal.calc_add_factor } $ '
|
|
1220
|
+
for parts, normal in self.normal if len(normal.calc_add_factor)
|
|
1221
|
+
])
|
|
1222
|
+
|
|
1223
|
+
self.normal_frml = '\n'.join( [self.normal_main,self.normal_fit,self.normal_calc_add])
|
|
1224
|
+
|
|
1225
|
+
check_syntax_model(self.normal_frml)
|
|
1226
|
+
|
|
1227
|
+
self.list_specification = self.get_lists()
|
|
1228
|
+
return
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def get_lists (self) -> str:
|
|
1232
|
+
"""
|
|
1233
|
+
Extract all LIST statements (ending with $) from clean_frml_statements.
|
|
1234
|
+
Returns them as a single string, separated by newlines.
|
|
1235
|
+
"""
|
|
1236
|
+
if not getattr(self, "clean_frml_statements", ""):
|
|
1237
|
+
return ""
|
|
1238
|
+
|
|
1239
|
+
# split by '$' and strip spaces/newlines
|
|
1240
|
+
statements = [stmt.strip() + " $"
|
|
1241
|
+
for stmt in self.clean_frml_statements.split("$")
|
|
1242
|
+
if stmt.strip()]
|
|
1243
|
+
|
|
1244
|
+
# filter for LIST statements
|
|
1245
|
+
list_statements = [stmt for stmt in statements if stmt.upper().startswith("LIST ")]
|
|
1246
|
+
return "\n".join(list_statements)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
@property
|
|
1252
|
+
def showlists(self):
|
|
1253
|
+
print ( pformat(self.modellist, width=100, compact=False))
|
|
1254
|
+
|
|
1255
|
+
@property
|
|
1256
|
+
def showmodellist(self):
|
|
1257
|
+
print ( pformat(self.modellist, width=100, compact=False))
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def pdf(self,**kwargs):
|
|
1261
|
+
pre= r'''
|
|
1262
|
+
{\setlength{\parskip}{1em}
|
|
1263
|
+
\setlength{\parindent}{0pt}
|
|
1264
|
+
'''
|
|
1265
|
+
|
|
1266
|
+
return LatexRepo(fr'{pre} {self.original_statements} }}').pdf(**kwargs)
|
|
1267
|
+
|
|
1268
|
+
def __getattr__(self, attr):
|
|
1269
|
+
if attr.startswith("show"):
|
|
1270
|
+
prop = attr[4:].lower()
|
|
1271
|
+
field_defs = fields(self)
|
|
1272
|
+
fieldnames = [f.name for f in field_defs]
|
|
1273
|
+
|
|
1274
|
+
if prop in fieldnames:
|
|
1275
|
+
value = getattr(self, prop)
|
|
1276
|
+
if isinstance(value, list) and all(isinstance(v, nz.Normalized_frml) for v in value):
|
|
1277
|
+
print(f"\n--- {prop.upper()} ({len(value)} items) ---")
|
|
1278
|
+
for i, frml in enumerate(value, 1):
|
|
1279
|
+
print(f"\n[{i}]")
|
|
1280
|
+
print(frml) # uses Normalized_frml.__str__
|
|
1281
|
+
else:
|
|
1282
|
+
|
|
1283
|
+
print(f"{prop.capitalize()}: \n{value}")
|
|
1284
|
+
else:
|
|
1285
|
+
import inspect
|
|
1286
|
+
# --- guess varname only if property not found ---
|
|
1287
|
+
varname = self.__class__.__name__.lower()
|
|
1288
|
+
frame = inspect.currentframe()
|
|
1289
|
+
try:
|
|
1290
|
+
caller = frame.f_back if frame else None
|
|
1291
|
+
if caller:
|
|
1292
|
+
varname = next((k for k, v in caller.f_locals.items() if v is self), varname)
|
|
1293
|
+
finally:
|
|
1294
|
+
del frame
|
|
1295
|
+
try:
|
|
1296
|
+
del caller
|
|
1297
|
+
except NameError:
|
|
1298
|
+
pass
|
|
1299
|
+
# -------------------------------------------------
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
left_parts = [f"{varname}.show{f.name}" for f in field_defs]
|
|
1303
|
+
maxlen = max(len(lp) for lp in left_parts)
|
|
1304
|
+
options = "\n".join(
|
|
1305
|
+
f"{lp.ljust(maxlen)} # {f.metadata.get('description')}"
|
|
1306
|
+
if f.metadata.get("description")
|
|
1307
|
+
else lp
|
|
1308
|
+
for lp, f in zip(left_parts, field_defs)
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
print(f"No such property '{prop}'. Try one of:\n{options}")
|
|
1312
|
+
return None
|
|
1313
|
+
|
|
1314
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'")
|
|
1315
|
+
|
|
1316
|
+
def __add__ (self, other):
|
|
1317
|
+
if isinstance(other, Mexplode):
|
|
1318
|
+
return Lexplode(mexplodes=[self, other])
|
|
1319
|
+
elif isinstance(other, Lexplode):
|
|
1320
|
+
return Lexplode(mexplodes= [self] + other.mexplodes)
|
|
1321
|
+
else:
|
|
1322
|
+
return NotImplemented
|
|
1323
|
+
|
|
1324
|
+
def __radd__(self, other):
|
|
1325
|
+
# handle reversed order
|
|
1326
|
+
if other == 0:
|
|
1327
|
+
return self
|
|
1328
|
+
if isinstance(other, Mexplode):
|
|
1329
|
+
return Lexplode(mexplodes=[other, self])
|
|
1330
|
+
elif isinstance(other, Lexplode):
|
|
1331
|
+
return Lexplode(mexplodes=other.mexplodes + [self])
|
|
1332
|
+
else:
|
|
1333
|
+
return NotImplemented
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def __repr__(self) -> str:
|
|
1337
|
+
def fmt(value: Any) -> str:
|
|
1338
|
+
# Show multiline strings as real blocks (no quotes)
|
|
1339
|
+
if isinstance(value, str) and '\n' in value:
|
|
1340
|
+
return "\n" + textwrap.indent(value.rstrip(), " ")
|
|
1341
|
+
# Pretty containers
|
|
1342
|
+
if isinstance(value, (list, tuple, set, dict)):
|
|
1343
|
+
return pformat(value, width=100, compact=False)
|
|
1344
|
+
# defaultdict / custom objects get pformat fallback via repr-str mix:
|
|
1345
|
+
try:
|
|
1346
|
+
from collections import defaultdict
|
|
1347
|
+
if isinstance(value, defaultdict):
|
|
1348
|
+
return pformat(value, width=100, compact=False)
|
|
1349
|
+
except Exception:
|
|
1350
|
+
pass
|
|
1351
|
+
# Everything else
|
|
1352
|
+
return repr(value)
|
|
1353
|
+
|
|
1354
|
+
items = vars(self)
|
|
1355
|
+
w = max(len(k) for k in items) if items else 0
|
|
1356
|
+
lines = []
|
|
1357
|
+
for k, v in items.items():
|
|
1358
|
+
rendered = fmt(v)
|
|
1359
|
+
if rendered.startswith("\n"): # multiline block
|
|
1360
|
+
lines.append(f"{k:<{w}} :{rendered}")
|
|
1361
|
+
else:
|
|
1362
|
+
lines.append(f"{k:<{w}} : {rendered}")
|
|
1363
|
+
return f"{self.__class__.__name__}(\n " + "\n ".join(lines) + "\n)"
|
|
1364
|
+
|
|
1365
|
+
# -*- coding: utf-8 -*-
|
|
1366
|
+
"""
|
|
1367
|
+
Extension to modelconstruct.py
|
|
1368
|
+
Adds Lexplode dataclass and addition support for Mexplode and Lexplode.
|
|
1369
|
+
"""
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
@dataclass
|
|
1374
|
+
class Lexplode(BaseExplode):
|
|
1375
|
+
"""Container for multiple Mexplode instances.
|
|
1376
|
+
|
|
1377
|
+
Enables combination of several model explosions using + operations.
|
|
1378
|
+
"""
|
|
1379
|
+
|
|
1380
|
+
mexplodes: List['Mexplode'] = field(default_factory=list)
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
# def __post_init__(self):
|
|
1384
|
+
|
|
1385
|
+
# self.normal_frml = '\n'.join(m.normal_frml.strip() for m in self.mexplodes)
|
|
1386
|
+
|
|
1387
|
+
@cached_property
|
|
1388
|
+
def normal_frml(self) -> str:
|
|
1389
|
+
"""Compute and cache concatenated normal_frml lazily on first access."""
|
|
1390
|
+
return "\n".join(m.normal_frml.strip() for m in self.mexplodes)
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
def __add__(self, other):
|
|
1394
|
+
if isinstance(other, Mexplode):
|
|
1395
|
+
return Lexplode(mexplodes=self.mexplodes + [other])
|
|
1396
|
+
elif isinstance(other, Lexplode):
|
|
1397
|
+
return Lexplode(mexplodes=self.mexplodes + other.mexplodes)
|
|
1398
|
+
else:
|
|
1399
|
+
return NotImplemented
|
|
1400
|
+
|
|
1401
|
+
def __radd__(self, other):
|
|
1402
|
+
if other == 0:
|
|
1403
|
+
return self
|
|
1404
|
+
if isinstance(other, Mexplode):
|
|
1405
|
+
return Lexplode(mexplodes=[other] + self.mexplodes)
|
|
1406
|
+
elif isinstance(other, Lexplode):
|
|
1407
|
+
return Lexplode(mexplodes=other.mexplodes + self.mexplodes)
|
|
1408
|
+
else:
|
|
1409
|
+
return NotImplemented
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def __repr__(self):
|
|
1413
|
+
return f"Lexplode(mexplodes={self.mexplodes!r})"
|
|
1414
|
+
|
|
1415
|
+
# __str__(self):
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
if __name__ == '__main__' :
|
|
1421
|
+
#%%
|
|
1422
|
+
pass
|
|
1423
|
+
if 0:
|
|
1424
|
+
res3 = Mexplode('''
|
|
1425
|
+
<endo=f,stoc> a = gamma+ f+O
|
|
1426
|
+
<endo=f> a = gamma+ f+O
|
|
1427
|
+
<endo=x,stoc,endo_lhs> a+x = gamma+ f+O
|
|
1428
|
+
<endo=x,stoc> a+x = gamma+ f+O
|
|
1429
|
+
<fit,exo,stoc> c = b*42
|
|
1430
|
+
|
|
1431
|
+
''',replacements=None)
|
|
1432
|
+
|
|
1433
|
+
res3.shownormal_expressions
|
|
1434
|
+
tlists = '''
|
|
1435
|
+
LIST BANKS = BANKS : IB SOREN MARIE /
|
|
1436
|
+
COUNTRY : DENMARK SWEDEN DENMARK /
|
|
1437
|
+
SELECTED : 1 0 1
|
|
1438
|
+
$
|
|
1439
|
+
LIST SECTORS = SECTORS : NFC SME HH $
|
|
1440
|
+
|
|
1441
|
+
'''
|
|
1442
|
+
|
|
1443
|
+
xx = '''
|
|
1444
|
+
doable <HEST,sum=abe> [banks=country=sweden, sektors=sektors] LOSS__{BANKS}__{SECTORs} = HOLDING__{BANKS}__{SECTORs} * PD__{BANKS}__{SECTORs}$
|
|
1445
|
+
doable <HEST,sum=goat> [banks=country=denmark,sektors=sektors] LOSS2__{BANKS}__{SECTORs} = HOLDING__{BANKS}__{SECTORs} * PD__{BANKS}__{SECTORs}$
|
|
1446
|
+
do sectors $
|
|
1447
|
+
frml <> x_{sectors} = 42 $
|
|
1448
|
+
enddo $
|
|
1449
|
+
|
|
1450
|
+
'''.upper()
|
|
1451
|
+
# xx = 'doabel <HEST,sum=abe> LOSS__{BANKS}__{SECTORs} =HOLDING__{BANKS}__{SECTORs} * PD__{BANKS}__{SECTORs} $'.upper()
|
|
1452
|
+
|
|
1453
|
+
# res = Mexplode(tlists+xx)
|
|
1454
|
+
# print(res)
|
|
1455
|
+
# breakpoint()
|
|
1456
|
+
res2 = Mexplode('''
|
|
1457
|
+
LIST BANKS = BANKS : IB SOREN MARIE /
|
|
1458
|
+
COUNTRY : DENMARK SWEDEN DENMARK /
|
|
1459
|
+
SELECTED : 1 1 0
|
|
1460
|
+
LIST SECTORS = SECTORS : NFC SME HH
|
|
1461
|
+
LIST test = t : 100*104
|
|
1462
|
+
a = b
|
|
1463
|
+
c = 33
|
|
1464
|
+
£ test
|
|
1465
|
+
doable <HEST,sum=goat> [banks country=denmark] LOSS2__{BANKS}__{SECTORs} = HOLDING__{BANKS}__{SECTORs} * PD__{BANKS}__{SECTORs}
|
|
1466
|
+
do banks
|
|
1467
|
+
£ sector {country}
|
|
1468
|
+
frml <> x_{banks} = 42
|
|
1469
|
+
enddo
|
|
1470
|
+
|
|
1471
|
+
''')
|
|
1472
|
+
|
|
1473
|
+
print(res2+res3)
|
|
1474
|
+
|
|
1475
|
+
if 0:
|
|
1476
|
+
test1= Mexplode('a=1')
|
|
1477
|
+
test2= Mexplode('b=2')
|
|
1478
|
+
test3= Mexplode('c=3')
|
|
1479
|
+
print(test1+(test2+test3))
|
|
1480
|
+
print(test1)
|
|
1481
|
+
|
|
1482
|
+
text = """>list ages = ages : age_0 * age_11
|
|
1483
|
+
>list sexes = sexes : female male /
|
|
1484
|
+
> fertile : 1 0
|
|
1485
|
+
other stuf
|
|
1486
|
+
and yes
|
|
1487
|
+
> dekdkd"""
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
print(text2:=mfmod_list_to_markdown(text))
|
|
1492
|
+
|
|
1493
|
+
#%% latex
|
|
1494
|
+
leq = r'CRF^{l,es,d}=\frac{DiscountRate^{l,es,d}}{1 - \left(1 + DiscountRate^{l,es,d}\right)^{-LifeSpan^{l,es,d}}}'
|
|
1495
|
+
meq = latex_to_doable(leq, '<ii>')
|
|
1496
|
+
print(meq)
|