Recent Posts

Wednesday, March 8, 2017

Understanding The sfc_models Framework Operation

The operation of the sfc_models package for building Stock-Flow Consistent models is somewhat unusual; the user puts into place high-level operations, and the framework fills in most of the details.  The user is then free to tweak the details of particular sector implementations.

One way of understanding the workflow is to think of sfc_models  as the analysis backbone of a computer-aided design program. The user "drags and drops" economic sectors into a model, and then the framework incorporates the outcome. The user can then drill into a particular sector's settings to get more precise control of the outcome. (Such a graphical interface may be built, but the model code needs to be solidified first.)

This article runs through the simplest possible models that we can create ("Hello World!"), and shows how to use the log files to understand how the framework builds models based on high level code.

Note on Example Code

The example in the next section is taken from the file intro_3_03_hello_world_1.py. Like all of my other examples, it is found in the examples.scripts directory of sfc_models.

Since all of the examples coexist within a single directory, I use a long-winded naming scheme. The intro_3_03 part of the file name tells us that these examples are to be included in Section 3.3 of Introduction to SFC Models Using PythonAlso, please note that this code will only work with Version 0.4.1 or later of sfc_models. The logging command used is new.

UPDATE: In the latest versions of sfc_models (>= 0.4.3), it is possible to run a GUI (dialog boxes) to choose where to install examples. In versions greater than 0.4.3 (which will probably arrive shortly after this article was published) use:

from sfc_models import *
install_examples()


In Version 0.4.3, you need to use:

from sfc_models.objects import *
install_examples()

Video Demo Of A Similar Example

This video shows how I run a simple example from within PyCharm. In this case, I built the example up line-by-line, rather than using my prebuilt examples.




Hello World!

The following code block is the simplest possible model you can build with sfc_models.

[From intro_3_03_hello_world_1.py:]

# This next line looks bizarre, but is needed for backwards
# compatibility with Python 2.7.
from __future__ import print_function

import sfc_models
from sfc_models.models import Model

print('*Starting up logging*')
# Log files are based on name of this module.
sfc_models.register_standard_logs(output_dir='output',
                                  base_file_name=__file__)
print('*Build Model*')
mod = Model()
print('*Running main()*')
print('*(This will cause a warning...)*')
mod.main()

The actual work is done in these lines (the remainder are print commands and set up):

mod = Model()
mod.main()

These two lines do the following:
  1. Create a Model object, assign it to the variable mod.
  2. Call the "main()" method of mod. The main method does most of the heavy lifting of the framework, as it builds a mathematical model based on the information embedded in that Model object. The name "main" is perhaps not descriptive, but it is following programming tradition.
The first few lines (up until print('*Build Model*')) are mainly background Python code that will generally make sense only to Python programmers; I will discuss the importance of the register_standard_logs call later. The rest of the code are print() commands.

Running this script generates the output (for versions after 0.4.1):

python intro_3_03_hello_world_1.py
*Starting up logging*
*Build Model*
*Running main()*
*(This will cause a warning...)*
Warning triggered: There are no equations in the system.

Process finished with exit code 0

If we look at the output, we see that the only visible output is the echoing of the print commands; the only thing visible from sfc_models was the line:

Warning triggered: There are no equations in the system.

(Note: In earlier versions of the code, the main() call will actually trigger an error in this case; the framework was unhappy with no equations in the system.)

This warning message is somewhat to be expected: we created a Model object, and incorporated no information into it. Obviously, there should not be any equations associated with it.

However, it cannot be said that that there was no other output; however, that output has been shunted to a log file. I will now return to the following function call.

sfc_models.register_standard_logs(output_dir='output',
                                  base_file_name=__file__)

The function register_standard_logs tells the framework to get ready to build a standard set of log files. (You can register only particular logs if you want more precise control of logging.) The function has two parameters:
  1. output_dir: which is the directory where the output goes. (In this example, "output".)
  2. base_file_name: What is the base name to be used for all logs. I pass into it the __file__ variable, which is the full file name of the Python module. The register_standard_logs function ignores the directory component of the file name, as well as the extension.
When I ran this on my computer (running Windows) the C:\Temp\Python directory looked like this:



When I looked into the output subdirectory, we see the following files were created.


There are three text files, whose names are based upon the file name of the source file.
  1. The file ending with "_eqn.txt" is the list of equations in the Model. (Not very interesting in this case.)
  2. The file end with "_out.txt" is a tab-delimited text file with the time series generated by the model, which can be imported into a spreadsheet. (Also not interesting due to the lack of equations in this case.) This sort of file is often called a csv file, but the usual convention for a csv is to use commas to separate entries. However, the use of commas as a file delimiter is a disaster (for example, commas are used instead of "." for the decimal point in French).
  3. The file ending with "_log.txt" is the log file, which I will discuss next.
The format of the log file (intro_3_03_hello_world_1_log.txt) is expected to change over time, as more information is logged. At the time of writing, the log file contained this text (I deleted some):

Entity Created: <class 'sfc_models.models.Model'> ID = 0
Starting Model main()
  Generating FullSector codes (Model._GenerateFullSectorCodes()
Model._GenerateEquations()
  Fixing aliases (Model._FixAliases)
Model._GenerateRegisteredCashFlows()
  Adding 0 cash flows to sectors
  Processing 0 exogenous variables
Model._CreateFinalEquations()
  Generating 0 initial conditions

Error or Warning raised:
Traceback (most recent call last):
  File "c:\Python33\lib\site-packages\sfc_models\models.py", line 140, in main
    self.FinalEquations = self._CreateFinalEquations()
  File "c:\Python33\lib\site-packages\sfc_models\models.py", line 499, in _CreateFinalEquations
    raise Warning('There are no equations in the system.')
Warning: There are no equations in the system.

The first line of code (mod = Model()) resulted in one line in the log.

Entity Created: <class 'sfc_models.models.Model'> ID = 0

The creation of the Model object was noted; it turns out the Model is an Entity as well, and it was assigned the ID of 0. This numeric ID is designed to provide an easy way to distinguish the objects that are created in the code.

The rest of the log was triggered by running main(). A few operations were run, but they did nothing since there were no equations within the system. This lack of equations was diagnosed, and we end with Raise Warning.

A Non-Empty Model

We then step to the next example, intro_3_03_hello_world_2.py.

from __future__ import print_function
import sfc_models
from sfc_models.models import Model, Country
from sfc_models.sector_definitions import Household

sfc_models.register_standard_logs(output_dir='output',
                                  base_file_name=__file__)
# Start work.
mod = Model()
can = Country(mod, 'Canada', 'CA')
household = Household(can, 'Household Sector', 'HH',
                      alpha_income=.7, alpha_fin=.3)
mod.main()

This code add two new lines.
  1. A Country object (can) is added to the Model (mod).
  2. A Household (a type of Sector) is added the Country, and assigned to the variable name household. There are two parameters associated with the household; the propensity to consume out of income (alpha_income), and the propensity to consume out of financial wealth (alpha_fin).
When this code is run, there is no output on the console (unless the system reports that the script ran without an error). However, there were actually equations generated, and the framework did some work. However, the system does not produce any output on the console -- unless there is some kind of problem (like the warning message from the previous example). In order to see what is happening, we need to look at the log file.

In the output subdirectory, there are three new files generated. The '_out.txt' file now contains some time series that can be viewed in a spreadsheet program, and the equation file has equations. I will only look at the log file ('_log.txt').

In Version 0.4.1 of sfc_models, the log file starts with:

Entity Created: <class 'sfc_models.models.Model'> ID = 0
Entity Created: <class 'sfc_models.models.Country'> ID = 1
Adding Country: CA ID=1
Entity Created: <class 'sfc_models.sector_definitions.Household'> ID = 2
Adding Sector HH To Country CA

We see that three Entity objects are being created. The creation of the Household causes a small flurry of activity. Variables are being added.

 [ID=2] Variable Added: F = F=LAG_F # Financial assets # Financial assets
 [ID=2] Variable Added: INC = INC=0.0 # Income (PreTax) # Income (PreTax)
 [ID=2] Variable Added: LAG_F = F(k-1) # Previous periods financial assets.
    Registering cash flow exclusion: DEM_GOOD for ID=2
 [ID=2] Variable Added: AlphaIncome = 0.7000 # Parameter for consumption out of income
 [ID=2] Variable Added: AlphaFin = 0.3000 # Parameter for consumption out of financial assets
 [ID=2] Variable Added: DEM_GOOD = AlphaIncome * AfterTax + AlphaFin * LAG_F # Expenditure on goods consumption
 [ID=2] Variable Added: AfterTax = INC - T # Aftertax income
 [ID=2] Variable Added: T =  # Taxes paid.
 [ID=2] Variable Added: SUP_LAB = 0. # Supply of Labour


Calling main() actually leads to some work, and the framework solves the equations, one step at a time. This is not actually very difficult, since everything other than the time axis consist of constants. The log file continues.

Starting Model main()
  Generating FullSector codes (Model._GenerateFullSectorCodes()
Model._GenerateEquations()
  Fixing aliases (Model._FixAliases)
Model._GenerateRegisteredCashFlows()
  Adding 0 cash flows to sectors
  Processing 0 exogenous variables
Model._CreateFinalEquations()
  Generating 0 initial conditions
    _FinalEquationFormatting()
Set Initial Conditions
Step: 1
  Number of iterations: 1
Step: 2
  Number of iterations: 1

The log then keeps going.

However, the equations being solved are not particularly interesting. The reason is that a single sector model does not lead to any interesting activity within the framework; the convention is that economic activity is the result of interactions of sectors. This may not be intuitive if you are thinking in terms of the real world household sector; people will undertake all sorts of activities on their own. (Some of these activities will show up in the national accounts.) We need to drop this real world intuition, and focus on the more abstract model behaviour, where activity is mainly between sectors.

The natural extension would be to drop in a business sector to the model; we can then have the interactions between the two sectors. The next example (intro_3_03_hello_world_3.py) attempts to do this.

from __future__ import print_function
import sfc_models
from sfc_models.models import Model, Country
from sfc_models.sector_definitions import Household, FixedMarginBusiness

sfc_models.register_standard_logs(output_dir='output',
                                  base_file_name=__file__)

mod = Model()
can = Country(mod, 'Canada', 'CA')
household = Household(can, 'Household Sector', 'HH',
                      alpha_income=.7, alpha_fin=.3)
business = FixedMarginBusiness(can, 'Business Sector', 'BUS')
mod.main()

When run, this generates a warning:

Warning triggered: Business BUS Cannot Find Market for GOOD

[NOTE: this will only hold on versions greater than 0.4.1; in Version 0.4.1 and below, the warning is actually an error.]

Looking at the relevant part of the log file, we see.

Starting Model main()
  Generating FullSector codes (Model._GenerateFullSectorCodes()
Model._GenerateEquations()
   Searching for Market Sector with Code GOOD in parent country

Error or Warning raised:
[Error trace deleted]
...
    raise Warning('Business {0} Cannot Find Market for {1}'.format(self.Code, self.OutputName))
Warning: Business BUS Cannot Find Market for GOOD

That is, the business sector started searching for a Market sector (with the Code 'GOOD') , and it could not find it. (A business sector by default produces an output with the code 'GOOD', the name of the output can be overridden by passing a new code to use.)

This log file information tells us something about the design of the sfc_models framework. It is not enough to define economic Sectors within our code, we need to add Market objects (or other objects) to allow the sectors to interact. In this case, the business sector assumes that there is a market for its output; otherwise the sector will do nothing. (No point in hiring workers to produce an output that you cannot sell.)

The final "Hello World" example (intro_3_03_hello_world_4.py) creates a goods market to fix that warning.

# NOTE: If you have an older version of sfc_models, you
# may need to replace this import line with specific imports 
from sfc_models.objects import *
sfc_models.register_standard_logs(output_dir='output',
                                  base_file_name=__file__)

mod = Model()
can = Country(mod, 'Canada', 'CA')
household = Household(can, 'Household Sector', 'HH',
                      alpha_income=.7, alpha_fin=.3)
business = FixedMarginBusiness(can, 'Business Sector', 'BUS')
market = Market(can, 'Goods Market', 'GOOD')
mod.main()

When run, we once again get no output on the console: there are no warnings or errors generated. (This could potentially change in future versions; the model is heavily under-determined, as discussed below.) If one examines the log file (or the equations file), we can see that the addition of the Market in goods has caused the framework to add equations linking the supply of goods from the business sector to the demand from the household sector.

However, this model is not really functional; any number of small changes (such as setting initial conditions away from zero) will result in there being no solution to the set of equations. The reason is that we are still missing some key components.
  • We need a labour market ('LAB') that also links the business and household sectors.
  • There are no supply constraints within this version of the business sector; until we add in something to stop an inherent positive feedback loop in the private sector, activity would be infinite (and hence the equations will not converge).
Unless the users switches to using other implementations for the business and household sector, the simplest working model is effectively Model SIM, which is taken from Chapter 3 of Godley and Lavoie's Monetary Economics. This model is implemented in sfc_models.gl_book.chapter3.py. [It also implemented in Section 3.2 of my book, as well as in other posts on bondeconomics.com, such as this article, although the code sample is based on an earlier version of the library. I will fix my examples on the website to match Version 1.0 when it is ready.]

(c) Brian Romanchuk 2017

6 comments:

  1. The last example text section (said to be intro_3_03_hello_world_4.py)
    is incomplete. I suspect a typo or copy-paste error.

    Just starting from the prior example text section (from intro_3_03_hello_world_3.py) and adding ", Market" to the:

    from sfc_models.sector_definitions import .....

    line, and the extra line:

    market = Market(can, 'Goods Market', 'GOOD')

    seems to work ok.

    FYI, I am following your examples, on Linux.

    ReplyDelete
    Replies
    1. Oops, yes, I dropped the import line.

      Good to know that there's no problems with Linux. The only major different will be file path naming; but as long as I avoid hard-coding local paths, I am OK.

      I believe that "from sfc_models.objects import *" will work on the latest packaged edition. Not considered best practice, but it makes example code like this shorter.

      Delete
  2. The last example text section (said to be intro_3_03_hello_world_4.py)
    is incomplete. I suspect a typo or copy-paste error.

    Just starting from the prior example text section (from intro_3_03_hello_world_3.py) and adding ", Market" to the:

    from sfc_models.sector_definitions import .....

    line, and the extra line:

    market = Market(can, 'Goods Market', 'GOOD')

    seems to work ok.

    FYI, I am following your examples, on Linux.

    ReplyDelete
  3. Hello,
    I'm wondering if there is an issue with Python update.
    Indeed, basic codes doesn't work anymore, with the following error raised each time while I'm trying to define a sector (such as household = Household(can, 'HH', 'Household Sector')):
    "NotImplementedError: Non-simple parsing not done"
    Have you any idea how to fix this issue please?
    Best regards,
    Thomas

    ReplyDelete
    Replies
    1. Hello, I’m on my phone, so can’t do much immediately.

      Something changed in a Python library, and my code broke. I think my latest versions are OK. You could try running from my latest GitHub version. I will take a look, and should post a note on this today.

      Delete
    2. Hello - out of curiosity, what version of Python are you using? I will be putting up an article shortly.

      Delete

Note: Posts are manually moderated, with a varying delay. Some disappear.

The comment section here is largely dead. My Substack or Twitter are better places to have a conversation.

Given that this is largely a backup way to reach me, I am going to reject posts that annoy me. Please post lengthy essays elsewhere.