modelflowib 2.86__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.86 → modelflowib-2.87}/PKG-INFO +1 -1
  2. {modelflowib-2.86 → modelflowib-2.87}/modelclass.py +53 -16
  3. {modelflowib-2.86 → modelflowib-2.87}/modelflowib.egg-info/PKG-INFO +1 -1
  4. {modelflowib-2.86 → modelflowib-2.87}/modelreport.py +189 -81
  5. {modelflowib-2.86 → modelflowib-2.87}/modelsolver_ng.py +36 -3
  6. {modelflowib-2.86 → modelflowib-2.87}/pyproject.toml +1 -1
  7. {modelflowib-2.86 → modelflowib-2.87}/README.md +0 -0
  8. {modelflowib-2.86 → modelflowib-2.87}/license.md +0 -0
  9. {modelflowib-2.86 → modelflowib-2.87}/modelBLfunk.py +0 -0
  10. {modelflowib-2.86 → modelflowib-2.87}/model_Excel.py +0 -0
  11. {modelflowib-2.86 → modelflowib-2.87}/model_cvx.py +0 -0
  12. {modelflowib-2.86 → modelflowib-2.87}/model_dynare.py +0 -0
  13. {modelflowib-2.86 → modelflowib-2.87}/model_financial_stability.py +0 -0
  14. {modelflowib-2.86 → modelflowib-2.87}/model_latex.py +0 -0
  15. {modelflowib-2.86 → modelflowib-2.87}/model_latex_class.py +0 -0
  16. {modelflowib-2.86 → modelflowib-2.87}/model_parquet_mixin.py +0 -0
  17. {modelflowib-2.86 → modelflowib-2.87}/modelcausality.py +0 -0
  18. {modelflowib-2.86 → modelflowib-2.87}/modelconstruct.py +0 -0
  19. {modelflowib-2.86 → modelflowib-2.87}/modelconstruct_estimation.py +0 -0
  20. {modelflowib-2.86 → modelflowib-2.87}/modeldash.py +0 -0
  21. {modelflowib-2.86 → modelflowib-2.87}/modeldashboot.py +0 -0
  22. {modelflowib-2.86 → modelflowib-2.87}/modeldashsidebar.py +0 -0
  23. {modelflowib-2.86 → modelflowib-2.87}/modeldekom.py +0 -0
  24. {modelflowib-2.86 → modelflowib-2.87}/modeldiff.py +0 -0
  25. {modelflowib-2.86 → modelflowib-2.87}/modeldisplay.py +0 -0
  26. {modelflowib-2.86 → modelflowib-2.87}/modelestimation.py +0 -0
  27. {modelflowib-2.86 → modelflowib-2.87}/modelestimator_new.py +0 -0
  28. {modelflowib-2.86 → modelflowib-2.87}/modelflowib.egg-info/SOURCES.txt +0 -0
  29. {modelflowib-2.86 → modelflowib-2.87}/modelflowib.egg-info/dependency_links.txt +0 -0
  30. {modelflowib-2.86 → modelflowib-2.87}/modelflowib.egg-info/requires.txt +0 -0
  31. {modelflowib-2.86 → modelflowib-2.87}/modelflowib.egg-info/top_level.txt +0 -0
  32. {modelflowib-2.86 → modelflowib-2.87}/modelgrab.py +0 -0
  33. {modelflowib-2.86 → modelflowib-2.87}/modelgrabgdx.py +0 -0
  34. {modelflowib-2.86 → modelflowib-2.87}/modelgrabwf2.py +0 -0
  35. {modelflowib-2.86 → modelflowib-2.87}/modelhelp.py +0 -0
  36. {modelflowib-2.86 → modelflowib-2.87}/modelhtml.py +0 -0
  37. {modelflowib-2.86 → modelflowib-2.87}/modelinput_shiny.py +0 -0
  38. {modelflowib-2.86 → modelflowib-2.87}/modelinvert.py +0 -0
  39. {modelflowib-2.86 → modelflowib-2.87}/modeljupyter.py +0 -0
  40. {modelflowib-2.86 → modelflowib-2.87}/modeljupytermagic.py +0 -0
  41. {modelflowib-2.86 → modelflowib-2.87}/modelmacrograb.py +0 -0
  42. {modelflowib-2.86 → modelflowib-2.87}/modelmanipulation.py +0 -0
  43. {modelflowib-2.86 → modelflowib-2.87}/modelmf.py +0 -0
  44. {modelflowib-2.86 → modelflowib-2.87}/modelnet.py +0 -0
  45. {modelflowib-2.86 → modelflowib-2.87}/modelnewton.py +0 -0
  46. {modelflowib-2.86 → modelflowib-2.87}/modelnormalize.py +0 -0
  47. {modelflowib-2.86 → modelflowib-2.87}/modelpattern.py +0 -0
  48. {modelflowib-2.86 → modelflowib-2.87}/modeluserfunk.py +0 -0
  49. {modelflowib-2.86 → modelflowib-2.87}/modelvis.py +0 -0
  50. {modelflowib-2.86 → modelflowib-2.87}/modelwidget.py +0 -0
  51. {modelflowib-2.86 → modelflowib-2.87}/modelwidget_core.py +0 -0
  52. {modelflowib-2.86 → modelflowib-2.87}/modelwidget_input.py +0 -0
  53. {modelflowib-2.86 → modelflowib-2.87}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.86
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
@@ -7215,9 +7215,14 @@ class Solver_Mixin():
7215
7215
  if self.previousbase and hasattr(self, 'lastdf'):
7216
7216
  self.basedf = self.lastdf.copy(deep=True)
7217
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
7218
7221
  if self.maxlead >= 1:
7219
7222
  if self.normalized:
7220
7223
  solverguess = 'newtonstack'
7224
+ elif self.hybrid:
7225
+ solverguess = 'newtonstack_ng'
7221
7226
  else:
7222
7227
  solverguess = 'newtonstack_un_normalized'
7223
7228
  else:
@@ -7226,6 +7231,8 @@ class Solver_Mixin():
7226
7231
  solverguess = 'xgenr'
7227
7232
  else:
7228
7233
  solverguess = 'sim'
7234
+ elif self.hybrid:
7235
+ solverguess = 'newton_ng'
7229
7236
  else:
7230
7237
  solverguess = 'newton_un_normalized'
7231
7238
 
@@ -7390,7 +7397,10 @@ class Solver_Mixin():
7390
7397
  for i in [j for j in self.allvar.keys() if self.allvar[j]['matrix']]:
7391
7398
  # Make sure columns with matrixes are of this type
7392
7399
  databank.loc[:, i] = databank.loc[:, i].astype('O')
7393
- 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)
7394
7404
  else:
7395
7405
  newdata = False
7396
7406
  return newdata, databank
@@ -8975,7 +8985,15 @@ class Solver_Mixin():
8975
8985
  - Equation list (rows): endovar (may contain both VAR and VAR___RES)
8976
8986
  - Unknowns (columns): declared_endo_list (base VAR names)
8977
8987
  - is_residual_eq: mask marking rows using residual form G(y,x)
8978
-
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
+
8979
8997
  Newton step:
8980
8998
  residual_i = {
8981
8999
  G_i(y,x) if is_residual_eq[i]
@@ -9034,10 +9052,20 @@ class Solver_Mixin():
9034
9052
  # ======================================================================
9035
9053
  # 1) Setup derivative solver / Jacobian factorization
9036
9054
  # ======================================================================
9037
- 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):
9038
9067
  # --- Determine equation list (rows of Jacobian) --------------------
9039
- endovar = (self.coreorder if hasattr(self, 'coreorder') and len(self.coreorder)
9040
- else self.solveorder)
9068
+ endovar = newton_endovar
9041
9069
 
9042
9070
  # Declared endogenous list (true unknowns / columns of Jacobian)
9043
9071
  self.declared_endo_list = [ v[:-6] if v.endswith('___RES') else v
@@ -9127,9 +9155,12 @@ class Solver_Mixin():
9127
9155
  )
9128
9156
 
9129
9157
  # --------------------------------------------------------------
9130
- # 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
9131
9160
  # --------------------------------------------------------------
9132
-
9161
+ if core_only:
9162
+ self.pronew2d(values, values, row, alfa)
9163
+
9133
9164
  # --------------------------------------------------------------
9134
9165
  # Newton iterations for this period
9135
9166
  # --------------------------------------------------------------
@@ -9232,14 +9263,15 @@ class Solver_Mixin():
9232
9263
  values[row, newton_col_unknown] = (
9233
9264
  y_old - base_damp * update
9234
9265
  )
9235
- values[row,newton_col_residual] = outvalues[row,newton_col_residual] # TO KEEP THE RES UPDATED
9236
-
9266
+ values[row,newton_col_residual] = outvalues[row,newton_col_residual] # TO KEEP THE RES UPDATED
9267
+
9237
9268
  ittotal += 1
9238
-
9239
-
9240
-
9241
-
9242
-
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
+
9243
9275
  if newton_conv > newton_absconv:
9244
9276
  print(
9245
9277
  f'{self.periode} not converged in '
@@ -9944,8 +9976,13 @@ class Report_Mixin:
9944
9976
  timeslice (List[int]): Time slice for data display, empty by default.
9945
9977
  max_cols (int): Maximum columns when displayed as a string, faulting to the system wide setting.
9946
9978
  last_cols (int): Specifies the number of last columns to include in a display slice, particularly in Latex.
9947
- col_desc (str): text centered on columns
9948
-
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
+
9949
9986
  Returns:
9950
9987
  DisplayVarTableDef: Configured table definition object ready for rendering, which includes detailed specifications
9951
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.86
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
@@ -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)
@@ -1525,22 +1640,15 @@ class DisplayKeepFigDef(DisplayDef):
1525
1640
  return [self.out_html ]
1526
1641
 
1527
1642
 
1528
- @property
1529
- def fig_tabwidget(self):
1530
- figlist = {t: htmlwidget_fig(f) for t,f in self.figs.items() }
1531
- return tabwidget(figlist,tab=False,selected_index=0)
1532
-
1533
- @property
1643
+ @property
1534
1644
  def out_html(self):
1535
- return self.fig_tabwidget.datawidget
1536
-
1537
-
1538
- def _ipython_display_(self):
1539
- # model.plain_html: the charts in plain html sections, which also display where ipywidgets containers don't (RISE)
1540
- if getattr(self.mmodel, 'plain_html', False):
1541
- display(HTML(self.fig_tabwidget.html))
1542
- else:
1543
- display(self.out_html)
1645
+ figlist = {t: htmlwidget_fig(f) for t,f in self.figs.items() }
1646
+ out = tabwidget(figlist,tab=False,selected_index=0)
1647
+ return out.datawidget
1648
+
1649
+
1650
+ # def _ipython_display_(self):
1651
+ # display(self.out_html)
1544
1652
 
1545
1653
  # display(self.out_html)
1546
1654
 
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modelflowib" # PyPI distribution name
7
- version = "2.86" # 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"
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