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.
- {modelflowib-2.85 → modelflowib-2.87}/PKG-INFO +2 -1
- {modelflowib-2.85 → modelflowib-2.87}/modelclass.py +72 -21
- {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/PKG-INFO +2 -1
- {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/requires.txt +1 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelreport.py +181 -66
- {modelflowib-2.85 → modelflowib-2.87}/modelsolver_ng.py +36 -3
- {modelflowib-2.85 → modelflowib-2.87}/modelwidget.py +50 -13
- {modelflowib-2.85 → modelflowib-2.87}/pyproject.toml +2 -1
- {modelflowib-2.85 → modelflowib-2.87}/README.md +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/license.md +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelBLfunk.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_Excel.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_cvx.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_dynare.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_financial_stability.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_latex.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_latex_class.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/model_parquet_mixin.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelcausality.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelconstruct.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelconstruct_estimation.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeldash.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeldashboot.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeldashsidebar.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeldekom.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeldiff.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeldisplay.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelestimation.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelestimator_new.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/SOURCES.txt +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/dependency_links.txt +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelflowib.egg-info/top_level.txt +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelgrab.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelgrabgdx.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelgrabwf2.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelhelp.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelhtml.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelinput_shiny.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelinvert.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeljupyter.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeljupytermagic.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelmacrograb.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelmanipulation.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelmf.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelnet.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelnewton.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelnormalize.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelpattern.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modeluserfunk.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelvis.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelwidget_core.py +0 -0
- {modelflowib-2.85 → modelflowib-2.87}/modelwidget_input.py +0 -0
- {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.
|
|
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' {blanks} <a href="{githubname}" target="_blank">{name}</a>'))
|
|
2391
2391
|
else:
|
|
2392
2392
|
display(HTML(
|
|
2393
|
-
f' {blanks} <a href="{notebook}" target="_blank">{name}</a>'))
|
|
2393
|
+
f' {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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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.
|
|
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"
|
|
@@ -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
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
else
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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.
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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.
|
|
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
|
|
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
|