def _GenerateRegisteredCashFlows(self): """ Create cash flows based on those previously registered. :return: """ Logger('Model._GenerateRegisteredCashFlows()') Logger('Adding {0} cash flows to sectors', priority=3, data_to_format=(len(self.RegisteredCashFlows), )) for source_sector, target_sector, amount_variable, is_income_source, is_income_dest in self.RegisteredCashFlows: is_cross_currency = source_sector.CurrencyZone != target_sector.CurrencyZone if is_cross_currency: if self.ExternalSector is None: msg = """Only can have cross-currency flows if an ExternalSector object is created\nSource={0} Destination={1}""".format( source_sector.FullCode, target_sector.FullCode) raise LogicError(msg) full_variable_name = source_sector.GetVariableName(amount_variable) source_sector.AddCashFlow('-' + full_variable_name, eqn=None, is_income=is_income_source) if is_cross_currency: fx = self.ExternalSector['FX'] fx._SendMoney(source_sector, full_variable_name) term = fx._ReceiveMoney(target_sector=target_sector, source_sector=source_sector, variable_name=full_variable_name) else: term = '+' + full_variable_name target_sector.AddCashFlow(term, eqn=None, is_income=is_income_dest)
def _GenerateEquations(self): """ Generate equation. :return: None """ Logger('MoneyMarket _GenerateEquations', priority=5) dem_name = 'DEM_' + self.Code self.AddVariable(dem_name, 'Total demand for ' + self.LongName, '') dem_terms = [] for s in self.SearchListSource.GetSectors(): if not s.HasF: continue if s.Code == self.IssuerShortCode: Logger('Found Issuer', priority=3) s.AddVariable('SUP_' + self.Code, 'Supply of ' + self.LongName, self.GetVariableName(dem_name)) self.AddVariable('SUP_' + self.Code, 'Supply of ' + self.LongName, s.GetVariableName('SUP_' + self.Code)) continue try: # If the Sector already has demand for this, add the term. term = s.GetVariableName(dem_name) self.AddTermToEquation(dem_name, term) # dem_terms.append(term) continue except KeyError: pass Logger('Sector is missing demand for {0}; automatically filled in', priority=3, data_to_format=(dem_name, )) s.AddVariable(dem_name, 'Demand for ' + self.LongName, s.GetVariableName('F')) self.AddTermToEquation(dem_name, s.GetVariableName(dem_name))
def _FinalSteps(self): # pragma: no cover self.EquationSolver.ParseString(self.FinalEquations) self.EquationSolver.SolveEquation() self.LogInfo() self.State = 'Finished Running' Logger(self.EquationSolver.GenerateCSVtext(), 'timeseries') Logger.cleanup()
def AddVariable(self, varname, desc='', eqn=''): """ Add a variable to the sector. The variable name (varname) is the local name; it will be decorated to create a full name. Equations within a sector can use the local name; other sectors need to use GetVariableName to get the full name. :param varname: str :param desc: str :param eqn: str :return: None """ if '__' in varname: raise ValueError('Cannot use "__" inside local variable names: ' + varname) if desc is None: desc = '' if type(eqn) == Equation: equation = eqn else: equation = Equation(varname, desc, [ Term(eqn, is_blob=True), ]) if varname in self.GetVariables(): Logger('[ID={0}] Variable Overwritten: {1}', priority=3, data_to_format=(self.ID, varname)) self.EquationBlock.AddEquation(equation) # self.Equations[varname] = eqn Logger('[ID={0}] Variable Added: {1} = {2} # {3}', priority=2, data_to_format=(self.ID, varname, eqn, desc))
def test_solver_logging(self): Logger.cleanup() mock = MockFile() Logger.log_file_handles = {'step': mock} obj = EquationSolver() obj.TraceStep = 2 obj.RunEquationReduction = False # By forcing 't' into the variable list, no automatic creation of time variables obj.ParseString(""" x=t z=x+1 z(0) = 2. exogenous t=[10.]*20 MaxTime=3""") obj.ExtractVariableList() obj.SetInitialConditions() self.assertEqual([], mock.buffer) obj.SolveStep(1) self.assertEqual([], mock.buffer) obj.SolveStep(2) # I do not care what the contents are; I just want to validate # that it triggers self.assertTrue(len(mock.buffer) > 0) # Reset the buffer mock.buffer = [] obj.SolveStep(3) self.assertEqual([], mock.buffer)
def _GenerateEquations(self): # self.AddVariable('SUP_GOOD', 'Supply of goods', '<TO BE DETERMINED>') wage_share = 1.0 - self.ProfitMargin Logger('Searching for Market Sector with Code {0} in parent country', priority=4, data_to_format=(self.OutputName, )) try: market_sup_good = self.Parent.LookupSector( self.OutputName).GetVariableName('SUP_' + self.OutputName) except KeyError: raise Warning('Business {0} Cannot Find Market for {1}'.format( self.Code, self.OutputName)) if self.ProfitMargin == 0: self.AddVariable('DEM_' + self.LabourInputName, 'Demand for labour', market_sup_good) # self.Equations['PROF'] = '' else: self.AddVariable('DEM_' + self.LabourInputName, 'Demand for labour', '%0.3f * %s' % (wage_share, market_sup_good)) self.SetEquationRightHandSide( 'PROF', '%0.3f * %s' % (self.ProfitMargin, market_sup_good)) for s in self.Parent.SectorList: if 'DIV' in s.EquationBlock.Equations: Logger('Adding dividend flow', priority=5) self.AddCashFlow('-DIV', 'PROF', 'Dividends paid', is_income=False) s.AddCashFlow('DIV', self.GetVariableName('PROF'), 'Dividends received', is_income=True) break
def test_empty(self): mock = MockFile() Logger.log_file_handles = {'log': mock} Logger('', endline=False) self.assertEqual([], mock.buffer) Logger('', endline=True) self.assertEqual([' \n'], mock.buffer) Logger.cleanup()
def _GenerateEquations(self): """ Call _GenerateEquations on all child Sector objects. :return: """ Logger('Model._GenerateEquations()', priority=1) for cntry in self.CountryList: for sector in cntry.SectorList: Logger('Calling _GenerateEquations on {0} ({1})', priority=3, data_to_format=(sector.FullCode, type(sector))) sector._GenerateEquations()
def test_register(self): Logger.cleanup() Logger.register_log('filename', 'log') self.assertEqual({'log': 'filename'}, Logger.log_file_handles) with self.assertRaises(ValueError): Logger.register_log('fname2', 'log') Logger.cleanup()
def _FinalEquationFormatting(self, out): """ Convert equation information in list into formatted strings. :param out: list :return: """ Logger('_FinalEquationFormatting()', priority=5) endo = [] exo = [] for row in out: if 'EXOGENOUS' in row[1]: new_eqn = row[1].replace('EXOGENOUS', '') exo.append((row[0], new_eqn, row[2])) else: endo.append(row) max0 = max([len(x[0]) for x in out]) max1 = max([len(x[1]) for x in out]) formatter = '%<max0>s = %-<max1>s # %s' formatter = formatter.replace('<max0>', str(max0)) formatter = formatter.replace('<max1>', str(max1)) endo = [formatter % x for x in endo] exo = [formatter % x for x in exo] s = '\n'.join(endo) + '\n\n# Exogenous Variables\n\n' + '\n'.join(exo) s += '\n\nMaxTime = {0}\nErr_Tolerance=1e-6'.format(self.MaxTime) return s
def AddExogenous(self, sector_fullcode, varname, value): """ Add an exogenous variable to the model. Overwrites an existing variable definition. Need to use the full sector code. Exogenous variables are sepcified as time series, which are implemented as list variables ([1, 2, 3,...]) The exogenous variable can either be specified as a string (which can be evaluated to a list), or else as a list object. The list object will be converted into a string representation using repr(), which means that it may be much longer than using something like '[20,] * 100'. At present, does not support the usage of specifying a constant value. For example value='20.' does not work, you need '[20.]*100.' :param sector_fullcode: str :param varname: str :param value: str :return: """ Logger('Adding exogenous variable: {0} in {1}', priority=5, data_to_format=(varname, sector_fullcode)) # If the user passes in a list or tuple, convert it to a string representation. if type(value) in (list, tuple): value = repr(value) self.Exogenous.append((sector_fullcode, varname, value))
def _SearchSupplier(self): """ Find the sector that is a single supplier in a country. Throws a LogicError if more than one, or none. Need to set SupplyAllocation if you want to do something not covered by this default behaviour. :return: Sector """ Logger('Market {0} searching Country {1} for a supplier', priority=3, data_to_format=(self.Code, self.Parent.Code)) ret_value = None for sector in self.Parent.GetSectors(): if sector.ID == self.ID: continue if 'SUP_' + self.Code in sector.EquationBlock.Equations: if ret_value is None: ret_value = sector else: raise LogicError( 'More than one supplier, must set SupplyAllocation: ' + self.Code) if ret_value is None: raise LogicError('No supplier: ' + self.Code) self.ResidualSupply = ret_value return ret_value
def _CreateFinalEquations(self): """ Create Final equations. Final output, which is a text block of equations :return: str """ Logger('Model._CreateFinalEquations()') out = [] for cntry in self.CountryList: for sector in cntry.SectorList: out.extend(sector._CreateFinalEquations()) out.extend(self._GenerateInitialConditions()) out.extend(self.GlobalVariables) if len(out) == 0: self.FinalEquations = '' raise Warning('There are no equations in the system.') # Build the FinalEquationBlock self.FinalEquationBlock = EquationBlock() for row in out: if 'EXOGENOUS' in row[1]: eq = Equation(row[0], desc=row[2], rhs=row[1].replace('EXOGENOUS', '')) else: eq = Equation(row[0], desc=row[2], rhs=row[1]) self.FinalEquationBlock.AddEquation(eq) out = self._FinalEquationFormatting(out) self.FinalEquations = out return out
def RegisterCashFlow(self, source_sector, target_sector, amount_variable, is_income_source=True, is_income_dest=True): """ Register a cash flow between two sectors. The amount_variable is the name of the local variable within the source sector. Only allowed across currency zones if an ExternalSector object has been defined; otherwise throws a LogicError. The currency value of the amount_variable is assumed to be in the source currency. :param is_income_dest: :param is_income_source: :param source_sector: Sector :param target_sector: Sector :param amount_variable: str :return: """ # if amount_variable not in source_sector.Equations: # raise KeyError('Must define the variable that is the amount of the cash flow') Logger('Cash flow registered {0}: {1} -> {2} [ID: {3} -> {4}]', priority=3, data_to_format=(amount_variable, source_sector.Code, target_sector.Code, source_sector.ID, target_sector.ID)) self.RegisteredCashFlows.append( (source_sector, target_sector, amount_variable, is_income_source, is_income_dest))
def _GenerateTermsLowLevel(self, prefix, long_desc): """ Generate the terms associated with this market, for supply and demand. TODO: This is now only called for the demand function; simplify to just refer to demand. :param prefix: str :param long_desc: str :return: None """ Logger('Searching for demand for market {0}', priority=3, data_to_format=(self.FullCode, )) if prefix not in ('SUP', 'DEM'): raise LogicError('Input to function must be "SUP" or "DEM"') # country = self.Parent short_name = prefix + '_' + self.Code long_name = prefix + '_' + self.FullCode self.AddVariable(short_name, long_desc + ' for Market ' + self.Code, '') term_list = [] for s in self.CurrencyZone.GetSectors(): if s.ID == self.ID: continue if self.ShareParent(s): var_name = short_name else: var_name = long_name try: term = s.GetVariableName(var_name) except KeyError: Logger('Variable {0} does not exist in {1}', priority=10, data_to_format=(var_name, s.FullCode)) continue term_list.append('+ ' + term) if prefix == 'SUP': # pragma: no cover # Since we assume that there is a single supplier, we can set the supply equation to # point to the equation in the market. s.AddCashFlow(var_name, self.GetVariableName(var_name), long_desc) else: # Must fill in demand equation in sectors. s.AddCashFlow('-' + var_name, '', long_desc) eqn = create_equation_from_terms(term_list) self.SetEquationRightHandSide(short_name, eqn)
def __init__(self, parent=None, code=''): self.ID = EconomicObject.ID EconomicObject.ID += 1 self.Parent = parent self.Code = code self.LongName = '' Logger('EconomicObject Created: {0} ID = {1}', priority=1, data_to_format=(type(self), self.ID))
def _GenerateEquationsFrontEnd(self): # pragma: no cover """ Used by graphical front ends; generates a logging message. (In Model.Main(), the logging is done by the Model before it calls the Sector.) :return: """ Logger('Running _GenerateEquations on {0} [{1}]', priority=3, data_to_format=(self.Code, self.ID)) self._GenerateEquations()
def _FitIntoCurrencyZone(self, country): """ Find whether Country fits into an existing CurrencyZone; if not, create a new one. :param country: Country :return: CurrencyZone """ for czone in self.CurrencyZoneList: if country.Currency == czone.Currency: czone.CountryList.append(country) Logger('Fitting {0} into CurrencyZone {1}', data_to_format=(country.LongName, czone.Currency)) return czone Logger('Creating new currency zone {0}, adding {1} to it', data_to_format=(country.Currency, country.LongName)) czone = CurrencyZone(self, country.Currency) czone.CountryList.append(country) self.CurrencyZoneList.append(czone) return czone
def _FixAliases(self): """ Assign the proper names to variables in Sector objects (that were perviously aliases). :return: """ Logger('Fixing aliases (Model._FixAliases)', priority=3) lookup = {} for alias in self.Aliases: sector, varname = self.Aliases[alias] lookup[alias] = sector.GetVariableName(varname) for sector in self.GetSectors(): sector._ReplaceAliases(lookup)
def _GenerateEquations(self): # self.AddVariable('SUP_GOOD', 'Supply of goods', '<TO BE DETERMINED>') wage_share = 1.0 - self.ProfitMargin Logger('Searching for Market Sector with Code {0} in parent country', priority=4, data_to_format=(self.OutputName, )) # Since we have inventories, profits are volume of sales * profit margin self.SetEquationRightHandSide('PROF', '.1 * SUP_GOOD') for s in self.Parent.SectorList: if s.Code == 'HH': Logger('Adding dividend flow', priority=5) self.AddCashFlow('-DIV', 'DIVPAID', 'Dividends paid', is_income=False) s.AddCashFlow('DIV', self.GetVariableName('DIVPAID'), 'Dividends received', is_income=True) break
def _RegisterAlias(self, alias, sector, local_variable_name): """ Used by Sector objects to register aliases for local variables. :param alias: str :param sector: Sector :param local_variable_name: str :return: """ Logger('Registering alias {0} for {1} in ID={2}', priority=5, data_to_format=(alias, local_variable_name, sector.ID)) self.Aliases[alias] = (sector, local_variable_name)
def AddGlobalEquation(self, var, description, eqn): """ Add a variable that is not associated with a sector. Typical example: 't' :param var: str :param description: str :param eqn: str :return: None """ Logger('Registering global equation: {0} = {1}', priority=5, data_to_format=(var, eqn)) self.GlobalVariables.append((var, eqn, description))
def SolveStep(self, step): """ Solve a step. (Assumed to be called in order.) :param step: int :return: """ is_trace_step = step == self.TraceStep if is_trace_step: Logger('Starting convergence tracing.', log='step') Logger('Step {0}'.format(step), log='step') self.TimeSeriesStepTrace = TimeSeriesHolder('iteration') self.TimeSeriesStepTrace['iteration'] = [] self.TimeSeriesStepTrace['iteration_error'] = [] self.TimeSeriesStepTrace['iteration_abs_change'] = [] Logger(""" Values at beginning of step. (Only includes variables that are solved within iteration. Decorative variables calculated later).""", log='step') try: self._SolveStep(step, is_trace_step) finally: if is_trace_step: Logger(self.TimeSeriesStepTrace.GenerateCSVtext(), log='step')
def OnRunModel(self): name = self.GetModelName() if name is None: # Not sure how we get here, but, go back to chooser frame self.FrameChooser.tkraise() return self.CleanupOnModelChange() Logger.cleanup() if not self.Parameters.LogDir == '': base_name = os.path.join(self.Parameters.LogDir, name) Logger.register_standard_logs(base_file_name=base_name) python_mod = self.Importer(name) try: self.Model = python_mod.build_model() except Exception as e: messagebox.showinfo(message=str(e), icon='error', title='Error') raise e if type(self.Model) is not Model: raise ValueError('Expected a Model, got {0} instead'.format( type(Model))) self.Sectors = self.Model.GetSectors() self.UpdateModelViewer() self.FrameModelViewer.tkraise()
def main(self, base_file_name=None): # pragma: no cover """ Routine that does most of the work of model building. The model is build based upon the Sector objects that have been registered as children of this Model. The base_file_name is the base filename that is used for Logging operations; just used to call Logger.register_standard_logs(base_file_name). It is recommended that you call Logger.register_standard_logs() before main, so that Sector creation can be logged. The major operations: [1] Call GenerateEquations() on all Sectors. The fact that GenerateEquations() is only called now at the Sector level means that Sectors can be created independently, and the GenerateEquations() call which ties it to other sectors is only called after all other sectors are created. (There is at least one exception where Sectors have to be created in a specific order, which we want to avoid.) [2] Cleanup work: processing initial conditions, exogenous variables, replacing aliases in equations. [3] Tie the sector level equations into a single block of equations. (Currently strings, not Equation objects.) [4] The equations are passed to self.EquationSolver, and they are solved. The user can then use GetTimeSeries() to access the output time series (if they can be calculated.) :param base_file_name: str :return: None """ self.State = 'Running' try: if base_file_name is not None: Logger.register_standard_logs(base_file_name) Logger('Starting Model main()') self._GenerateFullSectorCodes() self._GenerateEquations() self._FixAliases() self._GenerateRegisteredCashFlows() self._ProcessExogenous() self.FinalEquations = self._CreateFinalEquations() self.EquationSolver.ParseString(self.FinalEquations) self.EquationSolver.SolveEquation() self.LogInfo() except Warning as e: self.LogInfo(ex=e) print('Warning triggered: ' + str(e)) return self.FinalEquations except Exception as e: self.LogInfo(ex=e) raise finally: self.State = 'Finished Running' Logger(self.EquationSolver.GenerateCSVtext(), 'timeseries') Logger.cleanup() return self.FinalEquations
def _GenerateFullSectorCodes(self): """ Create full sector names (which is equal to '[country.Code]_[sector.Code]' - if there is more than one country. Equals the sector code otherwise. :return: None """ Logger('Generating FullSector codes (Model._GenerateFullSectorCodes()', priority=3) add_country_code = len(self.CountryList) > 1 for cntry in self.CountryList: for sector in cntry.SectorList: if add_country_code: sector.FullCode = cntry.Code + '_' + sector.Code else: sector.FullCode = sector.Code
def _AddSector(self, sector): """ Add a sector to this country. This is called by the Sector constructore; users should not call directly. :param sector: Sector :return: """ Logger('Adding Sector {0} To Country {1}', priority=1, data_to_format=(sector.Code, self.Code)) if sector.Code in self: raise LogicError( 'Sector with Code {0} already in Country {1}'.format( sector.Code, self.Code)) self.SectorList.append(sector)
def _AddCountry(self, country): """ Add a country to the list. This is called by the object constructore; users should not call this. :param country: Country :return: None """ Logger('Adding Country: {0} ID={1}', data_to_format=(country.Code, country.ID)) if country.Code in self: raise LogicError('Country with Code {0} already in Model'.format( country.Code)) self.CountryList.append(country) self.DefaultCurrency = country.Currency czone = self._FitIntoCurrencyZone(country) country.CurrencyZone = czone
def _ProcessExogenous(self): """ Handles the exogenous variables. :return: None """ Logger('Processing {0} exogenous variables', priority=3, data_to_format=(len(self.Exogenous), )) for sector_code, varname, eqn in self.Exogenous: if type(sector_code) is str: sector = self.LookupSector(sector_code) else: sector = sector_code if varname not in sector.EquationBlock.Equations: raise KeyError('Sector %s does not have variable %s' % (sector_code, varname)) # Need to mark exogenous variables sector.SetEquationRightHandSide(varname, 'EXOGENOUS ' + eqn)
def _GenerateInitialConditions(self): """ Create block of equations for initial conditions. Validates that the variables exist. :return: """ Logger('Generating {0} initial conditions', priority=3, data_to_format=(len(self.InitialConditions), )) out = [] for sector_code, varname, value in self.InitialConditions: sector = self.LookupSector(sector_code) if varname not in sector.EquationBlock.Equations: raise KeyError('Sector %s does not have variable %s' % (sector_code, varname)) out.append(('%s(0)' % (sector.GetVariableName(varname), ), value, 'Initial Condition')) return out