modelflowib 2.85__tar.gz → 2.87__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 (53) hide show
  1. {modelflowib-2.85 → modelflowib-2.87}/PKG-INFO +2 -1
  2. {modelflowib-2.85 → modelflowib-2.87}/modelclass.py +72 -21
  3. {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/PKG-INFO +2 -1
  4. {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/requires.txt +1 -0
  5. {modelflowib-2.85 → modelflowib-2.87}/modelreport.py +181 -66
  6. {modelflowib-2.85 → modelflowib-2.87}/modelsolver_ng.py +36 -3
  7. {modelflowib-2.85 → modelflowib-2.87}/modelwidget.py +50 -13
  8. {modelflowib-2.85 → modelflowib-2.87}/pyproject.toml +2 -1
  9. {modelflowib-2.85 → modelflowib-2.87}/README.md +0 -0
  10. {modelflowib-2.85 → modelflowib-2.87}/license.md +0 -0
  11. {modelflowib-2.85 → modelflowib-2.87}/modelBLfunk.py +0 -0
  12. {modelflowib-2.85 → modelflowib-2.87}/model_Excel.py +0 -0
  13. {modelflowib-2.85 → modelflowib-2.87}/model_cvx.py +0 -0
  14. {modelflowib-2.85 → modelflowib-2.87}/model_dynare.py +0 -0
  15. {modelflowib-2.85 → modelflowib-2.87}/model_financial_stability.py +0 -0
  16. {modelflowib-2.85 → modelflowib-2.87}/model_latex.py +0 -0
  17. {modelflowib-2.85 → modelflowib-2.87}/model_latex_class.py +0 -0
  18. {modelflowib-2.85 → modelflowib-2.87}/model_parquet_mixin.py +0 -0
  19. {modelflowib-2.85 → modelflowib-2.87}/modelcausality.py +0 -0
  20. {modelflowib-2.85 → modelflowib-2.87}/modelconstruct.py +0 -0
  21. {modelflowib-2.85 → modelflowib-2.87}/modelconstruct_estimation.py +0 -0
  22. {modelflowib-2.85 → modelflowib-2.87}/modeldash.py +0 -0
  23. {modelflowib-2.85 → modelflowib-2.87}/modeldashboot.py +0 -0
  24. {modelflowib-2.85 → modelflowib-2.87}/modeldashsidebar.py +0 -0
  25. {modelflowib-2.85 → modelflowib-2.87}/modeldekom.py +0 -0
  26. {modelflowib-2.85 → modelflowib-2.87}/modeldiff.py +0 -0
  27. {modelflowib-2.85 → modelflowib-2.87}/modeldisplay.py +0 -0
  28. {modelflowib-2.85 → modelflowib-2.87}/modelestimation.py +0 -0
  29. {modelflowib-2.85 → modelflowib-2.87}/modelestimator_new.py +0 -0
  30. {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/SOURCES.txt +0 -0
  31. {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/dependency_links.txt +0 -0
  32. {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/top_level.txt +0 -0
  33. {modelflowib-2.85 → modelflowib-2.87}/modelgrab.py +0 -0
  34. {modelflowib-2.85 → modelflowib-2.87}/modelgrabgdx.py +0 -0
  35. {modelflowib-2.85 → modelflowib-2.87}/modelgrabwf2.py +0 -0
  36. {modelflowib-2.85 → modelflowib-2.87}/modelhelp.py +0 -0
  37. {modelflowib-2.85 → modelflowib-2.87}/modelhtml.py +0 -0
  38. {modelflowib-2.85 → modelflowib-2.87}/modelinput_shiny.py +0 -0
  39. {modelflowib-2.85 → modelflowib-2.87}/modelinvert.py +0 -0
  40. {modelflowib-2.85 → modelflowib-2.87}/modeljupyter.py +0 -0
  41. {modelflowib-2.85 → modelflowib-2.87}/modeljupytermagic.py +0 -0
  42. {modelflowib-2.85 → modelflowib-2.87}/modelmacrograb.py +0 -0
  43. {modelflowib-2.85 → modelflowib-2.87}/modelmanipulation.py +0 -0
  44. {modelflowib-2.85 → modelflowib-2.87}/modelmf.py +0 -0
  45. {modelflowib-2.85 → modelflowib-2.87}/modelnet.py +0 -0
  46. {modelflowib-2.85 → modelflowib-2.87}/modelnewton.py +0 -0
  47. {modelflowib-2.85 → modelflowib-2.87}/modelnormalize.py +0 -0
  48. {modelflowib-2.85 → modelflowib-2.87}/modelpattern.py +0 -0
  49. {modelflowib-2.85 → modelflowib-2.87}/modeluserfunk.py +0 -0
  50. {modelflowib-2.85 → modelflowib-2.87}/modelvis.py +0 -0
  51. {modelflowib-2.85 → modelflowib-2.87}/modelwidget_core.py +0 -0
  52. {modelflowib-2.85 → modelflowib-2.87}/modelwidget_input.py +0 -0
  53. {modelflowib-2.85 → modelflowib-2.87}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.85
3
+ Version: 2.87
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
@@ -42,6 +42,7 @@ Requires-Dist: dash-interactive-graphviz; sys_platform != "emscripten"
42
42
  Requires-Dist: plotly; sys_platform != "emscripten"
43
43
  Requires-Dist: pyarrow; sys_platform != "emscripten"
44
44
  Requires-Dist: jupyter; sys_platform != "emscripten"
45
+ Requires-Dist: jupyterlab-myst; sys_platform != "emscripten"
45
46
  Provides-Extra: shiny
46
47
  Requires-Dist: shiny; extra == "shiny"
47
48
  Requires-Dist: shinywidgets; extra == "shiny"
@@ -2390,7 +2390,7 @@ class Model_help_Mixin():
2390
2390
  f'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{blanks} <a href="{githubname}" target="_blank">{name}</a>'))
2391
2391
  else:
2392
2392
  display(HTML(
2393
- f'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{blanks} <a href="{notebook}" target="_blank">{name}</a>'))
2393
+ f'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{blanks} <a href="{notebook.as_posix()}" target="_blank">{name}</a>'))
2394
2394
 
2395
2395
 
2396
2396
 
@@ -5246,6 +5246,11 @@ class Graph_Draw_Mixin():
5246
5246
 
5247
5247
  class Display_Mixin():
5248
5248
 
5249
+ #: True: the result viewers (mpak['...'] and .rplot()) are shown as plain html tabs and
5250
+ #: collapsible sections instead of ipywidgets. They then also display where ipywidgets
5251
+ #: containers don't, like RISE slides. Set it for all models with model.plain_html = True
5252
+ plain_html = False
5253
+
5249
5254
  def vis(self, *args, **kwargs):
5250
5255
  ''' Visualize the data of this model instance
5251
5256
  if the user has another vis class she can place it in _vis, then that will be used'''
@@ -6454,7 +6459,7 @@ class Display_Mixin():
6454
6459
  title = Display_Mixin._notebook_title(notebook)
6455
6460
  rows.append(
6456
6461
  f'<tr><td style="text-align:left;padding:2px 24px 2px {indent+2}em;white-space:nowrap">'
6457
- f'<a href="{notebook}" target="_blank">{name}</a></td>'
6462
+ f'<a href="{notebook.as_posix()}" target="_blank">{name}</a></td>'
6458
6463
  f'<td style="text-align:left;padding:2px 12px">{title}</td></tr>')
6459
6464
 
6460
6465
  # try:
@@ -6514,7 +6519,7 @@ class Display_Mixin():
6514
6519
  title = Display_Mixin._notebook_title(fname) if ext == 'ipynb' else ''
6515
6520
  rows.append(
6516
6521
  f'<tr><td style="text-align:left;padding:2px 24px 2px 12px;white-space:nowrap">'
6517
- f'<a href="{fname}" target="_blank">{name}</a></td>'
6522
+ f'<a href="{fname.as_posix()}" target="_blank">{name}</a></td>'
6518
6523
  f'<td style="text-align:left;padding:2px 12px">{title}</td></tr>')
6519
6524
  if rows:
6520
6525
  display(HTML(
@@ -6843,11 +6848,20 @@ class Json_Mixin():
6843
6848
  base.index = base_dates
6844
6849
  return base, current_dates
6845
6850
 
6851
+ def any_case(p):
6852
+ '''p, or else a file in the same folder whose name only differs in case
6853
+ (Windows ignores case, Linux and the browser (JupyterLite) do not)'''
6854
+ if p.exists() or not p.parent.is_dir():
6855
+ return p
6856
+ return next((f for f in p.parent.iterdir() if f.name.lower() == p.name.lower()), p)
6857
+
6846
6858
  pinfile = Path(nname:=infile.replace('\\','/'))
6847
6859
  if not pinfile.suffix:
6848
- pinfile = pinfile.with_suffix(cls.data_suffix)
6849
- if not pinfile.exists() and (nodata := pinfile.with_suffix(cls.nodata_suffix)).exists():
6860
+ pinfile = any_case(pinfile.with_suffix(cls.data_suffix))
6861
+ if not pinfile.exists() and (nodata := any_case(pinfile.with_suffix(cls.nodata_suffix))).exists():
6850
6862
  pinfile = nodata
6863
+ else:
6864
+ pinfile = any_case(pinfile)
6851
6865
 
6852
6866
  def read_file(pinfile):
6853
6867
  try:
@@ -7201,9 +7215,14 @@ class Solver_Mixin():
7201
7215
  if self.previousbase and hasattr(self, 'lastdf'):
7202
7216
  self.basedf = self.lastdf.copy(deep=True)
7203
7217
 
7218
+ # normalized and fully implicit models get their own solvers; a hybrid
7219
+ # model (normalized and ___RES equations mixed) needs a unified Newton
7220
+ # solver, which handles both residual conventions in one system
7204
7221
  if self.maxlead >= 1:
7205
7222
  if self.normalized:
7206
7223
  solverguess = 'newtonstack'
7224
+ elif self.hybrid:
7225
+ solverguess = 'newtonstack_ng'
7207
7226
  else:
7208
7227
  solverguess = 'newtonstack_un_normalized'
7209
7228
  else:
@@ -7212,6 +7231,8 @@ class Solver_Mixin():
7212
7231
  solverguess = 'xgenr'
7213
7232
  else:
7214
7233
  solverguess = 'sim'
7234
+ elif self.hybrid:
7235
+ solverguess = 'newton_ng'
7215
7236
  else:
7216
7237
  solverguess = 'newton_un_normalized'
7217
7238
 
@@ -7376,7 +7397,10 @@ class Solver_Mixin():
7376
7397
  for i in [j for j in self.allvar.keys() if self.allvar[j]['matrix']]:
7377
7398
  # Make sure columns with matrixes are of this type
7378
7399
  databank.loc[:, i] = databank.loc[:, i].astype('O')
7379
- newdata = True
7400
+ # a frame that only lacked model variables (e.g. the X___RES columns
7401
+ # of an implicit model) ends up with the column layout the solver
7402
+ # code was generated for -- that is not new data
7403
+ newdata = not self.eqcolumns(self.genrcolumns, databank.columns)
7380
7404
  else:
7381
7405
  newdata = False
7382
7406
  return newdata, databank
@@ -8961,7 +8985,15 @@ class Solver_Mixin():
8961
8985
  - Equation list (rows): endovar (may contain both VAR and VAR___RES)
8962
8986
  - Unknowns (columns): declared_endo_list (base VAR names)
8963
8987
  - is_residual_eq: mask marking rows using residual form G(y,x)
8964
-
8988
+
8989
+ Which equations are Newton rows:
8990
+ - normalized model: the simultaneous core (coreorder); the recursive
8991
+ prolog / epilog are evaluated in place before / after the Newton
8992
+ iterations of each period
8993
+ - implicit or hybrid model: every equation (solveorder) -- implicit
8994
+ equations can sit outside the core of the dependency graph, which
8995
+ does not see dependencies on a declared unknown
8996
+
8965
8997
  Newton step:
8966
8998
  residual_i = {
8967
8999
  G_i(y,x) if is_residual_eq[i]
@@ -9020,10 +9052,20 @@ class Solver_Mixin():
9020
9052
  # ======================================================================
9021
9053
  # 1) Setup derivative solver / Jacobian factorization
9022
9054
  # ======================================================================
9023
- if not hasattr(self, 'newton_diff_implicit') or newton_reset:
9055
+ # Normalized model: Newton on the simultaneous core only; the recursive
9056
+ # prolog / epilog are evaluated in place once before / after the Newton
9057
+ # iterations of each period (as legacy newton).
9058
+ # Implicit or hybrid model: the whole model is one Newton system. The
9059
+ # dependency graph cannot see dependencies on a declared unknown, so
9060
+ # implicit equations can end up in the prolog / epilog -- they must be
9061
+ # Newton rows, and so must the normalized equations around them.
9062
+ core_only = bool(self.normalized and self.use_preorder and len(self.coreorder))
9063
+ newton_endovar = list(self.coreorder if core_only else self.solveorder)
9064
+
9065
+ if (not hasattr(self, 'newton_diff_implicit') or newton_reset
9066
+ or list(self.newton_diff_implicit.endovar) != newton_endovar):
9024
9067
  # --- Determine equation list (rows of Jacobian) --------------------
9025
- endovar = (self.coreorder if hasattr(self, 'coreorder') and len(self.coreorder)
9026
- else self.solveorder)
9068
+ endovar = newton_endovar
9027
9069
 
9028
9070
  # Declared endogenous list (true unknowns / columns of Jacobian)
9029
9071
  self.declared_endo_list = [ v[:-6] if v.endswith('___RES') else v
@@ -9113,9 +9155,12 @@ class Solver_Mixin():
9113
9155
  )
9114
9156
 
9115
9157
  # --------------------------------------------------------------
9116
- # Recursive "pre" block: update lags, identities, etc.
9158
+ # Recursive "pre" block (normalized model): solved exactly, in
9159
+ # place, before the Newton iterations on the core
9117
9160
  # --------------------------------------------------------------
9118
-
9161
+ if core_only:
9162
+ self.pronew2d(values, values, row, alfa)
9163
+
9119
9164
  # --------------------------------------------------------------
9120
9165
  # Newton iterations for this period
9121
9166
  # --------------------------------------------------------------
@@ -9218,14 +9263,15 @@ class Solver_Mixin():
9218
9263
  values[row, newton_col_unknown] = (
9219
9264
  y_old - base_damp * update
9220
9265
  )
9221
- values[row,newton_col_residual] = outvalues[row,newton_col_residual] # TO KEEP THE RES UPDATED
9222
-
9266
+ values[row,newton_col_residual] = outvalues[row,newton_col_residual] # TO KEEP THE RES UPDATED
9267
+
9223
9268
  ittotal += 1
9224
-
9225
-
9226
-
9227
-
9228
-
9269
+
9270
+ # Recursive "post" block (normalized model): the epilog, in
9271
+ # place, from the converged core
9272
+ if core_only:
9273
+ self.epinew2d(values, values, row, alfa)
9274
+
9229
9275
  if newton_conv > newton_absconv:
9230
9276
  print(
9231
9277
  f'{self.periode} not converged in '
@@ -9930,8 +9976,13 @@ class Report_Mixin:
9930
9976
  timeslice (List[int]): Time slice for data display, empty by default.
9931
9977
  max_cols (int): Maximum columns when displayed as a string, faulting to the system wide setting.
9932
9978
  last_cols (int): Specifies the number of last columns to include in a display slice, particularly in Latex.
9933
- col_desc (str): text centered on columns
9934
-
9979
+ col_desc (str): text centered on columns
9980
+ scenarios (str): Empty uses basedf/lastdf (the default). '*' uses all keep_solutions,
9981
+ exact names are separated by '|', wildcard patterns by spaces. Impact datatypes compare
9982
+ with the first selected scenario.
9983
+ scenario_layout (str): 'headline' (default) shows the table once per scenario under a
9984
+ headline row ('scenario vs reference' for impacts); 'suffix' appends ' — scenario' to row labels.
9985
+
9935
9986
  Returns:
9936
9987
  DisplayVarTableDef: Configured table definition object ready for rendering, which includes detailed specifications
9937
9988
  such as units and type of transformation based on the datatype.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.85
3
+ Version: 2.87
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
@@ -42,6 +42,7 @@ Requires-Dist: dash-interactive-graphviz; sys_platform != "emscripten"
42
42
  Requires-Dist: plotly; sys_platform != "emscripten"
43
43
  Requires-Dist: pyarrow; sys_platform != "emscripten"
44
44
  Requires-Dist: jupyter; sys_platform != "emscripten"
45
+ Requires-Dist: jupyterlab-myst; sys_platform != "emscripten"
45
46
  Provides-Extra: shiny
46
47
  Requires-Dist: shiny; extra == "shiny"
47
48
  Requires-Dist: shinywidgets; extra == "shiny"
@@ -30,6 +30,7 @@ dash-interactive-graphviz
30
30
  plotly
31
31
  pyarrow
32
32
  jupyter
33
+ jupyterlab-myst
33
34
 
34
35
  [shiny]
35
36
  shiny
@@ -60,8 +60,9 @@ import pandas as pd
60
60
  import matplotlib.pyplot as plt
61
61
  import matplotlib as mpl
62
62
  import seaborn as sns
63
- import fnmatch
64
- import re
63
+ import fnmatch
64
+ import html
65
+ import re
65
66
  from matplotlib import dates
66
67
  import matplotlib.ticker as ticker
67
68
  import matplotlib.gridspec as gridspec
@@ -253,7 +254,9 @@ class Options:
253
254
  size (tuple): Tuple specifying the figure size (width, height). Default is (10, 6).
254
255
  legend (bool): If True, display legend in plots. Default is True.
255
256
  transpose (bool): If True, transpose the data when displaying. Default is False.
256
- scenarios (str): Text specifying the scenarios for the display. Default is an empty string. if Empty use basedf/lastdf
257
+ scenarios (str): Text specifying the scenarios for the display. Default is an empty string. if Empty use basedf/lastdf
258
+ scenario_layout (str): Scenario tables only. 'headline' (default) groups the whole table under one
259
+ headline row per scenario; 'suffix' appends ' — scenario' to each row label. Transposed tables use 'suffix'.
257
260
  smpl (tuple): Tuple specifying start and end periods. Default is ('', '').
258
261
  landscape (bool): If True, the table will be displayed in landscape mode. Default is False.
259
262
  latex_text (str): Text for a LaTeX output. Default is an empty string.
@@ -288,6 +291,7 @@ class Options:
288
291
  legend: bool = True
289
292
  transpose : bool = False
290
293
  scenarios : str =''
294
+ scenario_layout : str = 'headline'
291
295
  smpl : tuple = ('','')
292
296
  landscape : bool = False
293
297
 
@@ -858,48 +862,123 @@ class DisplayVarTableDef(DisplayDef):
858
862
  def __post_init__(self):
859
863
  super().__post_init__() # Call the parent class's __post_init__
860
864
 
861
-
862
- with self.mmodel.set_smpl(*self.options.smpl):
863
- self.dfs = [self.make_var_df(line).astype('float') for line in self.lines ]
864
- self.report_smpl = self.get_report_smpl
865
-
865
+ self.base_last = self.options.scenarios in ('', 'base_last')
866
+ self.unitline = ''
867
+ if not self.base_last:
868
+ # keepswitch falls back to all solutions on an unmatched selector.
869
+ # A report must not silently display a different set of scenarios.
870
+ selector = self.options.scenarios
871
+ sep = ' ' if '*' in selector or '?' in selector else '|'
872
+ patterns = selector.split(sep)
873
+ if not self.mmodel.keep_solutions or any(
874
+ not fnmatch.filter(self.mmodel.keep_solutions, pat)
875
+ for pat in patterns
876
+ ):
877
+ raise ValueError(f'No scenarios match part or all of: {selector!r}')
878
+
879
+ with self.mmodel.keepswitch(scenarios=self.options.scenarios,
880
+ base_last=self.base_last):
881
+ self.scenario_names = list(self.mmodel.keep_solutions)
882
+ if not self.base_last and len(self.scenario_names) < 2 and any(
883
+ line.diftype in ('dif', 'difpct') and not line.textlinetype
884
+ for line in self.lines
885
+ ):
886
+ raise ValueError('An impact table needs a reference scenario and at least one alternative.')
887
+ with self.mmodel.set_smpl(*self.options.smpl):
888
+ results = [self.make_var_df(line) for line in self.lines]
889
+ self.report_smpl = self.get_report_smpl
890
+
891
+ self.headline = (not self.base_last and not self.options.transpose
892
+ and self.options.scenario_layout == 'headline')
893
+ # display_lines/dfs are the rendered rows: one frame per entry, aligned
894
+ # with the Line that controls its formatting (textline, decimals ...).
895
+ if self.headline:
896
+ self.display_lines, self.dfs = self.make_headline_rows(results)
897
+ else:
898
+ self.display_lines = self.lines
899
+ self.dfs = [self.make_suffix_rows(line, result)
900
+ for line, result in zip(self.lines, results)]
901
+
866
902
  if self.options.transpose:
867
- self.df = self.dfs[-1].T
868
- ...
903
+ if self.base_last:
904
+ self.df = self.dfs[-1].T
905
+ self.column_decimals = [self.lines[-1].dec] * len(self.df.columns)
906
+ else:
907
+ numeric = [(line, df) for line, df in zip(self.lines, self.dfs)
908
+ if line.textlinetype != 'textline']
909
+ self.df = (pd.concat([df for line, df in numeric]).T if numeric
910
+ else pd.DataFrame(index=self.dfs[0].columns))
911
+ self.column_decimals = [line.dec for line, df in numeric for _ in df.index]
869
912
  else:
870
913
  self.df = pd.concat( self.dfs )
871
914
  # assert 1==2
872
915
  return
873
916
 
874
- def make_var_df(self, line):
875
- showtype = line.showtype
876
- diftype = line.diftype
877
-
878
- with self.mmodel.keepswitch(switch=True):
879
-
880
- # Pre-process for cases that use linevars and linedes
881
- if line.textlinetype in ['textline']:
882
- linedf = pd.DataFrame(np.nan , index=self.mmodel.current_per, columns=[line.centertext]).T
883
- self.unitline = self.lines[0].centertext
884
- else:
885
- def getline(start_ofset= 0,**kvargs):
886
- locallinedfdict = self.mmodel.keep_get_plotdict_new(pat=line.pat,showtype=showtype,
887
- diftype = diftype,by_var=False)
888
- if diftype == 'basedf':
889
- locallinedf = next(iter((locallinedfdict.values()))).T
890
- else:
891
- locallinedf = next(iter(reversed(locallinedfdict.values()))).T
892
-
893
- return locallinedf.loc[:,self.mmodel.current_per]
894
-
895
- # print(line.mul)
896
-
897
-
898
-
899
- linedf = getline() * line.mul
900
- linedf = self.get_rowdes(linedf,line)
901
-
902
- return(linedf)
917
+ def make_var_df(self, line):
918
+ '''Returns a frame for a textline, else a dict {scenario: frame} with plain row labels'''
919
+ if line.textlinetype == 'textline':
920
+ self.unitline = self.lines[0].centertext
921
+ return pd.DataFrame(np.nan, index=[line.centertext],
922
+ columns=self.mmodel.current_per)
923
+
924
+ results = self.mmodel.keep_get_plotdict_new(
925
+ pat=line.pat, showtype=line.showtype, diftype=line.diftype, by_var=False)
926
+ if self.base_last or line.diftype in ('basedf', 'lastdf'):
927
+ key = next(iter(results)) if line.diftype == 'basedf' else next(reversed(results))
928
+ results = {key: results[key]}
929
+
930
+ return {scenario: self.get_rowdes(df.loc[self.mmodel.current_per].T * line.mul, line).astype('float')
931
+ for scenario, df in results.items()}
932
+
933
+ def make_suffix_rows(self, line, result):
934
+ '''One frame per Line: variable first, scenarios beneath it, labelled "label — scenario"'''
935
+ if line.textlinetype == 'textline':
936
+ return result.astype('float')
937
+ blocks = list(result.values())
938
+ if not self.base_last:
939
+ blocks = [block.set_axis([f'{label} — {scenario}' for label in block.index])
940
+ for scenario, block in result.items()]
941
+ combined = pd.concat(blocks)
942
+ nvars, nscenarios = len(blocks[0]), len(blocks)
943
+ return combined.iloc[[s * nvars + v for v in range(nvars)
944
+ for s in range(nscenarios)]]
945
+
946
+ def make_headline_rows(self, results):
947
+ '''The whole table is repeated once per scenario under a headline row.
948
+
949
+ Leading textlines (heading, unit line) are shown once at the top. A Line
950
+ appears in a scenario block only if it has data for that scenario: base
951
+ datatypes for the reference, impacts for the alternatives. A textline
952
+ inside the table is dropped from a block when the lines it heads are.
953
+ A block with only impact lines is headed "scenario vs reference".
954
+ '''
955
+ first_numeric = next((i for i, line in enumerate(self.lines)
956
+ if line.textlinetype != 'textline'), len(self.lines))
957
+ rows = [(line, result.astype('float')) for line, result in
958
+ zip(self.lines[:first_numeric], results[:first_numeric])]
959
+ reference = self.scenario_names[0]
960
+
961
+ for scenario in self.scenario_names:
962
+ block, pending, after_numeric, impact = [], [], False, set()
963
+ for line, result in zip(self.lines[first_numeric:], results[first_numeric:]):
964
+ if line.textlinetype == 'textline':
965
+ if after_numeric: # a new section starts
966
+ pending, after_numeric = [], False
967
+ pending.append((line, result.astype('float')))
968
+ continue
969
+ after_numeric = True
970
+ if scenario in result:
971
+ block += pending + [(line, result[scenario])]
972
+ pending = []
973
+ impact.add(line.diftype in ('dif', 'difpct'))
974
+ if block:
975
+ headline = f'{scenario} vs {reference}' if impact == {True} else scenario
976
+ headline_line = Line(textlinetype='scenarioline', centertext=headline)
977
+ rows.append((headline_line, pd.DataFrame(np.nan, index=[headline],
978
+ columns=self.mmodel.current_per)))
979
+ rows += block
980
+
981
+ return [line for line, df in rows], [df for line, df in rows]
903
982
 
904
983
 
905
984
 
@@ -925,36 +1004,43 @@ class DisplayVarTableDef(DisplayDef):
925
1004
  @property
926
1005
  def df_str(self):
927
1006
  width = self.options.width
928
- # df = self.df.copy( )
1007
+ fmt = lambda x, dec: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip()
1008
+ # The frame is built in one go: assigning rows into a string frame with
1009
+ # iloc fails in pandas 3 (python string storage) when it has one column.
929
1010
  if self.options.transpose:
930
- dec = self.lines[-1].dec
931
- thisdf = self.df.loc[self.timeslice,:] if self.timeslice else self.df
932
- df_char = pd.DataFrame(' ', index=thisdf.index, columns=thisdf.columns)
933
- # for c in thisdf.columns:
934
- # df_char.loc[:,c] = thisdf.loc[:,c].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
935
- for i,_ in enumerate(thisdf.index):
936
- df_char.iloc[i] = self.df.iloc[i].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
1011
+ thisdf = self.df.loc[self.timeslice,:] if self.timeslice else self.df
1012
+ values = [[fmt(x, dec) for x, dec in zip(row, self.column_decimals)]
1013
+ for row in thisdf.to_numpy()]
937
1014
  else:
938
- df_char = pd.DataFrame(' ', index=self.df.index, columns=self.df.columns)
939
-
940
- format_decimal = [ line.dec for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
941
- for i, dec in enumerate(format_decimal):
942
- df_char.iloc[i] = self.df.iloc[i].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
943
-
944
- return df_char
1015
+ thisdf = self.df
1016
+ format_decimal = [ line.dec for line,df in zip(self.display_lines,self.dfs) for row in range(len(df))]
1017
+ values = [[fmt(x, dec) for x in row]
1018
+ for row, dec in zip(thisdf.to_numpy(), format_decimal)]
1019
+ df_char = pd.DataFrame(values, index=thisdf.index, columns=thisdf.columns, dtype=object)
1020
+ return df_char
945
1021
 
946
1022
 
947
1023
 
948
1024
  @property
949
1025
  def df_str_disp(self):
950
- center = [ (line.textlinetype == 'textline' and line.centertext !='' ) for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
1026
+ center = [ (line.textlinetype == 'textline' and line.centertext !='' ) for line,df in zip(self.display_lines,self.dfs) for row in range(len(df))]
951
1027
  center_index = [index+1 for index, value in enumerate(center) if value]
952
1028
 
953
1029
  width = self.options.width
954
- thisdf = self.df_str.loc[:,self.timeslice] if self.timeslice else self.df_str
955
-
1030
+ thisdf = self.df_str.loc[:,self.timeslice] if self.timeslice else self.df_str
1031
+ # Scenario headlines are written across the row afterwards, so a long
1032
+ # name does not widen the label column.
1033
+ rowtypes = [line.textlinetype for line, df in zip(self.display_lines, self.dfs) for row in range(len(df))]
1034
+ headline_index = [index for index, rowtype in enumerate(rowtypes) if rowtype == 'scenarioline']
1035
+ if headline_index:
1036
+ thisdf = thisdf.copy()
1037
+ thisdf.index = ['' if rowtype == 'scenarioline' else label
1038
+ for label, rowtype in zip(thisdf.index, rowtypes)]
1039
+
956
1040
  rawdata = thisdf.to_string(max_cols= self.options.max_cols).split('\n')
957
1041
  data = center_title_under_years(rawdata,center_index)
1042
+ for index in headline_index:
1043
+ data[index+1] = self.df_str.index[index]
958
1044
  # print(*data,sep='\n')
959
1045
  # rawdata[0],rawdata[1] = rawdata[1],rawdata[0]
960
1046
  out = '\n'.join(data)
@@ -1026,11 +1112,14 @@ class DisplayVarTableDef(DisplayDef):
1026
1112
  def tab_to_html(i,df,line):
1027
1113
  nonlocal endhtml
1028
1114
  out = ''
1029
- html_all = self.mmodel.ibsstyle(df,use_tooltip=False,dec=line.dec).to_html()
1115
+ style = self.mmodel.ibsstyle(df,use_tooltip=False,dec=line.dec)
1116
+ if not self.base_last:
1117
+ style = style.format_index(escape='html', axis=0)
1118
+ html_all = style.to_html()
1030
1119
  splitted_html = HTMLSplitData(html_all)
1031
1120
  if i == 0:
1032
1121
  caption = f'<caption>{self.mmodel.string_substitution(self.options.title)}</caption>' if self.options.title else ''
1033
- out = splitted_html.text_before_thead + caption + '<thead>' + splitted_html.thead+'\n'
1122
+ out = splitted_html.text_before_thead + caption + splitted_html.thead+'\n'
1034
1123
 
1035
1124
 
1036
1125
  endhtml = splitted_html.text_after_tbody
@@ -1039,7 +1128,12 @@ class DisplayVarTableDef(DisplayDef):
1039
1128
  col0 = '<tr><td <th class="row_heading level0 row0" > </td>'
1040
1129
  out = out + '\n'+col0 + f"<td colspan='{len(df.columns)}' style='text-align: center;position: sticky; top: 0; background: white; left: 0;'>{line.centertext}</td></tr>"
1041
1130
 
1042
- else:
1131
+ elif line.textlinetype == 'scenarioline':
1132
+ # Spans the whole row so a long name does not widen the label column
1133
+ out = (out + '\n' + f"<tr><th colspan='{len(df.columns) + 1}' style='text-align: left; font-weight: bold;'>"
1134
+ + f"{html.escape(line.centertext)}</th></tr>")
1135
+
1136
+ else:
1043
1137
  out = out + splitted_html.tbody
1044
1138
 
1045
1139
  return out
@@ -1050,6 +1144,8 @@ class DisplayVarTableDef(DisplayDef):
1050
1144
 
1051
1145
 
1052
1146
  outsty = self.make_html_style(thisdf)
1147
+ if not self.base_last:
1148
+ outsty = outsty.format_index(escape='html', axis=1)
1053
1149
 
1054
1150
  if self.options.title:
1055
1151
  outsty = outsty.set_caption(self.options.title)
@@ -1064,11 +1160,11 @@ class DisplayVarTableDef(DisplayDef):
1064
1160
 
1065
1161
  else:
1066
1162
  thisdfs = [(i,df.loc[:,self.timeslice] if self.timeslice else df, line) for i,(df,line) in
1067
- enumerate(zip(self.dfs,self.lines))]
1163
+ enumerate(zip(self.dfs,self.display_lines))]
1068
1164
 
1069
1165
  out = '\n'.join([tab_to_html(i,df,line) for i,df,line in thisdfs])+'\n'
1070
1166
  if self.options.foot:
1071
- foot = f"<tfoot><tr><td colspan='5' style='text-align: left;'>{self.options.foot}</td></tr></tfoot>"
1167
+ foot = f"<tfoot><tr><td colspan='{len(thisdfs[0][1].columns) + 1}' style='text-align: left;'>{self.options.foot}</td></tr></tfoot>"
1072
1168
  else:
1073
1169
  foot =''
1074
1170
 
@@ -1085,11 +1181,19 @@ class DisplayVarTableDef(DisplayDef):
1085
1181
  def latex(self):
1086
1182
  return self.latex_transpose if self.options.transpose else self.latex_straight
1087
1183
 
1184
+ @staticmethod
1185
+ def _latex_scenario_label(label):
1186
+ # Percent is escaped by the existing final LaTeX replacements below.
1187
+ escapes = {'\\': r'\textbackslash{}', '&': r'\&', '_': r'\_',
1188
+ '#': r'\#', '$': r'\$', '{': r'\{', '}': r'\}',
1189
+ '~': r'\textasciitilde{}', '^': r'\textasciicircum{}'}
1190
+ return ''.join(escapes.get(char, char) for char in str(label))
1191
+
1088
1192
 
1089
1193
  @property
1090
1194
  def latex_straight(self):
1091
1195
  last_cols = 0
1092
- rowlines = [ line for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
1196
+ rowlines = [ line for line,df in zip(self.display_lines,self.dfs) for row in range(len(df))]
1093
1197
  if self.timeslice:
1094
1198
  dfs = [self.df_str.loc[:,self.timeslice]]
1095
1199
  else:
@@ -1107,10 +1211,18 @@ class DisplayVarTableDef(DisplayDef):
1107
1211
  outlist = []
1108
1212
  for i,df in enumerate(dfs):
1109
1213
  ncol=len(df.columns)
1110
- newindex = [fr'&\multicolumn{{{ncol}}}'+'{c}{' + f'{line.latexfont}' + '{' + df.index[i]+'}}'
1214
+ first_headline = next((line for line in rowlines if line.textlinetype == 'scenarioline'), None)
1215
+ newindex = [fr'&\multicolumn{{{ncol}}}'+'{c}{' + f'{line.latexfont}' + '{' + df.index[i]+'}}'
1111
1216
  if line.textlinetype == 'textline'
1112
- else df.index[i]
1113
- for i, line in enumerate(rowlines)]
1217
+ # spans the whole row, the blank cells are removed with the textline cells below.
1218
+ # Zero width: LaTeX adds any excess width of a multicolumn to the last column.
1219
+ else ('' if line is first_headline else r'\addlinespace ')
1220
+ + fr'\multicolumn{{{ncol + 1}}}{{l}}{{\makebox[0pt][l]{{\textbf{{'
1221
+ + self._latex_scenario_label(df.index[i]) + '}}}'
1222
+ if line.textlinetype == 'scenarioline'
1223
+ else (df.index[i] if self.base_last or self.headline
1224
+ else self._latex_scenario_label(df.index[i]))
1225
+ for i, line in enumerate(rowlines)]
1114
1226
 
1115
1227
  df.index = newindex
1116
1228
  tabformat = 'l'+'r'*(ncol-last_cols) + ( ('|'+'r'*last_cols) if last_cols else '')
@@ -1159,6 +1271,9 @@ class DisplayVarTableDef(DisplayDef):
1159
1271
  def latex_transpose(self):
1160
1272
 
1161
1273
  thisdf = self.df_str.loc[self.timeslice,:] if self.timeslice else self.df_str
1274
+ if not self.base_last:
1275
+ thisdf = thisdf.copy()
1276
+ thisdf.columns = [self._latex_scenario_label(c) for c in thisdf.columns]
1162
1277
  tabformat = 'l'+'r'*len(thisdf.columns)
1163
1278
 
1164
1279
  latex_df = (thisdf.style.format(lambda x:x)
@@ -2467,7 +2467,8 @@ class NewtonStackSolver(SolverBase):
2467
2467
 
2468
2468
  The per-period prolog/core/epilog decomposition lifted to the stacked
2469
2469
  graph: *core* variables take part in some cycle of the collapsed
2470
- (variable-level) dependency graph -- or are ``___RES`` equations --
2470
+ (variable-level) dependency graph -- or are ``___RES`` equations, or
2471
+ lie on a path from the core back into the core (hybrid models) --
2471
2472
  and become the only stacked Newton unknowns; *prolog* variables never
2472
2473
  depend on the core at any lag or lead and are solved exactly by one
2473
2474
  stacked topological sweep before Newton; *epilog* (reporting)
@@ -2563,6 +2564,19 @@ class NewtonStackSolver(SolverBase):
2563
2564
  core |= {v for v, lags in selflags.items()
2564
2565
  if min(lags) < 0 and max(lags) > 0}
2565
2566
 
2567
+ # a variable on a path from the core back into the core (a descendant
2568
+ # AND an ancestor of the core) must be solved with the core: as an
2569
+ # epilog variable it would only be swept after convergence, so the
2570
+ # core equations depending on it would iterate against a stale value
2571
+ # (wrong solution). It happens when the core is not one strongly
2572
+ # connected block -- notably in a hybrid model, where the ___RES
2573
+ # equations forced into the core leave normalized equations between
2574
+ # them
2575
+ if core:
2576
+ desc = set().union(*(nx.descendants(cg, v) for v in core))
2577
+ anc = set().union(*(nx.ancestors(cg, v) for v in core))
2578
+ core |= desc & anc
2579
+
2566
2580
  desc = (set().union(*(nx.descendants(cg, v) for v in core))
2567
2581
  if core else set())
2568
2582
  epi_vars = [v for v in fulldiff.endovar if v not in core and v in desc]
@@ -2679,14 +2693,22 @@ class NewtonStackSolver(SolverBase):
2679
2693
  if not silent:
2680
2694
  print(f"Iteration {iteration} Max residual "
2681
2695
  f"{newton_conv:>{25},.{12}f}")
2682
- if iteration != 0 and nonlin and not (iteration % nonlin):
2696
+ # scheduled Jacobian refresh -- skipped when the equations
2697
+ # already hold (max |residual| <= newton_absconv): the
2698
+ # remaining step is tiny and the current Jacobian is good
2699
+ # enough for it, while a rebuild (stacked derivatives +
2700
+ # factorization) costs many times a whole iteration
2701
+ if (iteration != 0 and nonlin and not (iteration % nonlin)
2702
+ and newton_conv > newton_absconv):
2683
2703
  with m.timer("Updating solver", timeit):
2684
2704
  if not silent:
2685
2705
  print(f"Updating solver, iteration {iteration}")
2686
2706
  df_now = pd.DataFrame(
2687
2707
  values, index=ctx.databank.index,
2688
2708
  columns=ctx.databank.columns)
2709
+ buildstart = time.time()
2689
2710
  ctx.solver = m.ng_getstacksolver(df_now, resmask)
2711
+ m.ng_stackbuildtime += time.time() - buildstart
2690
2712
  m.ng_stacksolver = ctx.solver
2691
2713
  ctx.diffcount += 1
2692
2714
 
@@ -2748,6 +2770,7 @@ class NewtonStackSolver(SolverBase):
2748
2770
  f'Setup time (seconds) :{m.setuptime:>15,.4f}',
2749
2771
  f'Total model evaluations :{ctx.ittotal:>15,}',
2750
2772
  f'Number of solver update :{ctx.diffcount:>15,}',
2773
+ # all builds in this call: in prepare and the nonlin refreshes
2751
2774
  f'Jacobian build time (seconds) :{getattr(m, "ng_stackbuildtime", 0.0):>15,.4f}',
2752
2775
  f'Simulation time (seconds) :{m.simtime:>15,.4f}',
2753
2776
  f'Floating point operations in model : {numberfloats:>15,}',
@@ -2948,6 +2971,8 @@ class NewtonStackFbminSolver(SolverBase):
2948
2971
  m.ng_stackfbmin_fb = [(per[t], diff.declared_endo_list[s])
2949
2972
  for t, s in zip(fb_t, fb_s)]
2950
2973
 
2974
+ m.ng_stackfbmin_buildtime = 0.0 # all solver builds in this call
2975
+
2951
2976
  if not len(fb):
2952
2977
  # the stacked graph is acyclic: one topological sweep solves it
2953
2978
  ctx.solver = None
@@ -2979,10 +3004,12 @@ class NewtonStackFbminSolver(SolverBase):
2979
3004
  print(f"Creating new stacked fbmin Newton solver "
2980
3005
  f"(jacobian={jac_mode!r})")
2981
3006
  ctx.diffcount += 1
3007
+ buildstart = time.time()
2982
3008
  if jac_mode == "stack":
2983
3009
  m.ng_stackfbmin_solver = self._stack_schur_solver(ctx, databank)
2984
3010
  else:
2985
3011
  m.ng_stackfbmin_solver = self._fd_jacobian_solver(ctx)
3012
+ m.ng_stackfbmin_buildtime = time.time() - buildstart
2986
3013
  m.ng_stackfbmin_jacmode = jac_mode
2987
3014
  ctx.solver = m.ng_stackfbmin_solver
2988
3015
 
@@ -3180,6 +3207,9 @@ class NewtonStackFbminSolver(SolverBase):
3180
3207
  # the factorization like newtonstack does
3181
3208
  nonlin = opt("nonlin", (max(len(self.fb_rows), 2)
3182
3209
  if jac_mode == "fd" else False))
3210
+ # no scheduled solver refresh once the feedback equations hold
3211
+ # (max |residual| <= newton_absconv) -- see NewtonStackSolver.iterate
3212
+ newton_absconv = opt("newton_absconv", 0.001)
3183
3213
  timeit = opt("timeit", False)
3184
3214
  newtonalfa = opt("newtonalfa", 1.0)
3185
3215
  newtonnodamp = opt("newtonnodamp", 0)
@@ -3219,10 +3249,11 @@ class NewtonStackFbminSolver(SolverBase):
3219
3249
  f"{newton_conv:>25,.6f}")
3220
3250
 
3221
3251
  if (iteration != 0 and nonlin and not (iteration % nonlin)
3222
- and jac_mode != "gauss"):
3252
+ and jac_mode != "gauss" and newton_conv > newton_absconv):
3223
3253
  with m.timer("Updating solver", timeit):
3224
3254
  if not silent:
3225
3255
  print(f"Updating solver, iteration {iteration}")
3256
+ buildstart = time.time()
3226
3257
  if jac_mode == "stack":
3227
3258
  df_now = pd.DataFrame(
3228
3259
  values, index=ctx.databank.index,
@@ -3230,6 +3261,7 @@ class NewtonStackFbminSolver(SolverBase):
3230
3261
  ctx.solver = self._stack_schur_solver(ctx, df_now)
3231
3262
  else:
3232
3263
  ctx.solver = self._fd_jacobian_solver(ctx)
3264
+ m.ng_stackfbmin_buildtime += time.time() - buildstart
3233
3265
  m.ng_stackfbmin_solver = ctx.solver
3234
3266
  ctx.diffcount += 1
3235
3267
 
@@ -3298,6 +3330,7 @@ class NewtonStackFbminSolver(SolverBase):
3298
3330
  f'Stacked DAG sweeps :{getattr(ctx, "dag_sweeps", 0):>15,}',
3299
3331
  f'Newton steps on the feedback system :{ctx.ittotal:>15,}',
3300
3332
  f'Number of solver builds :{ctx.diffcount:>15,}',
3333
+ f'Solver build time (seconds) :{getattr(m, "ng_stackfbmin_buildtime", 0.0):>15,.4f}',
3301
3334
  f'Simulation time (seconds) :{m.simtime:>15,.2f}',
3302
3335
  ]
3303
3336
 
@@ -15,8 +15,9 @@ import ipywidgets as widgets
15
15
  # except:
16
16
  # ...
17
17
  # print('No ipysheet ')
18
- from IPython.display import display, clear_output,Latex, Markdown
18
+ from IPython.display import display, clear_output,Latex, Markdown, HTML
19
19
  from dataclasses import dataclass,field
20
+ import uuid
20
21
  import matplotlib.pylab as plt
21
22
 
22
23
  try:
@@ -41,8 +42,34 @@ class basewidget:
41
42
  ''' will update container widgets'''
42
43
  for w in self.datachildren:
43
44
  w.update_df(df,current_per)
44
-
45
-
45
+
46
+
47
+ def html_tabs(parts, tab=True, selected=0):
48
+ '''Plain html (no ipywidgets) version of a Tab or an Accordion, used when model.plain_html is True.
49
+
50
+ parts: {title: html string}. tab=True gives tabs made of hidden radio buttons, labels and css,
51
+ tab=False collapsible <details> sections. selected: the index shown first (None: all closed). '''
52
+ uid = 'mft' + uuid.uuid4().hex[:8] # unique, so tabs in other outputs are not affected
53
+ if not tab:
54
+ # the same name makes the sections exclusive: opening one closes the others
55
+ return ''.join(f'<details name="{uid}"{" open" if i == selected else ""}>'
56
+ f'<summary style="cursor:pointer;font-weight:bold">{title}</summary>{body}</details>'
57
+ for i, (title, body) in enumerate(parts.items()))
58
+ css = [f'#{uid} > input {{display:none}}',
59
+ f'#{uid} > label {{display:inline-block;padding:4px 12px;margin-right:2px;cursor:pointer;'
60
+ f'border:1px solid #bbb;border-bottom:none;border-radius:4px 4px 0 0;background:#eee}}',
61
+ f'#{uid} > div {{display:none;border-top:1px solid #bbb;padding-top:6px}}']
62
+ head, panels = '', ''
63
+ for i, (title, body) in enumerate(parts.items()):
64
+ rid = f'{uid}-{i}'
65
+ head += (f'<input type="radio" name="{uid}" id="{rid}"{" checked" if i == (selected or 0) else ""}>'
66
+ f'<label for="{rid}">{title}</label>')
67
+ panels += f'<div>{body}</div>'
68
+ css.append(f'#{rid}:checked + label {{background:#fff;font-weight:bold}}')
69
+ css.append(f'#{rid}:checked ~ div:nth-of-type({i + 1}) {{display:block}}')
70
+ return f'<style>{" ".join(css)}</style><div id="{uid}">{head}{panels}</div>'
71
+
72
+
46
73
  @dataclass
47
74
  class tabwidget:
48
75
  '''A widget to create tab or acordium contaners'''
@@ -62,8 +89,13 @@ class tabwidget:
62
89
 
63
90
  for i,key in enumerate(self.tabdefdict.keys()):
64
91
  self.datawidget.set_title(i,key)
65
-
66
-
92
+
93
+ @property
94
+ def html(self):
95
+ ''' the same container as plain html, see html_tabs '''
96
+ return html_tabs({key: child.html for key, child in zip(self.tabdefdict.keys(), self.datachildren)},
97
+ tab=self.tab, selected=self.selected_index)
98
+
67
99
  def update_df(self,df,current_per):
68
100
  ''' will update container widgets'''
69
101
  for w in self.datachildren:
@@ -514,6 +546,7 @@ table, th, td {
514
546
 
515
547
  self.whtml = widgets.HTML(image)
516
548
  self.datawidget=widgets.VBox([self.wexp,self.whtml]) if len(self.expname) else self.whtml
549
+ self.html = (f'<div>{self.expname}</div>' if len(self.expname) else '') + image
517
550
 
518
551
 
519
552
 
@@ -540,6 +573,7 @@ class htmlwidget_fig:
540
573
  image = fig_to_image(self.figs,format=self.format)
541
574
  self.whtml = widgets.HTML(image)
542
575
  self.datawidget=widgets.VBox([self.wexp,self.whtml]) if len(self.expname) else self.whtml
576
+ self.html = (f'<div>{self.expname}</div>' if len(self.expname) else '') + image
543
577
 
544
578
  @dataclass
545
579
  class htmlwidget_style:
@@ -561,7 +595,8 @@ table, th, td {
561
595
  }
562
596
  </style>
563
597
  """
564
- self.datawidget = widgets.HTML(f'{style_html}{self.styler.to_html()}' )
598
+ self.html = f'{style_html}{self.styler.to_html()}'
599
+ self.datawidget = widgets.HTML(self.html)
565
600
 
566
601
  def display(self):
567
602
  # Function to display the widget
@@ -580,9 +615,10 @@ class htmlwidget_label:
580
615
  ...
581
616
 
582
617
  self.wexp = widgets.Label(value = self.expname,layout={'width':'54%'})
583
-
584
-
618
+
619
+
585
620
  self.datawidget=self.wexp
621
+ self.html = f'<div>{self.expname}</div>'
586
622
 
587
623
  @dataclass
588
624
  class htmlwidget_text:
@@ -593,8 +629,9 @@ class htmlwidget_text:
593
629
 
594
630
  def __post_init__(self):
595
631
  # Initialize the label widget with the provided text
632
+ self.html = f"<p style='font-family:sans-serif;'>{self.text}</p>"
596
633
  self.datawidget = widgets.HTML(
597
- value=f"<p style='font-family:sans-serif;'>{self.text}</p>",
634
+ value=self.html,
598
635
  layout={'width': '90%'}
599
636
  )
600
637
 
@@ -684,10 +721,10 @@ class visshow:
684
721
  # print('exo inf created')
685
722
 
686
723
  this = tabwidget(tabnew,selected_index=0)
687
- # print('this created ')
688
-
689
-
690
- self.datawidget = this.datawidget
724
+ # print('this created ')
725
+
726
+ # model.plain_html: plain html tabs, which also display where ipywidgets containers don't (RISE)
727
+ self.datawidget = HTML(this.html) if getattr(self.mmodel, 'plain_html', False) else this.datawidget
691
728
  if self.show_on:
692
729
  ...
693
730
  display(self.datawidget)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modelflowib" # PyPI distribution name
7
- version = "2.85" # OVERWRITTEN at build time from MF_VERSION
7
+ version = "2.87" # 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"
@@ -58,6 +58,7 @@ dependencies = [
58
58
  "plotly; sys_platform != 'emscripten'", # modeldashsidebar
59
59
  "pyarrow; sys_platform != 'emscripten'", # only .pcimz (large=True) dump/load; not in every Pyodide (0.27 lacks it)
60
60
  "jupyter; sys_platform != 'emscripten'", # JupyterLite is the notebook there
61
+ "jupyterlab-myst; sys_platform != 'emscripten'", # MyST markdown (directives, mermaid) in notebook cells; a front-end extension, in JupyterLite it comes from the site build
61
62
  ]
62
63
 
63
64
  [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