modelflowib 2.75__tar.gz → 2.77__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {modelflowib-2.75 → modelflowib-2.77}/PKG-INFO +1 -1
  2. {modelflowib-2.75 → modelflowib-2.77}/modelclass.py +51 -31
  3. {modelflowib-2.75 → modelflowib-2.77}/modelconstruct_estimation.py +220 -13
  4. {modelflowib-2.75 → modelflowib-2.77}/modelflowib.egg-info/PKG-INFO +1 -1
  5. {modelflowib-2.75 → modelflowib-2.77}/modeljupytermagic.py +5 -1
  6. {modelflowib-2.75 → modelflowib-2.77}/modelnormalize.py +31 -11
  7. {modelflowib-2.75 → modelflowib-2.77}/modelsolver_ng.py +3652 -3652
  8. {modelflowib-2.75 → modelflowib-2.77}/pyproject.toml +1 -1
  9. {modelflowib-2.75 → modelflowib-2.77}/README.md +0 -0
  10. {modelflowib-2.75 → modelflowib-2.77}/license.md +0 -0
  11. {modelflowib-2.75 → modelflowib-2.77}/modelBLfunk.py +0 -0
  12. {modelflowib-2.75 → modelflowib-2.77}/model_Excel.py +0 -0
  13. {modelflowib-2.75 → modelflowib-2.77}/model_cvx.py +0 -0
  14. {modelflowib-2.75 → modelflowib-2.77}/model_dynare.py +0 -0
  15. {modelflowib-2.75 → modelflowib-2.77}/model_financial_stability.py +0 -0
  16. {modelflowib-2.75 → modelflowib-2.77}/model_latex.py +0 -0
  17. {modelflowib-2.75 → modelflowib-2.77}/model_latex_class.py +0 -0
  18. {modelflowib-2.75 → modelflowib-2.77}/model_parquet_mixin.py +0 -0
  19. {modelflowib-2.75 → modelflowib-2.77}/modelconstruct.py +0 -0
  20. {modelflowib-2.75 → modelflowib-2.77}/modeldash.py +0 -0
  21. {modelflowib-2.75 → modelflowib-2.77}/modeldashboot.py +0 -0
  22. {modelflowib-2.75 → modelflowib-2.77}/modeldashsidebar.py +0 -0
  23. {modelflowib-2.75 → modelflowib-2.77}/modeldekom.py +0 -0
  24. {modelflowib-2.75 → modelflowib-2.77}/modeldiff.py +0 -0
  25. {modelflowib-2.75 → modelflowib-2.77}/modeldisplay.py +0 -0
  26. {modelflowib-2.75 → modelflowib-2.77}/modelestimation.py +0 -0
  27. {modelflowib-2.75 → modelflowib-2.77}/modelestimator_new.py +0 -0
  28. {modelflowib-2.75 → modelflowib-2.77}/modelflowib.egg-info/SOURCES.txt +0 -0
  29. {modelflowib-2.75 → modelflowib-2.77}/modelflowib.egg-info/dependency_links.txt +0 -0
  30. {modelflowib-2.75 → modelflowib-2.77}/modelflowib.egg-info/requires.txt +0 -0
  31. {modelflowib-2.75 → modelflowib-2.77}/modelflowib.egg-info/top_level.txt +0 -0
  32. {modelflowib-2.75 → modelflowib-2.77}/modelgrab.py +0 -0
  33. {modelflowib-2.75 → modelflowib-2.77}/modelgrabgdx.py +0 -0
  34. {modelflowib-2.75 → modelflowib-2.77}/modelgrabwf2.py +0 -0
  35. {modelflowib-2.75 → modelflowib-2.77}/modelhelp.py +0 -0
  36. {modelflowib-2.75 → modelflowib-2.77}/modelhtml.py +0 -0
  37. {modelflowib-2.75 → modelflowib-2.77}/modelinvert.py +0 -0
  38. {modelflowib-2.75 → modelflowib-2.77}/modeljupyter.py +0 -0
  39. {modelflowib-2.75 → modelflowib-2.77}/modelmacrograb.py +0 -0
  40. {modelflowib-2.75 → modelflowib-2.77}/modelmanipulation.py +0 -0
  41. {modelflowib-2.75 → modelflowib-2.77}/modelmf.py +0 -0
  42. {modelflowib-2.75 → modelflowib-2.77}/modelnet.py +0 -0
  43. {modelflowib-2.75 → modelflowib-2.77}/modelnewton.py +0 -0
  44. {modelflowib-2.75 → modelflowib-2.77}/modelpattern.py +0 -0
  45. {modelflowib-2.75 → modelflowib-2.77}/modelreport.py +0 -0
  46. {modelflowib-2.75 → modelflowib-2.77}/modeluserfunk.py +0 -0
  47. {modelflowib-2.75 → modelflowib-2.77}/modelvis.py +0 -0
  48. {modelflowib-2.75 → modelflowib-2.77}/modelwidget.py +0 -0
  49. {modelflowib-2.75 → modelflowib-2.77}/modelwidget_input.py +0 -0
  50. {modelflowib-2.75 → modelflowib-2.77}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.75
3
+ Version: 2.77
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
@@ -3188,7 +3188,7 @@ class Modify_Mixin():
3188
3188
  newnormalisation = [l.split() for l in lines]
3189
3189
  vars_before_normalization = {beforeendo.upper() for beforeendo,afterendo in newnormalisation }
3190
3190
  frml_normalize_strip = {(beforeendo.upper(),afterendo.upper()) : self.allvar[beforeendo]['frml'].replace('$','').split(' ',2) for beforeendo,afterendo in newnormalisation }
3191
- frml_normalize_gross = {(beforeendo,afterendo) : (fname ,normal(expression,the_endo = afterendo,endo_lhs=False,add_factor=False)) for (beforeendo,afterendo),(frml,fname,expression) in frml_normalize_strip.items() }
3191
+ frml_normalize_gross = {(beforeendo,afterendo) : (fname ,normal(expression,the_endo = afterendo,endo_lhs=False,add_add_factor=False)) for (beforeendo,afterendo),(frml,fname,expression) in frml_normalize_strip.items() }
3192
3192
  frmldict_normalized = {afterendo : f'FRML {fname} {nexpression.normalized}$'
3193
3193
  for (beforeendo,afterendo),(fname,nexpression) in frml_normalize_gross.items() }
3194
3194
  # breakpoint()
@@ -3282,8 +3282,10 @@ class Modify_Mixin():
3282
3282
 
3283
3283
  Parameters
3284
3284
  ----------
3285
- updateeq : TYPE
3286
- new equations seperated by newline .
3285
+ updateeq : str or Makemodel
3286
+ new equations seperated by newline, or a Makemodel instance whose
3287
+ normalized equations (including per-equation add factors and
3288
+ fitted equations, when present) are used as the update.
3287
3289
  newfunks : TYPE, optional
3288
3290
  Additional userspecified functions. The default is [].
3289
3291
  calc_add : bool, optional
@@ -3297,32 +3299,50 @@ class Modify_Mixin():
3297
3299
  a dataframe with calculated add factors. Origin is the original models lastdf.
3298
3300
 
3299
3301
  '''
3300
-
3301
-
3302
- updatefunks=list(set(self.funks+newfunks) ) #
3303
-
3304
- newmodelname = newname if newname else self.name+' Updated'
3305
-
3306
- dfvars = set(self.lastdf.columns)
3307
-
3308
- updatemodel = mp.tofrml(updateeq) # create the moedl with the usual processing of frml's
3309
- while ' ' in updatemodel:
3310
- updatemodel = updatemodel.replace(' ', ' ')
3311
- frml2 = updatemodel.split('$')
3312
- frml2_strip = [mp.split_frml(f+'$') for f in frml2 if len(f)>2]
3313
- frml2_normal = [[frml,fname,
3314
- normal(expression[:-1],do_preprocess=do_preprocess,add_add_factor=add_add_factor,
3315
- make_fixable = pt.kw_frml_name(fname.upper(),'FIXABLE'))]
3316
- for allfrml,frml,fname,expression in frml2_strip]
3317
- frmldict_update = {nexpression.endo_var: f'{frml} {fname} {nexpression.normalized}$' for
3318
- frml,fname,nexpression in frml2_normal}
3319
-
3320
-
3321
- if add_add_factor:
3322
- frmldict_calc_add = {nexpression.endo_var: f'{frml} {fname} {nexpression.calc_add_factor}$' for
3323
- frml,fname,nexpression in frml2_normal if nexpression.endo_var in self.endogene|self.exogene|dfvars }
3324
- else:
3325
- frmldict_calc_add={}
3302
+ from modelconstruct_estimation import Makemodel # local to avoid circular import
3303
+
3304
+ updatefunks=list(set(self.funks+newfunks) ) #
3305
+
3306
+ newmodelname = newname if newname else self.name+' Updated'
3307
+
3308
+ dfvars = set(self.lastdf.columns)
3309
+ new_var_description = self.var_description
3310
+
3311
+ if isinstance(updateeq, Makemodel):
3312
+ # the Makemodel has already expanded, optionally estimated and normalized the
3313
+ # equations, and decided add factor/fixable/fitted per equation from its flags
3314
+ updatefunks = list(set(updatefunks + updateeq.funks))
3315
+ new_var_description = {**self.var_description, **updateeq.var_description}
3316
+ frml2_normal = [['FRML', fname, nexpression]
3317
+ for fname, (parts, nexpression)
3318
+ in zip(updateeq.normal_output_frmlnames, updateeq.normal)]
3319
+ frmldict_update = {nexpression.endo_var: f'{frml} {fname} {nexpression.normalized}$' for
3320
+ frml,fname,nexpression in frml2_normal}
3321
+ frmldict_update = {**frmldict_update,
3322
+ **{f'{nexpression.endo_var}_FITTED': f'FRML <FIT> {nexpression.fitted}$'
3323
+ for frml,fname,nexpression in frml2_normal if len(nexpression.fitted)}}
3324
+ frmldict_calc_add = {nexpression.endo_var: f'{frml} <CALC_ADD_FACTOR> {nexpression.calc_add_factor}$' for
3325
+ frml,fname,nexpression in frml2_normal
3326
+ if len(nexpression.calc_add_factor) and nexpression.endo_var in self.endogene|self.exogene|dfvars }
3327
+ else:
3328
+ updatemodel = mp.tofrml(updateeq) # create the moedl with the usual processing of frml's
3329
+ while ' ' in updatemodel:
3330
+ updatemodel = updatemodel.replace(' ', ' ')
3331
+ frml2 = updatemodel.split('$')
3332
+ frml2_strip = [mp.split_frml(f+'$') for f in frml2 if len(f)>2]
3333
+ frml2_normal = [[frml,fname,
3334
+ normal(expression[:-1],do_preprocess=do_preprocess,add_add_factor=add_add_factor,
3335
+ make_fixable = pt.kw_frml_name(fname.upper(),'FIXABLE'))]
3336
+ for allfrml,frml,fname,expression in frml2_strip]
3337
+ frmldict_update = {nexpression.endo_var: f'{frml} {fname} {nexpression.normalized}$' for
3338
+ frml,fname,nexpression in frml2_normal}
3339
+
3340
+
3341
+ if add_add_factor:
3342
+ frmldict_calc_add = {nexpression.endo_var: f'{frml} {fname} {nexpression.calc_add_factor}$' for
3343
+ frml,fname,nexpression in frml2_normal if nexpression.endo_var in self.endogene|self.exogene|dfvars }
3344
+ else:
3345
+ frmldict_calc_add={}
3326
3346
  # breakpoint()
3327
3347
 
3328
3348
  frmldict = {k: v['frml'] for (k,v) in self.allvar.items() if k in self.endogene } # frml's in the existing model
@@ -3331,8 +3351,8 @@ class Modify_Mixin():
3331
3351
  newfrml = '\n'.join([f for f in newfrmldict.values()])
3332
3352
  newmodel = self.__class__(newfrml,modelname = f'updated {self.name}',
3333
3353
  funks=updatefunks,
3334
- var_description=self.var_description,
3335
- model_description = self.model_description,
3354
+ var_description=new_var_description,
3355
+ model_description = self.model_description,
3336
3356
  var_groups = self.var_groups ,
3337
3357
  reports = self.reports ,
3338
3358
  substitution = self.substitution,
@@ -982,6 +982,16 @@ from IPython.display import display, Markdown, Math
982
982
  import re
983
983
  from IPython.display import display, Markdown, Math
984
984
 
985
+ class Markdown(Markdown):
986
+ """IPython Markdown, but with the markdown source as the text/plain fallback.
987
+
988
+ The stock class reprs as '<IPython.core.display.Markdown object>', and that
989
+ string is what latex/pdf exporters show, as they can not render the
990
+ text/markdown mime type and fall back to text/plain. With the source as
991
+ repr, exported documents show the model text instead."""
992
+ def __repr__(self):
993
+ return str(self.data)
994
+
985
995
  def display_model(cell: str, spec: str = "markdown"):
986
996
  """
987
997
  Display a model specification.
@@ -1165,8 +1175,12 @@ def _parse_smpl(smpl, df=None):
1165
1175
  Preferred FRML syntax is::
1166
1176
 
1167
1177
  <smpl=start end>
1178
+ <smpl=(start end)>
1168
1179
 
1169
- where ``start`` and ``end`` are separated by one or more blanks. The
1180
+ where ``start`` and ``end`` are separated by one or more blanks, with the
1181
+ brackets optional. A comma may separate them when the value does not pass
1182
+ through a FRML tag -- ``smpl='2000, 2025'`` as a keyword argument -- but not
1183
+ inside ``<...>``, where the tag parser splits options on commas first. The
1170
1184
  labels are **not** coerced to integers, because the dataframe index may be
1171
1185
  a PeriodIndex, DatetimeIndex, quarterly strings, or another custom index
1172
1186
  type. If ``df`` is supplied, its index is used via ``slice_locs`` to
@@ -1256,16 +1270,28 @@ def _parse_smpl(smpl, df=None):
1256
1270
  if not text:
1257
1271
  return None
1258
1272
 
1273
+ # Accept a parenthesised spelling, so <smpl=(2000 2025)> reads the same as
1274
+ # the Python-side smpl=(2000, 2025) tuple. Note that inside a FRML tag only
1275
+ # the blank-separated form works: kw_frml_name splits the tag's options on
1276
+ # commas without regard for brackets, so <smpl=(2000,2025)> would arrive
1277
+ # here already broken in two.
1278
+ closing = {'(': ')', '[': ']'}
1279
+ if len(text) > 1 and text[0] in closing and text[-1] == closing[text[0]]:
1280
+ text = text[1:-1].strip()
1281
+ if not text:
1282
+ return None
1283
+
1259
1284
  # New preferred syntax: <smpl=start end>. Keep old colon syntax so older
1260
1285
  # notebooks do not break.
1261
- if ':' in text and len(text.split()) == 1:
1286
+ if ':' in text and ',' not in text and len(text.split()) == 1:
1262
1287
  start, end = [p.strip() for p in text.split(':', 1)]
1263
1288
  else:
1264
- parts = text.split()
1289
+ parts = [p for p in re.split(r'[,\s]+', text) if p]
1265
1290
  if len(parts) != 2:
1266
1291
  raise ModelSpecificationError(
1267
- "SMPL must be written as 'start end' separated by blanks "
1268
- f"(or legacy 'start:end'); got {smpl!r}"
1292
+ "SMPL must be written as 'start end' separated by blanks, "
1293
+ "optionally bracketed as '(start end)' (or legacy "
1294
+ f"'start:end'); got {smpl!r}"
1269
1295
  )
1270
1296
  start, end = parts
1271
1297
 
@@ -1631,11 +1657,17 @@ def _estimate_and_bake_expression(
1631
1657
  estimator_name,
1632
1658
  input_df=None,
1633
1659
  smpl=None,
1660
+ smpl_is_local: bool = False,
1634
1661
  caption: Optional[str] = None,
1635
1662
  estimator_kwargs: Optional[dict] = None,
1636
1663
  estimator_classes: Optional[dict] = None,
1637
1664
  ):
1638
- """Instantiate an estimator/factory, run it, and return (baked_expression, estimator_obj)."""
1665
+ """Instantiate an estimator/factory, run it, and return (baked_expression, estimator_obj).
1666
+
1667
+ ``smpl_is_local`` marks a sample that came from this equation's own
1668
+ ``<smpl=...>`` rather than from the model-wide default, which decides
1669
+ whether it may override a model-wide ``estimator_kwargs['smpl']``.
1670
+ """
1639
1671
  estimator_constructor = _get_estimator_class(estimator_name, estimator_classes)
1640
1672
  kwargs = dict(estimator_kwargs or {})
1641
1673
 
@@ -1644,13 +1676,16 @@ def _estimate_and_bake_expression(
1644
1676
  # still override the factory's stored defaults.
1645
1677
  if input_df is not None and 'input_df' not in kwargs:
1646
1678
  kwargs['input_df'] = input_df
1647
- if smpl is not None and 'smpl' not in kwargs:
1679
+ # An equation-local <smpl=...> outranks the model-wide estimator_kwargs; a
1680
+ # model-wide Makemodel.smpl does not.
1681
+ if smpl is not None and (smpl_is_local or 'smpl' not in kwargs):
1648
1682
  kwargs['smpl'] = smpl
1649
1683
  if caption is not None and 'caption' not in kwargs:
1650
1684
  kwargs['caption'] = caption
1651
1685
 
1652
1686
  estimator_obj = _instantiate_estimator(estimator_constructor, expression, kwargs)
1653
- estimator_obj = _require_estimator_backend_instance(estimator_obj, estimator_name)
1687
+ # Validates in place and raises; it returns the same object, so do not rebind.
1688
+ _require_estimator_backend_instance(estimator_obj, estimator_name)
1654
1689
  fit_result = _maybe_run_estimator_fit(estimator_obj)
1655
1690
 
1656
1691
  baked = _extract_expression_from_estimator(estimator_obj, fit_result)
@@ -1768,20 +1803,39 @@ def _markdown_with_estimation_blocks(original_text: str, estimation_records: lis
1768
1803
  """Insert estimation markdown blocks after estimator-tagged source lines.
1769
1804
 
1770
1805
  This preserves the original user-facing Markdown as much as possible. For
1771
- the common notebook syntax, each line beginning with ``>`` and containing
1806
+ the common notebook syntax, each equation beginning with ``>`` and containing
1772
1807
  ``<estimator=...>`` gets the next estimation block inserted immediately
1773
1808
  after it. If template expansion creates more estimated equations than can
1774
1809
  be matched to source lines, the remaining blocks are appended at the end.
1810
+
1811
+ An equation can be spread over several lines: a ``>`` line followed by
1812
+ ``>>`` continuations, which the parser joins into one statement. The block
1813
+ goes after the last of those, so the whole equation is shown before its
1814
+ estimation output rather than being split around it.
1775
1815
  """
1776
1816
  records = list(estimation_records or [])
1777
1817
  if not records:
1778
1818
  return original_text
1779
1819
 
1820
+ lines = original_text.splitlines()
1780
1821
  out = []
1781
1822
  rec_i = 0
1782
- for line in original_text.splitlines():
1823
+ i = 0
1824
+ while i < len(lines):
1825
+ line = lines[i]
1783
1826
  out.append(line)
1784
- if rec_i < len(records) and _line_has_estimator_tag(line):
1827
+ i += 1
1828
+
1829
+ if not _line_has_estimator_tag(line):
1830
+ continue
1831
+
1832
+ # Carry the rest of the equation across before inserting the block.
1833
+ # The tag may sit on the '>' line or on any of its continuations.
1834
+ while i < len(lines) and lines[i].lstrip().startswith(">>"):
1835
+ out.append(lines[i])
1836
+ i += 1
1837
+
1838
+ if rec_i < len(records):
1785
1839
  out.append(_estimation_record_to_markdown(records[rec_i]).rstrip())
1786
1840
  rec_i += 1
1787
1841
 
@@ -1795,6 +1849,34 @@ def _markdown_with_estimation_blocks(original_text: str, estimation_records: lis
1795
1849
  return "\n".join(out)
1796
1850
 
1797
1851
 
1852
+ def _strip_markdown_list_blocks(text: str) -> str:
1853
+ """Remove ``>list`` / ``>tlist`` definition blocks from markdown model text.
1854
+
1855
+ A list block is a line whose content (after the leading ``>``) begins with
1856
+ ``list`` or ``tlist``, together with any immediately following ``>>``
1857
+ continuation lines. Equation lines and their ``>>`` continuations are left
1858
+ untouched, because those continuations follow a ``>`` equation line rather
1859
+ than a ``>list`` line.
1860
+
1861
+ Used by ``markdown_with_estimation_no_list`` to support the
1862
+ ``render_list=0`` rendering path of the ``%%Makemymodel`` magic.
1863
+ """
1864
+ out = []
1865
+ in_list = False
1866
+ for line in text.splitlines():
1867
+ stripped = line.lstrip()
1868
+ if in_list:
1869
+ if stripped.startswith('>>'):
1870
+ # Continuation of the current list definition.
1871
+ continue
1872
+ in_list = False
1873
+ if re.match(r'>\s*(list|tlist)\b', stripped, flags=re.IGNORECASE):
1874
+ in_list = True
1875
+ continue
1876
+ out.append(line)
1877
+ return '\n'.join(out)
1878
+
1879
+
1798
1880
  @dataclass
1799
1881
  class BaseExplode:
1800
1882
  """Common parent for Makemodel and Listmodels."""
@@ -1939,9 +2021,80 @@ class BaseExplode:
1939
2021
 
1940
2022
  @dataclass
1941
2023
  class Makemodel(BaseExplode):
2024
+ """Expand a model template (markdown or modelflow) into solvable FRMLs.
2025
+
2026
+ Every attribute below can be inspected with a ``show`` prefix, which prints
2027
+ it nicely, for instance ``consumption.showpost_sum``. The stages follow the
2028
+ order in which ``__post_init__`` produces them.
2029
+
2030
+ **1. Input and options (what you pass in)**
2031
+
2032
+ | Attribute | Description |
2033
+ |---|---|
2034
+ | `showoriginal_statements` | Input expressions |
2035
+ | `showlist_defs` | Lists definitions |
2036
+ | `showtype_input` | Originaal as type modelflow or markdown |
2037
+ | `showreplacements` | list of string tupels with string replacements |
2038
+ | `showfunks` | List of user specified functions to be used in model |
2039
+ | `showmodelname` | A optional name for this (sub) model |
2040
+ | `showvar_description` | Variable descriptions (user-supplied, merged with descriptions from estimated equations) |
2041
+
2042
+ **2. Estimation setup (used only for `<estimator=...>` equations)**
2043
+
2044
+ | Attribute | Description |
2045
+ |---|---|
2046
+ | `showinput_df` | DataFrame used when tagged equations are estimated |
2047
+ | `showestimator` | Default estimator for tagged equations: method name or callable factory |
2048
+ | `showsmpl` | Model-wide default estimation sample. `None` unless passed to the constructor or the magic -- a per-equation `<smpl=start end>` or a `with_defaults(smpl=...)` factory does not show up here. The sample each equation really used is in `showestimation_records` |
2049
+ | `showestimator_kwargs` | Shared kwargs passed to estimator constructors |
2050
+ | `showestimator_classes` | Optional method-name to estimator-class mapping |
2051
+ | `showestimator_namespace` | Optional namespace for resolving `<estimator=name>`; defaults to caller locals/globals |
2052
+
2053
+ **3. Expansion pipeline (in the order they are produced)**
2054
+
2055
+ | Attribute | Description |
2056
+ |---|---|
2057
+ | `showclean_frml_statements` | With frml and nice lists |
2058
+ | `showpost_doable` | Expanded after doable |
2059
+ | `showpost_do` | Frmls after do expansion |
2060
+ | `showpost_sum` | Frmls after expanding sums |
2061
+ | `showexpanded_frml` | frmls after expanding |
2062
+
2063
+ **4. Lists**
2064
+
2065
+ | Attribute | Description |
2066
+ |---|---|
2067
+ | `showmodellist` | The lists defined in string as a dictionary |
2068
+ | `showlists` | Same as `showmodellist` |
2069
+ | `showlist_specification` | All list specifications in string |
2070
+
2071
+ `modellist` is built between `post_do` and `post_sum` (it feeds the sum
2072
+ expansion), `list_specification` is derived at the very end.
2073
+
2074
+ **5. Normalization and estimation output**
2075
+
2076
+ | Attribute | Description |
2077
+ |---|---|
2078
+ | `shownormal_input_expressions` | Expressions sent to modelnormalize, after optional estimation |
2079
+ | `showestimation_records` | Information about equations estimated during construction |
2080
+ | `shownormal_expressions` | List of normal expressions |
2081
+ | `shownormal_output_frmlnames` | FRML names emitted; estimation flags are preserved |
2082
+ | `shownormal_main` | Normalized frmls |
2083
+ | `shownormal_fit` | Normalized frmls for fitted values |
2084
+ | `shownormal_calc_add` | Normalized frmls to calculate add factors |
2085
+ | `shownormal_frml` | Output normalized expressions (`normal_main` + `normal_fit` + `normal_calc_add`) |
2086
+ | `show` | The finished `normal_frml`, printed |
2087
+
2088
+ **6. Rendering**
2089
+
2090
+ | Attribute | Description |
2091
+ |---|---|
2092
+ | `showmarkdown_model` | As markdown |
2093
+ """
2094
+
1942
2095
  # original_statements : str = field(default="", metadata={"description": "Input expressions"})
1943
2096
  # normal_frml : str = field(default="", metadata={"description": "Output normalized expressions"})
1944
-
2097
+
1945
2098
  normal_main : str = field(init=False, metadata={"description": "Normalized frmls"})
1946
2099
  normal_fit : str = field(init=False, metadata={"description": "Normalized frmls for fitted values"})
1947
2100
  normal_calc_add : str = field(init=False, metadata={"description": "Normalized frmls to calculate add factors"})
@@ -2084,7 +2237,9 @@ class Makemodel(BaseExplode):
2084
2237
  ))
2085
2238
 
2086
2239
  self.normal_expressions = [n for p,n in self.normal ]
2087
-
2240
+
2241
+ self._warn_on_repeated_endogenous()
2242
+
2088
2243
  # udrullet = lagarray_unroll(udrullet,funks=funks )
2089
2244
  # udrullet = creatematrix(udrullet,listin=modellist)
2090
2245
  # udrullet = createarray(udrullet,listin=modellist)
@@ -2115,6 +2270,35 @@ class Makemodel(BaseExplode):
2115
2270
  self.list_specification = self.get_lists()
2116
2271
  return
2117
2272
 
2273
+ def _warn_on_repeated_endogenous(self):
2274
+ """Warn when a variable is defined by more than one kept equation.
2275
+
2276
+ Nothing is removed: every equation stays in the emitted FRML, and
2277
+ ``model()`` resolves the collision by letting the last definition win
2278
+ (``modelclass.py``, where each repeat overwrites the variable's entry in
2279
+ ``allvar``). That is usually what a notebook author means when they
2280
+ re-estimate an equation further down, but it is worth saying out loud,
2281
+ because the earlier equations are then dead weight -- and because a
2282
+ typo in a mnemonic looks exactly like a deliberate redefinition.
2283
+ """
2284
+ equations_by_endo = {}
2285
+ for position, (parts, normal) in enumerate(self.normal, start=1):
2286
+ endo = getattr(normal, 'endo_var', '')
2287
+ if endo:
2288
+ equations_by_endo.setdefault(endo, []).append(position)
2289
+
2290
+ for endo, positions in equations_by_endo.items():
2291
+ if len(positions) < 2:
2292
+ continue
2293
+ listed = ', '.join(str(p) for p in positions)
2294
+ print(
2295
+ f"⚠️ {endo} is defined by {len(positions)} equations "
2296
+ f"(kept equations {listed}). The last of them, equation "
2297
+ f"{positions[-1]}, is the one used when the model is solved; "
2298
+ f"the earlier ones go into the FRML but are never evaluated. "
2299
+ f"Tag any you do not want with <DROP>."+'\n'
2300
+ )
2301
+
2118
2302
  def _expression_after_optional_estimation(self, parts, *, equation_index: Optional[int] = None,
2119
2303
  estimator_flag: Any = ...) -> str:
2120
2304
  """Return parts.expression, or an estimated/baked version when tagged.
@@ -2175,6 +2359,7 @@ class Makemodel(BaseExplode):
2175
2359
  estimator_name=estimator_name,
2176
2360
  input_df=self.input_df,
2177
2361
  smpl=smpl,
2362
+ smpl_is_local=local_smpl is not None,
2178
2363
  caption=local_caption,
2179
2364
  estimator_kwargs=self.estimator_kwargs,
2180
2365
  estimator_classes=self.estimator_classes,
@@ -2263,6 +2448,20 @@ class Makemodel(BaseExplode):
2263
2448
  """Alias for :attr:`markdown_with_estimation`."""
2264
2449
  return self.markdown_with_estimation
2265
2450
 
2451
+ @property
2452
+ def markdown_with_estimation_no_list(self) -> str:
2453
+ """Like :attr:`markdown_with_estimation`, with ``>list`` blocks removed.
2454
+
2455
+ Same inline estimation tables as :attr:`markdown_with_estimation`, but
2456
+ ``>list``/``>tlist`` definition blocks are stripped from the rendered
2457
+ Markdown. Used by the ``%%Makemymodel`` magic when ``render_list=0`` is
2458
+ combined with the default ``render_est`` rendering path.
2459
+ """
2460
+ return _markdown_with_estimation_blocks(
2461
+ _strip_markdown_list_blocks(self.original_statements),
2462
+ self.estimation_records,
2463
+ )
2464
+
2266
2465
  @property
2267
2466
  def clean_frml(self) -> str:
2268
2467
  """Normalized equations with add-factors, exogenization, and fitted-value flags stripped."""
@@ -2630,6 +2829,14 @@ class Listmodels(BaseExplode):
2630
2829
  """Alias for :attr:`markdown_with_estimation`."""
2631
2830
  return self.markdown_with_estimation
2632
2831
 
2832
+ @property
2833
+ def markdown_with_estimation_no_list(self) -> str:
2834
+ """Concatenate member Makemodel markdown-with-estimation-no-list strings."""
2835
+ return "\n\n".join(
2836
+ mex.markdown_with_estimation_no_list
2837
+ for mex in self.makemodels
2838
+ )
2839
+
2633
2840
  # __str__(self):
2634
2841
 
2635
2842
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.75
3
+ Version: 2.77
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
@@ -27,6 +27,10 @@ from modelclass import model
27
27
  from modelmanipulation import explode
28
28
  from model_latex_class import a_latex_model
29
29
  from modelconstruct_estimation import Makemodel, display_model
30
+ # Markdown subclass whose text/plain fallback is the markdown source itself,
31
+ # so latex/pdf exports show the model text instead of
32
+ # '<IPython.core.display.Markdown object>'. Shadows the IPython import above.
33
+ from modelconstruct_estimation import Markdown
30
34
  from modelhelp import debug_var
31
35
  from modelreport import LatexRepo
32
36
 
@@ -589,7 +593,7 @@ try:
589
593
  if dict_name in user_ns:
590
594
  segs = list(user_ns[dict_name].keys())
591
595
  print(f"Model `{name}` built from segments: {', '.join(segs)}")
592
- print(f"✅ Created Mexplode model: {name}")
596
+ print(f"✅ Created Makemodel: {name}")
593
597
  print(emodel)
594
598
 
595
599
  return emodel
@@ -129,8 +129,19 @@ def funk_find_arg(funk_match, streng):
129
129
  return streng[:start], match[1:index], match[index + 1:]
130
130
  assert 1==2,f'Parantese mismatch in {streng}'
131
131
 
132
+ def funk_check_arg(funk,arg,udtryk,udtryk_up):
133
+ '''Guards against variables named like a function.
134
+
135
+ A lagged variable with a function name, fx DIFF(-1), matches DIFF( and would
136
+ silently be swallowed as a function call. If the argument is a pure signed
137
+ integer it must be such a lag, so raise an informative error instead.'''
138
+ if re.fullmatch(r'[+-]?\d+',arg.replace(' ','')):
139
+ raise Exception(f"A variable can not be named {funk}, as {funk}( is a function in the business language.\n"
140
+ f"{funk}({arg}) looks like a lagged variable {funk}. Rename the variable.\n"
141
+ f"Original expression: {udtryk}\n"
142
+ f"Processed so far : {udtryk_up}")
143
+
132
144
 
133
-
134
145
  def preprocess(udtryk,funks=[]):
135
146
  '''
136
147
  test processing expanding dlog,diff,movavg,pct,logit functions
@@ -146,16 +157,21 @@ def preprocess(udtryk,funks=[]):
146
157
 
147
158
  '''
148
159
  udtryk_up = udtryk.upper().replace(' ','')
160
+ udtryk_up = funk_replace('PCT','PCT_GROWTH',udtryk_up)
161
+
149
162
  while dlog_match := funk_in('DLOG',udtryk_up):
150
163
  fordlog,dlogudtryk_up,efterdlog=funk_find_arg(dlog_match,udtryk_up)
151
- udtryk_up=fordlog+'DIFF(LOG('+dlogudtryk_up+'))'+efterdlog
152
-
164
+ funk_check_arg('DLOG',dlogudtryk_up,udtryk,udtryk_up)
165
+ udtryk_up=fordlog+'DIFF(LOG('+dlogudtryk_up+'))'+efterdlog
166
+
153
167
  while logit_match := funk_in('LOGIT',udtryk_up):
154
168
  forlogit,logitudtryk_up,efterlogit=funk_find_arg(logit_match,udtryk_up)
155
- udtryk_up=forlogit+'(LOG('+logitudtryk_up+'/(1.0 -'+logitudtryk_up+')))'+efterlogit
156
-
169
+ funk_check_arg('LOGIT',logitudtryk_up,udtryk,udtryk_up)
170
+ udtryk_up=forlogit+'(LOG('+logitudtryk_up+'/(1.0 -'+logitudtryk_up+')))'+efterlogit
171
+
157
172
  while movavg_match := funk_in('MOVAVG', udtryk_up):
158
173
  forkaede,kaedeudtryk,efterkaede=funk_find_arg(movavg_match,udtryk_up)
174
+ funk_check_arg('MOVAVG',kaedeudtryk,udtryk,udtryk_up)
159
175
  arg=kaedeudtryk.split(',',1)
160
176
  avg='(('
161
177
  term=arg[0]
@@ -168,19 +184,23 @@ def preprocess(udtryk,funks=[]):
168
184
 
169
185
  while pct_match := funk_in('PCT_GROWTH',udtryk_up):
170
186
  forpc,pcudtryk,efterpc=funk_find_arg(pct_match,udtryk_up)
171
- udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks)}) -1)) {efterpc}'
187
+ funk_check_arg('PCT_GROWTH',pcudtryk,udtryk,udtryk_up)
188
+ udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks)}) -1)) {efterpc}'
172
189
 
173
190
  while pct_match := funk_in('PCY',udtryk_up):
174
191
  forpc,pcudtryk,efterpc=funk_find_arg(pct_match,udtryk_up)
175
- udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks,laglead=-4)}) -1)) {efterpc}'
176
-
192
+ funk_check_arg('PCY',pcudtryk,udtryk,udtryk_up)
193
+ udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks,laglead=-4)}) -1)) {efterpc}'
194
+
177
195
  while diff_match := funk_in('DIFF' , udtryk_up):
178
196
  fordif,difudtryk_up,efterdif=funk_find_arg(diff_match,udtryk_up)
179
- udtryk_up=fordif+'(('+difudtryk_up+')-('+lagone(difudtryk_up+'',funks=funks)+'))'+efterdif
180
-
197
+ funk_check_arg('DIFF',difudtryk_up,udtryk,udtryk_up)
198
+ udtryk_up=fordif+'(('+difudtryk_up+')-('+lagone(difudtryk_up+'',funks=funks)+'))'+efterdif
199
+
181
200
  while diff_match := funk_in('D' , udtryk_up):
182
201
  # print(f'{udtryk_up} on D')
183
202
  fordif,difudtryk_up,efterdif=funk_find_arg(diff_match,udtryk_up)
203
+ funk_check_arg('D',difudtryk_up,udtryk,udtryk_up)
184
204
  difudtryk_up = difudtryk_up.replace(' ','').replace(',0,1','') if difudtryk_up.endswith(',0,1') else difudtryk_up
185
205
  udtryk_up=fordif+'(('+difudtryk_up+')-('+lagone(difudtryk_up+'',funks=funks)+'))'+efterdif
186
206
 
@@ -407,7 +427,7 @@ if __name__ == '__main__':
407
427
  normal('a+b = c',add_add_factor=1,make_fitted=1).fprint
408
428
  normal('PCT_growth(a) = n(-1)',add_add_factor=0).fprint
409
429
  normal('a = movavg(pct(b),2)',add_add_factor=0).fprint
410
- normal('pct_growth(c) = pct_growth(d)',add_add_factor=0).fprint
430
+ normal('pct_growth(c) = pct_growth(dd)',add_add_factor=0).fprint
411
431
  normal('pct_growth(c) = z+pct(b) + pct(e)').fprint
412
432
  normal('pct_growth(c) = z+pct(b) + pct(e)').fprint
413
433
  normal('a = pct_growth(b)',add_add_factor=0).fprint