modelflowib 2.80__tar.gz → 2.82__tar.gz

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.
Files changed (50) hide show
  1. {modelflowib-2.80 → modelflowib-2.82}/PKG-INFO +14 -10
  2. {modelflowib-2.80 → modelflowib-2.82}/modelclass.py +213 -55
  3. {modelflowib-2.80 → modelflowib-2.82}/modelflowib.egg-info/PKG-INFO +14 -10
  4. {modelflowib-2.80 → modelflowib-2.82}/modelflowib.egg-info/requires.txt +12 -6
  5. {modelflowib-2.80 → modelflowib-2.82}/pyproject.toml +23 -11
  6. {modelflowib-2.80 → modelflowib-2.82}/README.md +0 -0
  7. {modelflowib-2.80 → modelflowib-2.82}/license.md +0 -0
  8. {modelflowib-2.80 → modelflowib-2.82}/modelBLfunk.py +0 -0
  9. {modelflowib-2.80 → modelflowib-2.82}/model_Excel.py +0 -0
  10. {modelflowib-2.80 → modelflowib-2.82}/model_cvx.py +0 -0
  11. {modelflowib-2.80 → modelflowib-2.82}/model_dynare.py +0 -0
  12. {modelflowib-2.80 → modelflowib-2.82}/model_financial_stability.py +0 -0
  13. {modelflowib-2.80 → modelflowib-2.82}/model_latex.py +0 -0
  14. {modelflowib-2.80 → modelflowib-2.82}/model_latex_class.py +0 -0
  15. {modelflowib-2.80 → modelflowib-2.82}/model_parquet_mixin.py +0 -0
  16. {modelflowib-2.80 → modelflowib-2.82}/modelconstruct.py +0 -0
  17. {modelflowib-2.80 → modelflowib-2.82}/modelconstruct_estimation.py +0 -0
  18. {modelflowib-2.80 → modelflowib-2.82}/modeldash.py +0 -0
  19. {modelflowib-2.80 → modelflowib-2.82}/modeldashboot.py +0 -0
  20. {modelflowib-2.80 → modelflowib-2.82}/modeldashsidebar.py +0 -0
  21. {modelflowib-2.80 → modelflowib-2.82}/modeldekom.py +0 -0
  22. {modelflowib-2.80 → modelflowib-2.82}/modeldiff.py +0 -0
  23. {modelflowib-2.80 → modelflowib-2.82}/modeldisplay.py +0 -0
  24. {modelflowib-2.80 → modelflowib-2.82}/modelestimation.py +0 -0
  25. {modelflowib-2.80 → modelflowib-2.82}/modelestimator_new.py +0 -0
  26. {modelflowib-2.80 → modelflowib-2.82}/modelflowib.egg-info/SOURCES.txt +0 -0
  27. {modelflowib-2.80 → modelflowib-2.82}/modelflowib.egg-info/dependency_links.txt +0 -0
  28. {modelflowib-2.80 → modelflowib-2.82}/modelflowib.egg-info/top_level.txt +0 -0
  29. {modelflowib-2.80 → modelflowib-2.82}/modelgrab.py +0 -0
  30. {modelflowib-2.80 → modelflowib-2.82}/modelgrabgdx.py +0 -0
  31. {modelflowib-2.80 → modelflowib-2.82}/modelgrabwf2.py +0 -0
  32. {modelflowib-2.80 → modelflowib-2.82}/modelhelp.py +0 -0
  33. {modelflowib-2.80 → modelflowib-2.82}/modelhtml.py +0 -0
  34. {modelflowib-2.80 → modelflowib-2.82}/modelinvert.py +0 -0
  35. {modelflowib-2.80 → modelflowib-2.82}/modeljupyter.py +0 -0
  36. {modelflowib-2.80 → modelflowib-2.82}/modeljupytermagic.py +0 -0
  37. {modelflowib-2.80 → modelflowib-2.82}/modelmacrograb.py +0 -0
  38. {modelflowib-2.80 → modelflowib-2.82}/modelmanipulation.py +0 -0
  39. {modelflowib-2.80 → modelflowib-2.82}/modelmf.py +0 -0
  40. {modelflowib-2.80 → modelflowib-2.82}/modelnet.py +0 -0
  41. {modelflowib-2.80 → modelflowib-2.82}/modelnewton.py +0 -0
  42. {modelflowib-2.80 → modelflowib-2.82}/modelnormalize.py +0 -0
  43. {modelflowib-2.80 → modelflowib-2.82}/modelpattern.py +0 -0
  44. {modelflowib-2.80 → modelflowib-2.82}/modelreport.py +0 -0
  45. {modelflowib-2.80 → modelflowib-2.82}/modelsolver_ng.py +0 -0
  46. {modelflowib-2.80 → modelflowib-2.82}/modeluserfunk.py +0 -0
  47. {modelflowib-2.80 → modelflowib-2.82}/modelvis.py +0 -0
  48. {modelflowib-2.80 → modelflowib-2.82}/modelwidget.py +0 -0
  49. {modelflowib-2.80 → modelflowib-2.82}/modelwidget_input.py +0 -0
  50. {modelflowib-2.80 → modelflowib-2.82}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.80
3
+ Version: 2.82
4
4
  Summary: A tool to solve and manage dynamic economic and financial models
5
5
  Author-email: Ib Hansen <Ib.Hansen.Iv@gmail.com>
6
6
  License-Expression: MIT
@@ -20,22 +20,26 @@ Requires-Dist: numpy
20
20
  Requires-Dist: networkx
21
21
  Requires-Dist: tqdm
22
22
  Requires-Dist: scipy
23
- Requires-Dist: numexpr
24
- Requires-Dist: bottleneck
25
23
  Requires-Dist: XlsxWriter
26
24
  Requires-Dist: pyarrow
27
25
  Requires-Dist: tabulate
28
26
  Requires-Dist: openpyxl
29
27
  Requires-Dist: xarray
30
- Requires-Dist: cvxopt
31
- Requires-Dist: numba
32
- Requires-Dist: dash
33
- Requires-Dist: dash-bootstrap-components
34
- Requires-Dist: dash-interactive-graphviz
35
- Requires-Dist: lmfit
36
- Requires-Dist: jupyter
28
+ Requires-Dist: jinja2
29
+ Requires-Dist: ipython
37
30
  Requires-Dist: ipywidgets
38
31
  Requires-Dist: ipydatagrid
32
+ Requires-Dist: lmfit
33
+ Requires-Dist: statsmodels
34
+ Requires-Dist: numba; sys_platform != "emscripten"
35
+ Requires-Dist: cvxopt; sys_platform != "emscripten"
36
+ Requires-Dist: numexpr; sys_platform != "emscripten"
37
+ Requires-Dist: bottleneck; sys_platform != "emscripten"
38
+ Requires-Dist: dash; sys_platform != "emscripten"
39
+ Requires-Dist: dash-bootstrap-components; sys_platform != "emscripten"
40
+ Requires-Dist: dash-interactive-graphviz; sys_platform != "emscripten"
41
+ Requires-Dist: plotly; sys_platform != "emscripten"
42
+ Requires-Dist: jupyter; sys_platform != "emscripten"
39
43
  Provides-Extra: shiny
40
44
  Requires-Dist: shiny; extra == "shiny"
41
45
  Requires-Dist: shinywidgets; extra == "shiny"
@@ -73,6 +73,11 @@ from typing import Iterable
73
73
  import zipfile
74
74
  from functools import partial,cached_property,lru_cache
75
75
  from tqdm.auto import tqdm
76
+ if sys.platform == 'emscripten':
77
+ # a browser has no threads, so tqdm can not start its monitor thread. Set on the
78
+ # base class, which tqdm.auto and tqdm.notebook inherit it from
79
+ import tqdm as tqdm_module
80
+ tqdm_module.tqdm.monitor_interval = 0
76
81
  import operator
77
82
  from urllib.request import urlopen
78
83
  from io import BytesIO,StringIO
@@ -4093,8 +4098,10 @@ class Graph_Draw_Mixin():
4093
4098
  self.att_dic = {}
4094
4099
 
4095
4100
  G = nx.DiGraph()
4096
- # a set, to weed out multiple links
4097
- G.add_edges_from({(x.child, x.parent) for x in alllinks})
4101
+ # a set, to weed out multiple links, sorted so the nodes always go into
4102
+ # the graph in the same order. Then nx_back_edges cuts the same cycles
4103
+ # each time, and the same model gives the same drawing in every session
4104
+ G.add_edges_from(sorted({(x.child, x.parent) for x in alllinks}))
4098
4105
 
4099
4106
  for v in G.nodes:
4100
4107
  var_name = v.split('(')[0]
@@ -4130,14 +4137,18 @@ class Graph_Draw_Mixin():
4130
4137
  else:
4131
4138
  G.nodes[v]['layer'] = 0 if down_len is None else down_len
4132
4139
  else:
4133
- # no variable in the center, like drawmodel, then the layer is the logical
4134
- # order. The simultaneous blocks are condensed to one node, else there is
4135
- # no order, and a node of such a block gets the layer of its block
4136
- condensed = nx.condensation(G)
4137
- for layer, blocks in enumerate(nx.topological_generations(condensed)):
4138
- for block in blocks:
4139
- for v in condensed.nodes[block]['members']:
4140
- G.nodes[v]['layer'] = layer
4140
+ # no variable in the center, like drawmodel, then the layer is the
4141
+ # logical order. A simultaneous block is a cycle and has no order, so
4142
+ # the edges which close the cycles are taken out first, and then the
4143
+ # nodes of the block are spread over the columns like all the others.
4144
+ # This is what graphviz does before it ranks the nodes, and the edges
4145
+ # which were taken out are drawn as back edges by display_nx_svg
4146
+ dag = nx.DiGraph()
4147
+ dag.add_nodes_from(G)
4148
+ dag.add_edges_from(set(G.edges) - self.nx_back_edges(G))
4149
+ for layer, vs in enumerate(nx.topological_generations(dag)):
4150
+ for v in vs:
4151
+ G.nodes[v]['layer'] = layer
4141
4152
 
4142
4153
  for child, parent in G.edges:
4143
4154
  try:
@@ -4155,6 +4166,43 @@ class Graph_Draw_Mixin():
4155
4166
 
4156
4167
  return G
4157
4168
 
4169
+ @staticmethod
4170
+ def nx_back_edges(G):
4171
+ '''The edges which close a cycle, found by a depth first search
4172
+
4173
+ Taking them out leaves a graph without cycles, which can be sorted into
4174
+ columns. Graphviz does the same before it ranks the nodes of a drawing,
4175
+ and then draws the edges which were taken out as curved back edges.
4176
+
4177
+ For a model this means that a simultaneous block is spread over the
4178
+ columns like the rest of the model, instead of standing in one column
4179
+ where all its edges would be hidden behind the boxes.
4180
+
4181
+ Returns a set of (from, to) edges, self loops included.
4182
+ '''
4183
+ back = set()
4184
+ # the nodes on the current path have state 0, the finished ones state 1.
4185
+ # An edge to a node on the path closes a cycle, and is a back edge
4186
+ state = {}
4187
+ for root in G:
4188
+ if root in state:
4189
+ continue
4190
+ state[root] = 0
4191
+ stack = [(root, iter(G[root]))]
4192
+ while stack:
4193
+ v, children = stack[-1]
4194
+ for child in children:
4195
+ if state.get(child) == 0:
4196
+ back.add((v, child))
4197
+ elif child not in state:
4198
+ state[child] = 0
4199
+ stack.append((child, iter(G[child])))
4200
+ break
4201
+ else:
4202
+ state[v] = 1
4203
+ stack.pop()
4204
+ return back
4205
+
4158
4206
  @staticmethod
4159
4207
  def nx_layout(G, sort=True):
4160
4208
  '''Positions for a graph made by alllinks_to_nx
@@ -4246,7 +4294,10 @@ class Graph_Draw_Mixin():
4246
4294
  width=[G.edges[e]['width'] for e in edgelist],
4247
4295
  edge_color='grey', arrows=True, arrowstyle='-|>',
4248
4296
  arrowsize=12, node_size=1200,
4249
- min_source_margin=25, min_target_margin=25, ax=ax)
4297
+ min_source_margin=25, min_target_margin=25,
4298
+ # curved, so the two links between a variable and
4299
+ # its lagged value are not drawn on top of each other
4300
+ connectionstyle='arc3,rad=0.12', ax=ax)
4250
4301
  for v, (x, y) in pos.items():
4251
4302
  ax.text(x, y, G.nodes[v]['label'], ha='center', va='center',
4252
4303
  fontsize=fontsize, color='blue', zorder=3,
@@ -4279,7 +4330,7 @@ class Graph_Draw_Mixin():
4279
4330
  :browser: open the svg in a separate browser window, where it can be zoomed
4280
4331
  '''
4281
4332
  fontsize = kwargs.get('fontsize', 12)
4282
- pad, gap_x, gap_y = 6, 90, 22
4333
+ pad, gap_y = 6, 22
4283
4334
  char_width = 0.62 * fontsize # the labels are drawn in a monospace font
4284
4335
  pos = self.nx_layout(G, sort=kwargs.get('sort', True))
4285
4336
 
@@ -4291,14 +4342,20 @@ class Graph_Draw_Mixin():
4291
4342
  box = {v: (len(G.nodes[v]['label']) * char_width + 2 * pad, fontsize + 2 * pad)
4292
4343
  for v in G.nodes}
4293
4344
 
4345
+ # the space between two columns. A fixed distance looks cramped when the
4346
+ # labels carry the variable descriptions and the boxes get wide, so it
4347
+ # follows the widest box. des=False keeps the labels, and the boxes, short
4348
+ gap_x = max(90., 0.25 * max(w for w, h in box.values()))
4349
+
4294
4350
  # the nodes are placed in columns by layer, in the order found by nx_layout
4295
4351
  layers = defaultdict(list)
4296
4352
  for v in sorted(G.nodes, key=lambda v: pos[v][1]):
4297
4353
  layers[G.nodes[v]['layer']].append(v)
4298
4354
 
4299
- center, x = {}, gap_x
4355
+ center, colwidths, x = {}, {}, gap_x
4300
4356
  for lev in sorted(layers):
4301
4357
  colwidth = max(box[v][0] for v in layers[lev])
4358
+ colwidths[lev] = colwidth
4302
4359
  colheight = sum(box[v][1] + gap_y for v in layers[lev]) - gap_y
4303
4360
  y = -colheight / 2.
4304
4361
  for v in layers[lev]:
@@ -4312,9 +4369,9 @@ class Graph_Draw_Mixin():
4312
4369
  width = x
4313
4370
  height = max(cy + box[v][1] / 2. for v, (cx, cy) in center.items()) + gap_y
4314
4371
 
4315
- def border(v, other):
4316
- '''The point where the line from v towards other leaves the box of v'''
4317
- (cx, cy), (ox, oy) = center[v], center[other]
4372
+ def border(v, ox, oy):
4373
+ '''The point where the line from v towards (ox,oy) leaves the box of v'''
4374
+ cx, cy = center[v]
4318
4375
  w, h = box[v]
4319
4376
  dx, dy = ox - cx, oy - cy
4320
4377
  if not dx and not dy:
@@ -4322,21 +4379,51 @@ class Graph_Draw_Mixin():
4322
4379
  scale = min(w / 2. / abs(dx) if dx else 1e9, h / 2. / abs(dy) if dy else 1e9)
4323
4380
  return cx + dx * scale, cy + dy * scale
4324
4381
 
4382
+ def bow(child, parent):
4383
+ '''How far the link bows out from the straight line, 0 for a straight line
4384
+
4385
+ A straight line fails in three cases. A link which points back to an
4386
+ earlier column - the feedback over the periods from a variable to its
4387
+ lagged value is one - would run straight through the boxes between the
4388
+ two ends. The two links of a pair would fall on top of each other. And
4389
+ both ends in the same column gives a line behind the boxes of the
4390
+ column. Graphviz curves such back edges, and so does this.
4391
+ '''
4392
+ lchild, lparent = G.nodes[child]['layer'], G.nodes[parent]['layer']
4393
+ if lchild == lparent:
4394
+ # out of the column, so the curve is clear of the boxes
4395
+ return colwidths[lchild] + gap_x / 2.
4396
+ if lparent < lchild or G.has_edge(parent, child):
4397
+ # the longer the way back, the more boxes to arch over
4398
+ return gap_y * (1 + abs(lparent - lchild))
4399
+ return 0.
4400
+
4325
4401
  arrow = f'arrow{abs(hash(navn)) % 100000}' # an id of its own for each drawing
4326
4402
  lines = []
4327
4403
  for child, parent in G.edges:
4328
4404
  edge = G.edges[child, parent]
4329
4405
  if not edge.get('visible', True):
4330
4406
  continue
4331
- x1, y1 = border(child, parent)
4332
- x2, y2 = border(parent, child)
4333
- length = max(((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5, 0.1)
4407
+ (cx1, cy1), (cx2, cy2) = center[child], center[parent]
4408
+ dx, dy = cx2 - cx1, cy2 - cy1
4409
+ span = max((dx * dx + dy * dy) ** 0.5, 0.1)
4410
+ # the control point sits beside the middle of the straight line. It
4411
+ # follows the direction, so the two links of a pair bow to each their
4412
+ # side. With no bow it is the middle itself, and the curve is a line
4413
+ side = bow(child, parent)
4414
+ ctrlx = (cx1 + cx2) / 2. - dy / span * side
4415
+ ctrly = (cy1 + cy2) / 2. + dx / span * side
4416
+ x1, y1 = border(child, ctrlx, ctrly)
4417
+ x2, y2 = border(parent, ctrlx, ctrly)
4334
4418
  # stop short of the box, so the arrow head is not drawn on top of it
4335
- x2, y2 = x2 - (x2 - x1) * 8. / length, y2 - (y2 - y1) * 8. / length
4419
+ ex, ey = x2 - ctrlx, y2 - ctrly
4420
+ elen = max((ex * ex + ey * ey) ** 0.5, 0.1)
4421
+ x2, y2 = x2 - ex / elen * 8., y2 - ey / elen * 8.
4336
4422
  lines.append(
4337
- f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="grey" '
4423
+ f'<path d="M {x1:.1f} {y1:.1f} Q {ctrlx:.1f} {ctrly:.1f} {x2:.1f} {y2:.1f}" '
4424
+ f'fill="none" stroke="grey" '
4338
4425
  f'stroke-width="{max(1., edge["width"]):.1f}" marker-end="url(#{arrow})">'
4339
- f'<title>{esc(edge["tooltip"])}</title></line>')
4426
+ f'<title>{esc(edge["tooltip"])}</title></path>')
4340
4427
 
4341
4428
  nodes = []
4342
4429
  for v, (cx, cy) in center.items():
@@ -4350,11 +4437,24 @@ class Graph_Draw_Mixin():
4350
4437
  f'{esc(G.nodes[v]["label"])}</text></g>')
4351
4438
 
4352
4439
  title = kwargs.get('title', navn)
4353
- titletext = (f'<text x="{gap_x}" y="{fontsize + 8}" font-family="sans-serif" '
4440
+ titletext = (f'<text x="{gap_x:.0f}" y="{fontsize + 8}" font-family="sans-serif" '
4354
4441
  f'font-size="{fontsize + 2}" fill="black">{esc(title)}</text>') if title else ''
4355
4442
 
4443
+ # the drawing fits the width of the notebook, as it always did, but it is
4444
+ # not scaled further down than minfontsize, where the labels stop being
4445
+ # readable and the boxes start to look like they run into each other.
4446
+ # A drawing wider than that keeps its size and the cell scrolls sideways.
4447
+ # min-width does this in the browser, which is the one who knows how wide
4448
+ # the cell is. width= overrules, '100%' always fits
4449
+ svgwidth = kwargs.get('width', '')
4450
+ if svgwidth:
4451
+ style = 'max-width:100%;height:auto'
4452
+ else:
4453
+ floor = min(1., kwargs.get('minfontsize', 9) / fontsize)
4454
+ svgwidth = f'{width:.0f}'
4455
+ style = f'max-width:100%;min-width:{width * floor:.0f}px;height:auto'
4356
4456
  svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width:.0f} {height:.0f}" '
4357
- f'width="{kwargs.get("width", "100%")}" style="max-width:100%;height:auto">'
4457
+ f'width="{svgwidth}" style="{style}">'
4358
4458
  f'<defs><marker id="{arrow}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" '
4359
4459
  'markerHeight="6" orient="auto-start-reverse">'
4360
4460
  '<path d="M 0 0 L 10 5 L 0 10 z" fill="grey"/></marker></defs>'
@@ -4376,7 +4476,8 @@ class Graph_Draw_Mixin():
4376
4476
  print(f'No separate window here: {e}')
4377
4477
  print(f'The drawing is saved as {svgname} and is shown below')
4378
4478
 
4379
- display(HTML(svg))
4479
+ # the div scrolls when min-width keeps the drawing wider than the cell
4480
+ display(HTML(f'<div style="overflow-x:auto;max-width:100%">{svg}</div>'))
4380
4481
  self.last_svg = svg
4381
4482
  return
4382
4483
 
@@ -6317,36 +6418,41 @@ class Display_Mixin():
6317
6418
  from IPython.display import display, Markdown, HTML
6318
6419
  from pathlib import Path
6319
6420
  display(Markdown(text))
6421
+ rows = []
6320
6422
  for dir in sorted(Path(folder).glob('**')):
6321
6423
  # print(f'{dir=} {nocwd=}')
6322
6424
  if len(dir.parts) and str(dir.parts[-1]).startswith('.'):
6323
6425
  continue
6324
-
6426
+
6325
6427
  if dir == Path('.') and nocwd :
6326
6428
  continue
6327
-
6328
- filelist = (list(dir.glob('*readme.ipynb'))
6329
- + [f for f in sorted(dir.glob('*.ipynb'))
6429
+
6430
+ filelist = (list(dir.glob('*readme.ipynb'))
6431
+ + [f for f in sorted(dir.glob('*.ipynb'))
6330
6432
  if not f.stem.endswith('readme')])
6331
-
6332
- for i, notebook in enumerate(filelist):
6333
- # print(notebook)
6334
- if (not all) and (notebook.name.startswith('test') or notebook.name.startswith('Overview')):
6335
- continue
6336
- if i == 0:
6337
- blanks = ''.join(
6338
- ['&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;']*len(dir.parts))
6339
- if len(dir.parts):
6340
- display(HTML(f'{blanks}<b>{str(dir)}</b>'))
6341
- else:
6342
- display(
6343
- HTML(f'{blanks}<b>{str(Path.cwd().parts[-1])} (.)</b>'))
6344
6433
 
6345
- name = notebook.name.split('.')[0]
6434
+ if not all:
6435
+ filelist = [f for f in filelist
6436
+ if not (f.name.startswith('test') or f.name.startswith('Overview'))]
6437
+
6438
+ if not filelist:
6439
+ continue
6440
+
6441
+ indent = 2 * len(dir.parts) # indentation of this folder in em
6442
+ folder_name = str(dir) if len(dir.parts) else f'{str(Path.cwd().parts[-1])} (.)'
6443
+ rows.append(
6444
+ f'<tr><td colspan="2" style="text-align:left;padding:6px 12px 2px {indent}em">'
6445
+ f'<b>{folder_name}</b></td></tr>')
6446
+
6447
+ for notebook in filelist:
6346
6448
  # print(notebook)
6347
- display(HTML(
6348
- f'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{blanks} <a href="{notebook}" target="_blank">{name}</a>'))
6349
-
6449
+ name = notebook.name.split('.')[0]
6450
+ title = Display_Mixin._notebook_title(notebook)
6451
+ rows.append(
6452
+ f'<tr><td style="text-align:left;padding:2px 24px 2px {indent+2}em;white-space:nowrap">'
6453
+ f'<a href="{notebook}" target="_blank">{name}</a></td>'
6454
+ f'<td style="text-align:left;padding:2px 12px">{title}</td></tr>')
6455
+
6350
6456
  # try:
6351
6457
  # # Replace with your method of opening a notebook
6352
6458
  # # Example: Using jupyter command-line to open the notebook
@@ -6354,12 +6460,43 @@ class Display_Mixin():
6354
6460
  # subprocess.Popen(["jupyter", "notebook", str(notebook)])
6355
6461
  # except Exception as e:
6356
6462
  # print(f"Error opening notebook {notebook}: {e}")
6463
+ if rows:
6464
+ display(HTML(
6465
+ '<table style="border-collapse:collapse;border:none">'
6466
+ + ''.join(rows) + '</table>'))
6357
6467
 
6358
6468
 
6469
+ @staticmethod
6470
+ def _notebook_title(fname):
6471
+ '''Returns the first line starting with one or more # in the first cell of a jupyter notebook.
6472
+
6473
+ Returns an empty string if the notebook can not be read or no such line is found'''
6474
+
6475
+ import json
6476
+ from html import escape
6477
+
6478
+ try:
6479
+ with open(fname, 'rt', encoding='utf-8') as f:
6480
+ nb = json.load(f)
6481
+ cells = nb.get('cells', [])
6482
+ if not cells:
6483
+ return ''
6484
+ source = cells[0].get('source', '')
6485
+ lines = source.splitlines() if isinstance(source, str) else source
6486
+ for line in lines:
6487
+ stripped = line.strip()
6488
+ if stripped.startswith('#'):
6489
+ return escape(stripped.lstrip('#').strip())
6490
+ except Exception:
6491
+ ...
6492
+ return ''
6493
+
6359
6494
  @staticmethod
6360
6495
  def display_toc_this(pat='*',text='**Jupyter notebooks**',path='.',ext='ipynb',showext=False):
6361
-
6362
- '''In a jupyter notebook this function displays a clickable table of content in the folder pat with name in path'''
6496
+
6497
+ '''In a jupyter notebook this function displays a clickable table of content in the folder pat with name in path
6498
+
6499
+ For notebooks the first markdown header in the first cell is shown beside the name'''
6363
6500
 
6364
6501
  from IPython.display import display, Markdown, HTML
6365
6502
  from pathlib import Path
@@ -6367,10 +6504,18 @@ class Display_Mixin():
6367
6504
  display(Markdown(text))
6368
6505
  dir = Path(path)
6369
6506
  # print(dir,':')
6507
+ rows = []
6370
6508
  for fname in sorted(dir.glob(pat+'.'+ext)):
6371
6509
  name = fname.name if showext else fname.name.split('.')[0]
6510
+ title = Display_Mixin._notebook_title(fname) if ext == 'ipynb' else ''
6511
+ rows.append(
6512
+ f'<tr><td style="text-align:left;padding:2px 24px 2px 12px;white-space:nowrap">'
6513
+ f'<a href="{fname}" target="_blank">{name}</a></td>'
6514
+ f'<td style="text-align:left;padding:2px 12px">{title}</td></tr>')
6515
+ if rows:
6372
6516
  display(HTML(
6373
- f'&nbsp; &nbsp; <a href="{fname}" target="_blank">{name}</a>'))
6517
+ '<table style="border-collapse:collapse;border:none">'
6518
+ + ''.join(rows) + '</table>'))
6374
6519
 
6375
6520
 
6376
6521
  @staticmethod
@@ -6423,13 +6568,25 @@ class Display_Mixin():
6423
6568
  } catch (e) { /* ignore */ }
6424
6569
  """))
6425
6570
 
6426
- # Notebook 7 / JupyterLab: remove the scrolled-output max-height
6427
- # and the scrolled modifier wherever it is applied.
6571
+ # Notebook 7 / JupyterLab: the height is set on the output area of the
6572
+ # cell: .jp-mod-outputsScrolled .jp-Cell-outputArea {max-height: 24em}
6573
+ # height:auto also undoes a height the user has dragged in place, as an
6574
+ # important declaration wins over an inline style
6428
6575
  display(HTML("""
6429
6576
  <style id="mf-scroll-off">
6577
+ .jp-mod-outputsScrolled .jp-Cell-outputArea {
6578
+ max-height: none !important;
6579
+ height: auto !important;
6580
+ overflow-y: visible !important;
6581
+ resize: none !important;
6582
+ }
6430
6583
  .jp-OutputArea-output.jp-OutputArea-output { max-height: none !important; }
6431
- .jp-mod-outputsScrolled .jp-OutputArea-child {
6584
+ /* Classic Notebook (<=6) sets height, not max-height, on output_scroll */
6585
+ .output_scroll {
6586
+ height: auto !important;
6432
6587
  max-height: none !important;
6588
+ overflow: visible !important;
6589
+ box-shadow: none !important;
6433
6590
  }
6434
6591
  </style>
6435
6592
  """))
@@ -6454,11 +6611,12 @@ class Display_Mixin():
6454
6611
  } catch (e) { /* ignore */ }
6455
6612
  """))
6456
6613
 
6457
- # Remove the override stylesheet we injected above, if present.
6614
+ # Remove the override stylesheets we injected above, there is one for
6615
+ # each cell where scroll_off has been run
6458
6616
  display(Javascript("""
6459
6617
  try {
6460
- var s = document.getElementById('mf-scroll-off');
6461
- if (s) { s.parentNode.removeChild(s); }
6618
+ document.querySelectorAll('#mf-scroll-off').forEach(
6619
+ function (s) { s.parentNode.removeChild(s); });
6462
6620
  } catch (e) { /* ignore */ }
6463
6621
  """))
6464
6622
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.80
3
+ Version: 2.82
4
4
  Summary: A tool to solve and manage dynamic economic and financial models
5
5
  Author-email: Ib Hansen <Ib.Hansen.Iv@gmail.com>
6
6
  License-Expression: MIT
@@ -20,22 +20,26 @@ Requires-Dist: numpy
20
20
  Requires-Dist: networkx
21
21
  Requires-Dist: tqdm
22
22
  Requires-Dist: scipy
23
- Requires-Dist: numexpr
24
- Requires-Dist: bottleneck
25
23
  Requires-Dist: XlsxWriter
26
24
  Requires-Dist: pyarrow
27
25
  Requires-Dist: tabulate
28
26
  Requires-Dist: openpyxl
29
27
  Requires-Dist: xarray
30
- Requires-Dist: cvxopt
31
- Requires-Dist: numba
32
- Requires-Dist: dash
33
- Requires-Dist: dash-bootstrap-components
34
- Requires-Dist: dash-interactive-graphviz
35
- Requires-Dist: lmfit
36
- Requires-Dist: jupyter
28
+ Requires-Dist: jinja2
29
+ Requires-Dist: ipython
37
30
  Requires-Dist: ipywidgets
38
31
  Requires-Dist: ipydatagrid
32
+ Requires-Dist: lmfit
33
+ Requires-Dist: statsmodels
34
+ Requires-Dist: numba; sys_platform != "emscripten"
35
+ Requires-Dist: cvxopt; sys_platform != "emscripten"
36
+ Requires-Dist: numexpr; sys_platform != "emscripten"
37
+ Requires-Dist: bottleneck; sys_platform != "emscripten"
38
+ Requires-Dist: dash; sys_platform != "emscripten"
39
+ Requires-Dist: dash-bootstrap-components; sys_platform != "emscripten"
40
+ Requires-Dist: dash-interactive-graphviz; sys_platform != "emscripten"
41
+ Requires-Dist: plotly; sys_platform != "emscripten"
42
+ Requires-Dist: jupyter; sys_platform != "emscripten"
39
43
  Provides-Extra: shiny
40
44
  Requires-Dist: shiny; extra == "shiny"
41
45
  Requires-Dist: shinywidgets; extra == "shiny"
@@ -6,22 +6,28 @@ numpy
6
6
  networkx
7
7
  tqdm
8
8
  scipy
9
- numexpr
10
- bottleneck
11
9
  XlsxWriter
12
10
  pyarrow
13
11
  tabulate
14
12
  openpyxl
15
13
  xarray
16
- cvxopt
14
+ jinja2
15
+ ipython
16
+ ipywidgets
17
+ ipydatagrid
18
+ lmfit
19
+ statsmodels
20
+
21
+ [:sys_platform != "emscripten"]
17
22
  numba
23
+ cvxopt
24
+ numexpr
25
+ bottleneck
18
26
  dash
19
27
  dash-bootstrap-components
20
28
  dash-interactive-graphviz
21
- lmfit
29
+ plotly
22
30
  jupyter
23
- ipywidgets
24
- ipydatagrid
25
31
 
26
32
  [shiny]
27
33
  shiny
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modelflowib" # PyPI distribution name
7
- version = "2.80" # OVERWRITTEN at build time from MF_VERSION
7
+ version = "2.82" # OVERWRITTEN at build time from MF_VERSION
8
8
  # (set_pyproject_version.py / make_pip.py -v)
9
9
  description = "A tool to solve and manage dynamic economic and financial models"
10
10
  readme = "README.md"
@@ -18,8 +18,15 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  ]
20
20
 
21
- # PyPI dependencies (pip names). Carried over from the previous make_pip.py.
21
+ # PyPI dependencies (pip names).
22
+ #
23
+ # The ones marked `sys_platform != "emscripten"` have no wasm build, or need a web
24
+ # server, so they can not be installed in a browser (JupyterLite/Pyodide). pip on a
25
+ # desktop installs them as before; micropip in the browser skips them. That makes
26
+ # `piplite.install("modelflowib")` work *with* its dependencies, so a notebook in
27
+ # JupyterLite needs no list of packages of its own.
22
28
  dependencies = [
29
+ # --- everywhere ---
23
30
  "pandas",
24
31
  "matplotlib",
25
32
  "seaborn",
@@ -28,22 +35,27 @@ dependencies = [
28
35
  "networkx",
29
36
  "tqdm",
30
37
  "scipy",
31
- "numexpr",
32
- "bottleneck",
33
38
  "XlsxWriter",
34
39
  "pyarrow",
35
40
  "tabulate",
36
41
  "openpyxl",
37
42
  "xarray",
38
- "cvxopt",
39
- "numba",
40
- "dash",
41
- "dash-bootstrap-components",
42
- "dash-interactive-graphviz",
43
- "lmfit",
44
- "jupyter",
43
+ "jinja2", # pandas .style, used by the report and vis tables
44
+ "ipython", # IPython.display, imported by most modules
45
45
  "ipywidgets",
46
46
  "ipydatagrid",
47
+ "lmfit", # modelestimation, modelestimator_new
48
+ "statsmodels", # modelestimation, modelestimator_new
49
+ # --- not in a browser ---
50
+ "numba; sys_platform != 'emscripten'",
51
+ "cvxopt; sys_platform != 'emscripten'",
52
+ "numexpr; sys_platform != 'emscripten'", # pandas accelerators
53
+ "bottleneck; sys_platform != 'emscripten'",
54
+ "dash; sys_platform != 'emscripten'", # needs a web server
55
+ "dash-bootstrap-components; sys_platform != 'emscripten'",
56
+ "dash-interactive-graphviz; sys_platform != 'emscripten'",
57
+ "plotly; sys_platform != 'emscripten'", # modeldashsidebar
58
+ "jupyter; sys_platform != 'emscripten'", # JupyterLite is the notebook there
47
59
  ]
48
60
 
49
61
  [project.optional-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes