Please note that I added the feature without looking into how other modellers have dealt with these issues.
The script that runs the example models in this article is found in the examples of the development branch of sfc_models: https://github.com/brianr747/SFC_models/blob/development/sfc_models/examples/scripts/ex20220206_flexprice_intro.py This code will not work in the “production” branch — which is what is installed by pip. Users would need to understand how to work with GitHub (and Python paths) to be able to access this feature.
I will have to be sure that everything is running properly before I updated the package installed by pip.
Rather than attempt to build a SFC model with a flexible price added into it, I created the simplest possible example that just has a few variables. Users can see how that system of equations works easily, then copy the workflow to their economic models.
For the purposes of the software package, a “flexprice” variable is an “exogenous variable” that has its value modified until another “balance” variable equals zero. The most pressing example of such a situation is a model of two countries with a floating currency. The currency value needs to adjust until the supply/demand for the currency is equal to zero, where the currency supply/demand is determined by both trade and portfolio flows.
In that case, we have the following structure.
The flexprice variable is the currency price.
The “target” variable is the sum of all the flows into/out of the currency market.
If we created the system without the new flexprice solver, the only way to simulate that is to set the currency price as an exogenous variable, solve for the balance, and then keep moving our guess for the currency until the balance is zero (markets clear).
The new code automates the search process.
Please note that the algorithm only allows a single flexprice variable and a single target; it could be extended to multiple variables. I want to make sure we can solve the single variable case first.
import sfc_models from sfc_models.models import Model print('*Starting up logging*') # Log files are based on name of this module, which is given by: __file__ sfc_models.register_standard_logs(output_dir='output', base_file_name=__file__) print('*Build Model*') mod = Model() # For simplicity, all variables are global. mod.AddGlobalEquation('supply', 'supply function', 'sqrt(price)') mod.AddGlobalEquation('demand', 'demand function', '1/price') mod.AddGlobalEquation('balance', 'supply-demand balance', 'supply-demand') # Must specify price as exogenous. Note that the is_exogenous parameter was # added after version 1.0, so this command will not work on the production version of # sfc_models # The initial price = 4., which is the "wrong" value to balance supply demand # The user could experiment with manually changing this, and see what happens. mod.AddGlobalEquation('price', 'Da price', [4.]*20, is_exogenous=True) mod.MaxTime=10 if input('Run with flexprice solver (y/n)? > ') == 'n': print("running without flexprice!") else: # Run the Flexprice solver. # This step tells the solver to force "balance" to zero via moving "price" print('Running with flexprice!') print('Note that we are not solving initial conditions, so initial balance is non-zero.') mod.EquationSolver.FlexPrice['price'] = 'balance' mod.main()
The above code creates a model with just global variables. The equations generated are (with some manual formatting):
Final Equations: supply = sqrt(price) # supply function demand = 1/price # demand function balance = supply-demand # supply-demand balance # Exogenous Variables price = [4.0, 4.0, 4.0, <...>] # Da price
As can be seen, we have a supply and demand functions, and a balance equation. The price variable is tagged as exogenous.
Some pedants might argue that price is endogenous, and not exogenous. Although I have some sympathies for pedants, what happened is that the price variable has to be included in the list of exogenous variables for it to be calculated by the existing solver. I have zero desire to re-write the code and documentation to deal with this labelling issue.
If we choose to run the model without the flexprice solver, we get a solution based on our (wrong) guess for the price: supply=2, demand = 0.25, so the balance is non-zero (1.75). That is, the markets do not clear. Within an economic model, that would probably imply that balance sheets across the economy do not balance. (Note that users have to manually inject equations to break accounting identities in this fashion.)
Running the Solver
If we choose to run with the solver, we get a solution that looks as follows.
k t balance demand price supply 0 0 1.75 0.25 4 2 1 1 6.3852e-05 0.99996 1 1 2 2 6.3852e-05 0.99996 1 1
The first thing to note is that the price variable remains equal to our manually set guess of 4 at t=0. This is because the initial time step normally cannot be computed in a way consistent with times greater than zero, since models will generally depend upon lagged values of variables. Without values at t=-1, we cannot compute t=0 in that case. (The package allows for finding a steady state initial conditions.)
At t=1 (and greater), the price is calculated to equal 1, and supply = demand = 1, within the limits of calculation precision. The balance variable is close to, but not exactly equal to, zero.
How Does the Flexprice Solver Work?
Starting Bisection, step=1, flexible_variable=price, target_variable=balance Starting bisection Initial Guess=4.0 Remark Guess Value Lower Upper Gap 4.0 1.75 4.8 1.982556896687331 Target has same sign, reversing search direction 3.3333333333333335 1.5257418583505538 2.777777777777778 1.3066666666666669 2.3148148148148153 1.0894515486254617 1.929012345679013 0.8704888888888892 1.607510288065844 0.6457962905212181 1.3395919067215367 0.41091140740740784 1.116326588934614 0.16076837543434852 0.9302721574455116 -0.11044806716049327 Bracketed 1.0232993731900628 0.03435148091607676 0.9302721574455116 1.116326588934614 0.16666666666666663 0.9767857653177872 -0.03544121620208862 0.9302721574455116 1.0232993731900628 0.09090909090909094 1.000042569253925 6.385184231039176e-05 0.9767857653177872 1.0232993731900628 0.045454545454545525 Within tolerance, terminating 1.000042569253925 6.385184231039176e-05 Starting Bisection, step=2, flexible_variable=price, target_variable=balance Starting bisection Initial Guess=1.000042569253925 Remark Guess Value Lower Upper Gap 1.000042569253925 6.385184231039176e-05 Target within tolerance, terminating
The price variable is determined by a bisection algorithm. We treat the entire economic model (at a time step) as a function that maps the price variable to the balance.
At step 1, it starts off with an initial guess of 4.0 (provided by the exogenous variable). It evaluates the system at that guess, then starts looking at other guesses.
Initial Guess=4.0 Remark Guess Value Lower Upper Gap 4.0 1.75 4.8 1.982556896687331 Target has same sign, reversing search direction 3.3333333333333335 1.5257418583505538 2.777777777777778 1.3066666666666669
The first guess is higher than the initial guess (4.8), and the model is then solved. Both values of the balance are positive (1.75, 1.98), so the zero value has to be in the other direction. The guesses get progressively lower, until the guess is below 1., at which point we have two values of the price that have opposite signs.
The algorithm then takes the midpoint of the bracketing values, and keeps searching for the zero crossing until we are within error tolerances.
At later steps, we use the previous value of the price as the initial guess.
Starting Bisection, step=2, flexible_variable=price, target_variable=balance Starting bisection Initial Guess=1.000042569253925 Remark Guess Value Lower Upper Gap 1.000042569253925 6.385184231039176e-05 Target within tolerance, terminating
Since none of the variables depend upon time, the initial guess is within the error tolerance, and so the search algorithm terminates immediately.
Limitations on the Flexprice and Target Variables
The algorithm assumes that the flexprice variable is strictly positive (like a sensible price). The target variable is assumed to be a function of the flexprice variable that has a single root (the target is equal to zero at only one price), with positive and negative balances on either side of the root.
“Multiple equilibria” might make for exciting textual discussions, but such systems cannot be solved numerically without a lot of tricks.
Multiple Flexible Prices?
If there were more than one flexible price, barring the use of symbolic math, I believe that the only way of finding a solution would be to use Newton-Raphson. On paper, the bisection algorithm code could just be replaced with the equivalent Newton-Raphson search. (You would have multiple underlying targets, then do something like take the 2-norm of the balance error vector.)
My main worry is that the economic model evaluation already has an embedded error in the solution of variables. So we are doing a search for the root of a function with an error term. This means that the underlying function technically will have many possible roots in the vicinity of the “true” root. As such, modellers may need to keep an eye on error tolerances within the solver.
As we do the search over the space of flexprices, each “function evaluation” implies solving the entire system. This is an obviously expensive operation.
One potential way to speed up the solution would be to feed in the model solution from the previous guess as the initial guess of the model solution iteration. This would greatly speed up the evaluation of the model once the guesses are close to each other. I have not yet looked into whether this can be implemented without a major code rework.
If everything runs properly for the student who requested the feature, I may create some economic models that use this new feature. I also hope to update the PyPi repository so that everyone who installs sfc_models using pip can use this feature.