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
modeljupytermagic.py
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
'''
|
|
4
|
+
This module defines several magic jupyter functions:
|
|
5
|
+
|
|
6
|
+
:graphviz: Draw Graphviz graph
|
|
7
|
+
:dataframe: Create Pandas Dataframe
|
|
8
|
+
:latexflow: Create a modelflow modelinstance from latex script
|
|
9
|
+
|
|
10
|
+
he doc strings can only be displayed when the module is used from Jupyter. So they are not shown in the Sphinx documentation To display the doc strings use the functions in jupyter.
|
|
11
|
+
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
from IPython.display import display, Math, Latex, Markdown , Image, SVG, display_svg,IFrame
|
|
15
|
+
from IPython.lib.latextools import latex_to_png
|
|
16
|
+
from io import StringIO
|
|
17
|
+
import pandas as pd
|
|
18
|
+
from IPython.core.magic import register_line_magic, register_cell_magic
|
|
19
|
+
from subprocess import run
|
|
20
|
+
import re
|
|
21
|
+
import ast
|
|
22
|
+
import shlex
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from model_latex import latextotxt
|
|
26
|
+
from modelclass import model
|
|
27
|
+
from modelmanipulation import explode
|
|
28
|
+
from model_latex_class import a_latex_model
|
|
29
|
+
from modelconstruct_estimation import Makemodel, display_model
|
|
30
|
+
from modelhelp import debug_var
|
|
31
|
+
from modelreport import LatexRepo
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_options(line,defaultname = 'test'):
|
|
35
|
+
'''
|
|
36
|
+
Retrives options from the first line.
|
|
37
|
+
|
|
38
|
+
The line is tokenized with :mod:`shlex` (POSIX mode), so values that
|
|
39
|
+
contain whitespace or shell-special characters can be passed by quoting
|
|
40
|
+
them. Examples::
|
|
41
|
+
|
|
42
|
+
%%Makemymodel npl smpl="(2012, 2019)"
|
|
43
|
+
%%Makemymodel npl caption="My model"
|
|
44
|
+
|
|
45
|
+
Unquoted whitespace still separates tokens, exactly as before, so
|
|
46
|
+
existing usage like ``smpl=(2012,2019)`` (no inner space) keeps working.
|
|
47
|
+
A value may itself contain ``=`` — only the first ``=`` is treated as
|
|
48
|
+
the key/value separator.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
line (str): the magic line (everything after the magic name).
|
|
52
|
+
defaultname (str, optional): fallback name when the line is empty.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
name (str): the model/object name (first token).
|
|
56
|
+
opt (dict): parsed options. Bare flags map to ``True``;
|
|
57
|
+
``key=0`` and ``key=False`` map to ``False``.
|
|
58
|
+
'''
|
|
59
|
+
if line:
|
|
60
|
+
arglistfull = shlex.split(line, posix=True)
|
|
61
|
+
name= arglistfull[0]
|
|
62
|
+
arglist=arglistfull[1:] if len(arglistfull)>= 1 else []
|
|
63
|
+
else:
|
|
64
|
+
name = defaultname
|
|
65
|
+
arglist=[]
|
|
66
|
+
|
|
67
|
+
opt = {}
|
|
68
|
+
for o in arglist:
|
|
69
|
+
parts = o.split('=', 1)
|
|
70
|
+
opt[parts[0]] = parts[1] if len(parts) == 2 else True
|
|
71
|
+
opt = {o : False if ( v== '0' or v=='False') else v for o,v in opt.items()}
|
|
72
|
+
|
|
73
|
+
return name,opt
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_option(options, opt_name, user_ns, default=None):
|
|
77
|
+
"""Resolve a magic-line option that may be a literal or a name in user_ns.
|
|
78
|
+
|
|
79
|
+
Tries ``ast.literal_eval`` first so options like ``replacements=[('a','b')]``
|
|
80
|
+
work directly on the line. Falls back to looking up the raw string in the
|
|
81
|
+
notebook namespace, so users can pass a Python object by name:
|
|
82
|
+
|
|
83
|
+
df = pd.read_csv(...)
|
|
84
|
+
%%Makemymodel mymodel input_df=df
|
|
85
|
+
|
|
86
|
+
Returns ``default`` if the option is missing, unparseable, and not a name
|
|
87
|
+
in ``user_ns``. A warning is printed in the bad-name case so silent typos
|
|
88
|
+
don't go unnoticed.
|
|
89
|
+
"""
|
|
90
|
+
if opt_name not in options:
|
|
91
|
+
return default
|
|
92
|
+
raw = options[opt_name]
|
|
93
|
+
# Bare flags (e.g. ``segment`` with no =value) come through as True;
|
|
94
|
+
# handle them before ast.literal_eval which would reject them.
|
|
95
|
+
if raw is True or raw is False:
|
|
96
|
+
return raw
|
|
97
|
+
try:
|
|
98
|
+
return ast.literal_eval(raw)
|
|
99
|
+
except (ValueError, SyntaxError):
|
|
100
|
+
if raw in user_ns:
|
|
101
|
+
return user_ns[raw]
|
|
102
|
+
print(f"⚠️ Warning: {opt_name}={raw!r} not found in namespace and not a literal.")
|
|
103
|
+
return default
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
|
|
108
|
+
from IPython.core.magic import register_cell_magic, register_line_magic
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@register_cell_magic
|
|
113
|
+
def graphviz(line, cell):
|
|
114
|
+
'''Creates a ModelFlow model from a Latex document'''
|
|
115
|
+
name,options = get_options(line,'Testgraph')
|
|
116
|
+
gv = cell
|
|
117
|
+
# print(options)
|
|
118
|
+
model.display_graph(gv,name,**options)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# In[29]:
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@register_cell_magic
|
|
125
|
+
def latexflow(line, cell):
|
|
126
|
+
'''Creates a ModelFlow model from a Latex document'''
|
|
127
|
+
name,options = get_options(line)
|
|
128
|
+
|
|
129
|
+
lmodel = cell
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# display(Markdown(f'## Now creating the model **{name}**'))
|
|
133
|
+
|
|
134
|
+
fmodel = latextotxt(cell)
|
|
135
|
+
if options.get('debug',False):
|
|
136
|
+
display(Markdown('## Creating this Template model'))
|
|
137
|
+
print(fmodel)
|
|
138
|
+
mmodel = model.from_eq(fmodel)
|
|
139
|
+
mmodel.equations_latex = cell
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
globals()[f'{name}'] = mmodel
|
|
143
|
+
|
|
144
|
+
ia = get_ipython()
|
|
145
|
+
ia.push(f'{name}',interactive=True)
|
|
146
|
+
if options.get('render',True):
|
|
147
|
+
display(Markdown(cell))
|
|
148
|
+
|
|
149
|
+
# display(Markdown('## The model'))
|
|
150
|
+
if options.get('display',False):
|
|
151
|
+
display(Markdown(cell))
|
|
152
|
+
display(Markdown('## Creating this Template model'))
|
|
153
|
+
print(mmodel.equations_original)
|
|
154
|
+
display(Markdown('## And this Business Logic Language model'))
|
|
155
|
+
print(mmodel.equations)
|
|
156
|
+
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
@register_cell_magic
|
|
160
|
+
def latexmodelgrab(line, cell):
|
|
161
|
+
'''Creates a ModelFlow model from a Latex document'''
|
|
162
|
+
name,options = get_options(line)
|
|
163
|
+
ia = get_ipython()
|
|
164
|
+
|
|
165
|
+
# print(f'{options=}')
|
|
166
|
+
if options.get('segment',False):
|
|
167
|
+
|
|
168
|
+
if f'{name}_dict' not in globals():
|
|
169
|
+
globals()[f'{name}_dict'] = {}
|
|
170
|
+
ia.push(f'{name}_dict',interactive=True)
|
|
171
|
+
|
|
172
|
+
modelsegment= options.get('segment','rest')
|
|
173
|
+
globals()[f'{name}_dict'][modelsegment]=cell
|
|
174
|
+
|
|
175
|
+
if modelsegment.startswith('list'):
|
|
176
|
+
display(Markdown(cell))
|
|
177
|
+
return
|
|
178
|
+
if modelsegment.startswith('text'):
|
|
179
|
+
display(Markdown(cell))
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# we want this cell plus all list cells to make this small model
|
|
183
|
+
if options.get('all',False):
|
|
184
|
+
model_text = '\n'.join([text for name,text in globals()[f'{name}_dict'].items()] )
|
|
185
|
+
|
|
186
|
+
else:
|
|
187
|
+
model_text = '\n'.join([text for name,text in globals()[f'{name}_dict'].items()
|
|
188
|
+
if name.startswith('list')])+cell
|
|
189
|
+
else:
|
|
190
|
+
if f'{name}_dict' in globals():
|
|
191
|
+
temp='\n'
|
|
192
|
+
model_text = '\n'.join([text for name,text in globals()[f'{name}_dict'].items()] )
|
|
193
|
+
else:
|
|
194
|
+
model_text = cell
|
|
195
|
+
|
|
196
|
+
model_text = r'''
|
|
197
|
+
\documentclass{article}
|
|
198
|
+
|
|
199
|
+
% Add necessary packages
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
\usepackage{amsmath} % For mathematical equations
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
\begin{document}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
''' + model_text + r'''
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
\end{document}'''
|
|
213
|
+
replace = [('##','title'),('###','section')]
|
|
214
|
+
for pat,rep in replace:
|
|
215
|
+
pattern = fr'^{pat} (.*?)\s*$'
|
|
216
|
+
replacement = fr'\\{rep}{{\1}}'+'\n'
|
|
217
|
+
|
|
218
|
+
model_text = re.sub(pattern, replacement, model_text, flags=re.MULTILINE)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
latex_model = a_latex_model(model_text,modelname=name)
|
|
224
|
+
mmodel = latex_model.mmodel
|
|
225
|
+
mmodel.equations_latex = model_text
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
globals()[f'{name}'] = mmodel
|
|
229
|
+
globals()[f'{name}_latex_model_instance'] = latex_model
|
|
230
|
+
|
|
231
|
+
ia.push(f'{name}',interactive=True)
|
|
232
|
+
ia.push(f'{name}_latex_model_instance',interactive=True)
|
|
233
|
+
|
|
234
|
+
if options.get('render',True) and not options.get('display',False):
|
|
235
|
+
display(Markdown(cell))
|
|
236
|
+
|
|
237
|
+
# display(Markdown('## The model'))
|
|
238
|
+
if options.get('display',False):
|
|
239
|
+
display(Markdown(cell))
|
|
240
|
+
try:
|
|
241
|
+
print(f'Model:{name} is created from these segments:\n'+
|
|
242
|
+
f"{temp.join([s for s in globals()[f'{name}_dict'].keys()])} \n")
|
|
243
|
+
except:
|
|
244
|
+
...
|
|
245
|
+
display(Markdown('## Creating this Template model'))
|
|
246
|
+
print(mmodel.equations_original)
|
|
247
|
+
display(Markdown('## And this Business Logic Language model'))
|
|
248
|
+
print(mmodel.equations)
|
|
249
|
+
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def ibmelt(df,prefix='',per=3):
|
|
258
|
+
# breakpoint()
|
|
259
|
+
temp= df.reset_index().rename(columns={'index':'row'}).melt(id_vars='row',var_name='column').assign(var_name=lambda x: prefix+x.row+'_'+x.column) .loc[:,['value','var_name']].set_index('var_name')
|
|
260
|
+
newdf = pd.concat([temp]*per,axis=1).T
|
|
261
|
+
newdf.index = range(per)
|
|
262
|
+
newdf.index.name = 'Year'
|
|
263
|
+
return newdf
|
|
264
|
+
|
|
265
|
+
# @register_cell_magic
|
|
266
|
+
# def dataframe(line, cell):
|
|
267
|
+
# '''Converts this cell to a dataframe. and create a melted dataframe
|
|
268
|
+
|
|
269
|
+
# options can be added to the inpput line\:
|
|
270
|
+
|
|
271
|
+
# - t transposes the dataframe
|
|
272
|
+
# - periods=<number> repeats the melted datframe
|
|
273
|
+
# - repeat = <number> repeats the original dataframe - for parameters
|
|
274
|
+
# - melt will create a melted dataframe
|
|
275
|
+
# - prefix=<a string> prefix columns in the melted dataframe
|
|
276
|
+
# - show will show the resulting dataframes
|
|
277
|
+
# - start=index will set the index (default 2021)
|
|
278
|
+
# '''
|
|
279
|
+
|
|
280
|
+
# name,options = get_options(line,'Testgraph')
|
|
281
|
+
|
|
282
|
+
# trans = options.get('t',False )
|
|
283
|
+
|
|
284
|
+
# if (options.get('help',False )):
|
|
285
|
+
# print(__doc__)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# prefix = options.get('prefix','')
|
|
289
|
+
# periods = int(options.get('periods','1'))
|
|
290
|
+
# melt = options.get('melt',False)
|
|
291
|
+
# start = int(options.get('start','2021'))
|
|
292
|
+
# silent = options.get('silent',True)
|
|
293
|
+
# ia = get_ipython()
|
|
294
|
+
|
|
295
|
+
# xtrans = (lambda xx:xx.T) if trans else (lambda xx:xx)
|
|
296
|
+
# xcell= cell.replace('%','').replace(',','.')
|
|
297
|
+
# mul = 0.01 if '%' in cell else 1.
|
|
298
|
+
# sio = StringIO(xcell)
|
|
299
|
+
|
|
300
|
+
# df = pd.read_csv(sio,sep=r"\s+|\t+|\s+\t+|\t+\s+",engine='python')\
|
|
301
|
+
# .pipe(xtrans)\
|
|
302
|
+
# .pipe(lambda xx:xx.rename(index = {i:i.upper() if type(i) == str else i for i in xx.index}
|
|
303
|
+
# ,columns={c:c.upper() for c in xx.columns})) *mul
|
|
304
|
+
# # breakpoint()
|
|
305
|
+
# if not melt:
|
|
306
|
+
# df= pd.concat([df]*periods,axis=0)
|
|
307
|
+
# df.index = pd.period_range(start=start,freq = 'Y',periods=len(df))
|
|
308
|
+
# df.index.name = 'index'
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# globals()[f'{name}'] = df
|
|
312
|
+
# ia.push(f'{name}',interactive=True)
|
|
313
|
+
# if melt:
|
|
314
|
+
# df_melted = ibmelt(df,prefix=prefix.upper(),per=periods)
|
|
315
|
+
# df_melted.index = pd.period_range(start=start,freq = 'Y',periods=len(df_melted))
|
|
316
|
+
# df_melted.index.name = 'index'
|
|
317
|
+
|
|
318
|
+
# globals()[f'{name}_melted'] = df_melted
|
|
319
|
+
# ia.push(f'{name}_melted',interactive=True)
|
|
320
|
+
# if not silent:
|
|
321
|
+
# melttext = f' and {name}_melted' if melt else ''
|
|
322
|
+
# display(Markdown(f'## Created the dataframes: {name}{melttext}'))
|
|
323
|
+
# if options.get('show',False):
|
|
324
|
+
# display(df)
|
|
325
|
+
# if melt: display(df_melted)
|
|
326
|
+
# return
|
|
327
|
+
|
|
328
|
+
@register_cell_magic
|
|
329
|
+
def modeleviews(line, cell):
|
|
330
|
+
'''Creates a ModelFlow model from a Latex document
|
|
331
|
+
|
|
332
|
+
In developement
|
|
333
|
+
'''
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
import pyeviews as evp
|
|
337
|
+
except:
|
|
338
|
+
print('no pyeviews')
|
|
339
|
+
raise Exception('No pyeviews ')
|
|
340
|
+
from pathlib import Path, PureWindowsPath
|
|
341
|
+
|
|
342
|
+
name,options = get_options(line)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
display(Markdown(f'# Now creating the model **{name}**'))
|
|
346
|
+
|
|
347
|
+
commands = cell.split('\n')
|
|
348
|
+
print(commands)
|
|
349
|
+
if options.get('debug',False):
|
|
350
|
+
display(Markdown('# executing these eviews commands'))
|
|
351
|
+
print(cell)
|
|
352
|
+
|
|
353
|
+
eviewsapp = evp.GetEViewsApp(instance='new',showwindow=True)
|
|
354
|
+
for c in commands:
|
|
355
|
+
print(c)
|
|
356
|
+
evp.Run(c,eviewsapp)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
evp.Cleanup()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def ibmelt(df,prefix='',per=3):
|
|
366
|
+
# breakpoint()
|
|
367
|
+
temp= df.reset_index().rename(columns={'index':'row'}).melt(id_vars='row',var_name='column').assign(var_name=lambda x: prefix+x.row+'_'+x.column) .loc[:,['value','var_name']].set_index('var_name')
|
|
368
|
+
newdf = pd.concat([temp]*per,axis=1).T
|
|
369
|
+
newdf.index = range(per)
|
|
370
|
+
newdf.index.name = 'Year'
|
|
371
|
+
return newdf
|
|
372
|
+
|
|
373
|
+
@register_cell_magic
|
|
374
|
+
def dataframe(line, cell):
|
|
375
|
+
'''Converts this cell to a dataframe. and create a melted dataframe
|
|
376
|
+
|
|
377
|
+
Works for yearly data.
|
|
378
|
+
|
|
379
|
+
Options can be added to the input line
|
|
380
|
+
- t transposes the dataframe
|
|
381
|
+
- periods=<number> repeats the melted datframe
|
|
382
|
+
- melt will create a melted dataframe
|
|
383
|
+
- prefix=<a string> prefix columns in the melted dataframe
|
|
384
|
+
- show will show the resulting dataframes
|
|
385
|
+
- start will set the start index (default to 2021) '''
|
|
386
|
+
|
|
387
|
+
name,options = get_options(line,'Testgraph')
|
|
388
|
+
|
|
389
|
+
trans = options.get('t',False )
|
|
390
|
+
|
|
391
|
+
prefix = options.get('prefix','')
|
|
392
|
+
periods = int(options.get('periods','1'))
|
|
393
|
+
melt = options.get('melt',False)
|
|
394
|
+
start = int(options.get('start','2021'))
|
|
395
|
+
silent = options.get('silent',True)
|
|
396
|
+
ia = get_ipython()
|
|
397
|
+
|
|
398
|
+
xtrans = (lambda xx:xx.T) if trans else (lambda xx:xx)
|
|
399
|
+
xcell= cell.replace('%','').replace(',','.')
|
|
400
|
+
mul = 0.01 if '%' in cell else 1.
|
|
401
|
+
sio = StringIO(xcell)
|
|
402
|
+
|
|
403
|
+
df = pd.read_csv(sio,sep=r"\s+|\t+|\s+\t+|\t+\s+",engine='python')\
|
|
404
|
+
.pipe(xtrans)\
|
|
405
|
+
.pipe(lambda xx:xx.rename(index = {i:i.upper() if type(i) == str else i for i in xx.index}
|
|
406
|
+
,columns={c:c.upper() for c in xx.columns})) *mul
|
|
407
|
+
# breakpoint()
|
|
408
|
+
if not melt:
|
|
409
|
+
df= pd.concat([df]*periods,axis=0)
|
|
410
|
+
df.index = pd.period_range(start=start,freq = 'Y',periods=len(df))
|
|
411
|
+
df.index.name = 'index'
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
globals()[f'{name}'] = df
|
|
415
|
+
ia.push(f'{name}',interactive=True)
|
|
416
|
+
if melt:
|
|
417
|
+
df_melted = ibmelt(df,prefix=prefix.upper(),per=periods)
|
|
418
|
+
df_melted.index = pd.period_range(start=start,freq = 'Y',periods=len(df_melted))
|
|
419
|
+
df_melted.index.name = 'index'
|
|
420
|
+
|
|
421
|
+
globals()[f'{name}_melted'] = df_melted
|
|
422
|
+
ia.push(f'{name}_melted',interactive=True)
|
|
423
|
+
if not silent:
|
|
424
|
+
melttext = f' and {name}_melted' if melt else ''
|
|
425
|
+
display(Markdown(f'## Created the dataframes: {name}{melttext}'))
|
|
426
|
+
if options.get('show',False):
|
|
427
|
+
display(df)
|
|
428
|
+
if melt: display(df_melted)
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _mdmodel_impl(line, cell=None):
|
|
439
|
+
"""
|
|
440
|
+
Shared implementation for %mdmodel and %%mdmodel
|
|
441
|
+
"""
|
|
442
|
+
ip = get_ipython()
|
|
443
|
+
user_ns = ip.user_ns
|
|
444
|
+
|
|
445
|
+
name, options = get_options(line)
|
|
446
|
+
spec = options.get('spec', 'markdown')
|
|
447
|
+
|
|
448
|
+
# ------------------------------------------------------------
|
|
449
|
+
# Resolve options that can be literals or names in the user namespace.
|
|
450
|
+
# ``_resolve_option`` handles ast.literal_eval first, then user_ns
|
|
451
|
+
# lookup, and prints a warning on bad names instead of failing silently.
|
|
452
|
+
# ------------------------------------------------------------
|
|
453
|
+
replacements = _resolve_option(options, 'replacements', user_ns)
|
|
454
|
+
funks = _resolve_option(options, 'funks', user_ns, default=[]) or []
|
|
455
|
+
input_df = _resolve_option(options, 'input_df', user_ns)
|
|
456
|
+
estimator = _resolve_option(options, "estimator", user_ns, default=None)
|
|
457
|
+
|
|
458
|
+
if estimator is None:
|
|
459
|
+
estimator = _resolve_option(options, "est", user_ns, default=None) # smpl can be a tuple/list literal like (2010, 2019) or a name in
|
|
460
|
+
# user_ns; pass it through to Makemodel which already knows how to
|
|
461
|
+
# parse strings, tuples, slices, etc. via _parse_smpl.
|
|
462
|
+
smpl = _resolve_option(options, 'smpl', user_ns)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ------------------------------------------------------------
|
|
466
|
+
# Handle segmented models
|
|
467
|
+
# ------------------------------------------------------------
|
|
468
|
+
if options.get('segment', False):
|
|
469
|
+
dict_name = f"{name}_dict"
|
|
470
|
+
user_ns.setdefault(dict_name, {})
|
|
471
|
+
|
|
472
|
+
segment_name = options.get('segment', 'rest')
|
|
473
|
+
|
|
474
|
+
if cell is not None:
|
|
475
|
+
user_ns[dict_name][segment_name] = cell
|
|
476
|
+
|
|
477
|
+
# Display-only segments
|
|
478
|
+
if segment_name.startswith(('list', 'text')) and cell is not None:
|
|
479
|
+
display_model(cell, spec=spec)
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Combine lists + current cell
|
|
483
|
+
model_text = '\n'.join(
|
|
484
|
+
txt for seg, txt in user_ns[dict_name].items()
|
|
485
|
+
if seg.startswith('list')
|
|
486
|
+
) + (cell or "")
|
|
487
|
+
|
|
488
|
+
model_text_no_list = cell or ""
|
|
489
|
+
|
|
490
|
+
model_text_latex_nowrap = modeltext_to_latex(model_text)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
exploded_name = segment_name
|
|
494
|
+
|
|
495
|
+
else:
|
|
496
|
+
dict_name = f"{name}_dict"
|
|
497
|
+
|
|
498
|
+
if dict_name in user_ns:
|
|
499
|
+
segments = user_ns[dict_name]
|
|
500
|
+
|
|
501
|
+
model_text_lists = '\n'.join(
|
|
502
|
+
txt for k, txt in segments.items()
|
|
503
|
+
if k.startswith('list')
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
model_text_no_list = '\n'.join(
|
|
507
|
+
txt for k, txt in segments.items()
|
|
508
|
+
if not k.startswith('list')
|
|
509
|
+
) + (cell or "")
|
|
510
|
+
|
|
511
|
+
model_text = model_text_no_list + '\n' + model_text_lists
|
|
512
|
+
|
|
513
|
+
model_text_latex_nowrap = '\n'.join(
|
|
514
|
+
modeltext_to_latex(txt)
|
|
515
|
+
if k.startswith(('text', 'list')) else txt
|
|
516
|
+
for k, txt in segments.items()
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
else:
|
|
520
|
+
model_text = cell or ""
|
|
521
|
+
model_text_no_list = model_text
|
|
522
|
+
model_text_latex_nowrap = modeltext_to_latex(model_text)
|
|
523
|
+
|
|
524
|
+
exploded_name = name
|
|
525
|
+
|
|
526
|
+
# ------------------------------------------------------------
|
|
527
|
+
# Create Makemodel model
|
|
528
|
+
# ------------------------------------------------------------
|
|
529
|
+
# Only forward estimation kwargs when the user actually supplied them
|
|
530
|
+
# on the magic line; that keeps the no-estimation path identical to
|
|
531
|
+
# the previous behavior.
|
|
532
|
+
makemodel_kwargs = dict(
|
|
533
|
+
replacements=replacements,
|
|
534
|
+
estimator_namespace=user_ns,
|
|
535
|
+
funks=funks,
|
|
536
|
+
)
|
|
537
|
+
if input_df is not None:
|
|
538
|
+
makemodel_kwargs['input_df'] = input_df
|
|
539
|
+
if estimator is not None:
|
|
540
|
+
makemodel_kwargs['estimator'] = estimator
|
|
541
|
+
if smpl is not None:
|
|
542
|
+
makemodel_kwargs['smpl'] = smpl
|
|
543
|
+
|
|
544
|
+
emodel = Makemodel(model_text, **makemodel_kwargs)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
user_ns[exploded_name] = emodel
|
|
548
|
+
|
|
549
|
+
# ------------------------------------------------------------
|
|
550
|
+
# Rendering
|
|
551
|
+
# ------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if options.get('render_est', True):
|
|
555
|
+
if options.get('render_list', True):
|
|
556
|
+
display_model(emodel.markdown_with_estimation, spec=spec)
|
|
557
|
+
else:
|
|
558
|
+
display_model(emodel.markdown_with_estimation_no_list, spec=spec)
|
|
559
|
+
else:
|
|
560
|
+
if options.get('render', True):
|
|
561
|
+
if options.get('render_list', True):
|
|
562
|
+
display_model(model_text, spec=spec)
|
|
563
|
+
else:
|
|
564
|
+
display_model(model_text_no_list, spec=spec)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
if options.get('show', False):
|
|
569
|
+
emodel.show
|
|
570
|
+
|
|
571
|
+
if options.get('draw', False) and not options.get('display', False):
|
|
572
|
+
emodel.draw
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
if options.get('latex', False):
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
|
|
579
|
+
emodel.latex_nowrap = markdown_titles_to_latex(model_text_latex_nowrap)
|
|
580
|
+
|
|
581
|
+
LatexRepo( emodel.latex_nowrap,name=exploded_name).pdf(pdfopen=True)
|
|
582
|
+
except Exception:
|
|
583
|
+
print("no latex")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
if options.get('display', False):
|
|
588
|
+
display_model(model_text, spec=spec)
|
|
589
|
+
if dict_name in user_ns:
|
|
590
|
+
segs = list(user_ns[dict_name].keys())
|
|
591
|
+
print(f"Model `{name}` built from segments: {', '.join(segs)}")
|
|
592
|
+
print(f"✅ Created Mexplode model: {name}")
|
|
593
|
+
print(emodel)
|
|
594
|
+
|
|
595
|
+
return emodel
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# -----------------------------------------------------------------
|
|
599
|
+
# Cell magic
|
|
600
|
+
# -----------------------------------------------------------------
|
|
601
|
+
@register_cell_magic
|
|
602
|
+
def Makemymodel(line, cell):
|
|
603
|
+
_ = _mdmodel_impl(line, cell)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@register_line_magic
|
|
608
|
+
def Makemymodel(line):
|
|
609
|
+
"""
|
|
610
|
+
%Makemymodel <name> [options...]
|
|
611
|
+
|
|
612
|
+
Rebuild / re-render an existing Markdown-based model
|
|
613
|
+
without adding new content.
|
|
614
|
+
"""
|
|
615
|
+
_ = _mdmodel_impl(line, cell=None)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
except:
|
|
619
|
+
print('no magic')
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def modeltext_to_latex(source: str) -> str:
|
|
623
|
+
import re
|
|
624
|
+
from textwrap import dedent
|
|
625
|
+
|
|
626
|
+
def wrap_blockquote_equations(text):
|
|
627
|
+
"""
|
|
628
|
+
Wrap consecutive Markdown lines starting with '>' in LaTeX \\verb blocks.
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
text : str
|
|
633
|
+
Full Markdown text
|
|
634
|
+
|
|
635
|
+
Returns
|
|
636
|
+
-------
|
|
637
|
+
str
|
|
638
|
+
LaTeX-safe text
|
|
639
|
+
"""
|
|
640
|
+
lines = text.splitlines()
|
|
641
|
+
out = []
|
|
642
|
+
in_block = False
|
|
643
|
+
|
|
644
|
+
for line in lines:
|
|
645
|
+
if line.startswith(">"):
|
|
646
|
+
if not in_block:
|
|
647
|
+
out.append(r"\par\noindent")
|
|
648
|
+
in_block = True
|
|
649
|
+
|
|
650
|
+
# pick a safe delimiter for \verb
|
|
651
|
+
for delim in ("|", "!", "/", "+", "#"):
|
|
652
|
+
if delim not in line:
|
|
653
|
+
break
|
|
654
|
+
|
|
655
|
+
out.append(rf"\verb{delim}{line}{delim}\\")
|
|
656
|
+
else:
|
|
657
|
+
if in_block:
|
|
658
|
+
out.append(r"\par")
|
|
659
|
+
in_block = False
|
|
660
|
+
out.append(line)
|
|
661
|
+
|
|
662
|
+
if in_block:
|
|
663
|
+
out.append(r"\par")
|
|
664
|
+
|
|
665
|
+
return "\n".join(out)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def markdown_item_lists_to_latex(text):
|
|
669
|
+
"""
|
|
670
|
+
Convert Markdown bullet (- item) and numbered (1. item) lists
|
|
671
|
+
into LaTeX itemize / enumerate environments.
|
|
672
|
+
|
|
673
|
+
Guards against formulas, numeric lines, and symbols.
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
list_block = re.compile(
|
|
677
|
+
r'(?:^|\n)'
|
|
678
|
+
r'('
|
|
679
|
+
r'(?:\s*(?:-|\d+\.)\s+'
|
|
680
|
+
r'(?=.*[A-Za-z])' # must contain at least one letter
|
|
681
|
+
r'(?!.*=)' # reject equations
|
|
682
|
+
r'.+'
|
|
683
|
+
r'\n?)+'
|
|
684
|
+
r')',
|
|
685
|
+
flags=re.MULTILINE
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
def repl_list(match):
|
|
689
|
+
block = match.group(1)
|
|
690
|
+
|
|
691
|
+
first = re.search(r'\S+', block).group()
|
|
692
|
+
|
|
693
|
+
if first.startswith('-'):
|
|
694
|
+
items = re.findall(
|
|
695
|
+
r'\s*-\s+(?=(?:.*[A-Za-z]))(?!.*=)(.+)',
|
|
696
|
+
block
|
|
697
|
+
)
|
|
698
|
+
env = 'itemize'
|
|
699
|
+
else:
|
|
700
|
+
items = re.findall(
|
|
701
|
+
r'\s*\d+\.\s+(?=(?:.*[A-Za-z]))(?!.*=)(.+)',
|
|
702
|
+
block
|
|
703
|
+
)
|
|
704
|
+
env = 'enumerate'
|
|
705
|
+
|
|
706
|
+
latex = [f'\\begin{{{env}}}']
|
|
707
|
+
latex += [f' \\item {item}' for item in items]
|
|
708
|
+
latex.append(f'\\end{{{env}}}')
|
|
709
|
+
|
|
710
|
+
return '\n' + '\n'.join(latex)
|
|
711
|
+
|
|
712
|
+
return list_block.sub(repl_list, text)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
TABLE_RE = re.compile(
|
|
716
|
+
r"""
|
|
717
|
+
( # entire table
|
|
718
|
+
(?:^\|.*\|\s*\n) # header
|
|
719
|
+
(?:^\|\s*[-:]+.*\|\s*\n) # separator
|
|
720
|
+
(?:^\|.*\|\s*\n?)+ # body
|
|
721
|
+
)
|
|
722
|
+
""",
|
|
723
|
+
re.MULTILINE | re.VERBOSE,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def markdown_table_to_latex(table, align=None):
|
|
728
|
+
lines = [l.strip() for l in table.strip().splitlines()]
|
|
729
|
+
|
|
730
|
+
# Remove separator row
|
|
731
|
+
rows = []
|
|
732
|
+
for l in lines:
|
|
733
|
+
if re.match(r'^\|\s*[-:]+', l):
|
|
734
|
+
continue
|
|
735
|
+
rows.append([c.strip() for c in l.strip('|').split('|')])
|
|
736
|
+
|
|
737
|
+
ncols = len(rows[0])
|
|
738
|
+
align = align or ("l" * ncols)
|
|
739
|
+
|
|
740
|
+
def md_to_tex(cell):
|
|
741
|
+
cell = re.sub(r'\*\*(.*?)\*\*', r'\\textbf{\1}', cell)
|
|
742
|
+
return cell
|
|
743
|
+
|
|
744
|
+
rows = [[md_to_tex(c) for c in r] for r in rows]
|
|
745
|
+
|
|
746
|
+
latex = []
|
|
747
|
+
# latex.append(r"\\")
|
|
748
|
+
latex.append(r"\begin{tabular}{" + align + r"}")
|
|
749
|
+
latex.append(r"\hline")
|
|
750
|
+
|
|
751
|
+
for i, row in enumerate(rows):
|
|
752
|
+
latex.append(" & ".join(row) + r" \\")
|
|
753
|
+
if i == 0:
|
|
754
|
+
latex.append(r"\hline")
|
|
755
|
+
|
|
756
|
+
latex.append(r"\hline")
|
|
757
|
+
latex.append(r"\end{tabular}")
|
|
758
|
+
latex.append(r"\\")
|
|
759
|
+
latex.append(r"")
|
|
760
|
+
|
|
761
|
+
return "\n".join(latex)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def replace_markdown_tables(text):
|
|
765
|
+
"""
|
|
766
|
+
Replace all Markdown tables in a string with LaTeX tables.
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
def repl_table(match):
|
|
770
|
+
table = match.group(1)
|
|
771
|
+
return markdown_table_to_latex(table)
|
|
772
|
+
|
|
773
|
+
return TABLE_RE.sub(repl_table, dedent(text))
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
# source = dedent(source).strip()
|
|
779
|
+
|
|
780
|
+
# Markdown → LaTeX
|
|
781
|
+
|
|
782
|
+
body = markdown_item_lists_to_latex(source)
|
|
783
|
+
body = replace_markdown_tables(body)
|
|
784
|
+
body = wrap_blockquote_equations(body)
|
|
785
|
+
|
|
786
|
+
return body
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def wrap_latex(body):
|
|
790
|
+
# Assemble LaTeX
|
|
791
|
+
latex = rf"""
|
|
792
|
+
\documentclass[11pt]{{article}}
|
|
793
|
+
\usepackage{{amsmath,amssymb}}
|
|
794
|
+
\usepackage{{booktabs}}
|
|
795
|
+
\usepackage[utf8]{{inputenc}}
|
|
796
|
+
\usepackage{{geometry}}
|
|
797
|
+
\geometry{{margin=1in}}
|
|
798
|
+
|
|
799
|
+
\begin{{document}}
|
|
800
|
+
|
|
801
|
+
{body}
|
|
802
|
+
|
|
803
|
+
\end{{document}}
|
|
804
|
+
""".strip()
|
|
805
|
+
|
|
806
|
+
return latex
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def markdown_titles_to_latex(text: str) -> str:
|
|
810
|
+
outtext = re.sub(r"^# (.+)$", r"\\section{\1}", text, flags=re.MULTILINE)
|
|
811
|
+
outtext = re.sub(r"^## (.+)$", r"\\subsection{\1}", outtext, flags=re.MULTILINE)
|
|
812
|
+
outtext = re.sub(r"^### (.+)$", r"\\subsubsection{\1}", outtext, flags=re.MULTILINE)
|
|
813
|
+
return outtext
|