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 LookupSector(self, short_code): out = None for s in self.GetSectors(): if s.Code == short_code: if out is not None: raise LogicError( """Multiple sectors with same short code ({0}) in CurrencyZone {1}""".format(short_code, self.Code)) else: out = s if out is None: raise LogicError( 'Sector {0} does not exist in CurrencyZone {1}'.format( short_code, self.Code)) return out
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 _GenerateMultiSupply(self): """ Generate the supply terms with multiple suppliers. :return: """ sup_name = 'SUP_' + self.Code dem_name = 'DEM_' + self.Code # Set aggregate supply equal to demand self.SetEquationRightHandSide(sup_name, rhs=dem_name) # Generate individual supply equations # These are already supplied for everything other than the residual supply, so # we need to build it up. # Also, the name of the supply varies, depending on whether we are in te same # country/region. residual_sector = self.ResidualSupply residual_equation = Equation(self.GetSupplierTerm(residual_sector), 'Residual supply', sup_name) sector_list = self.OtherSuppliers # residual supply = total supply less other supply terms for supplier, _ in sector_list: term = '-SUP_' + supplier.FullCode residual_equation.AddTerm(term) # Now that we have an equation for the residual sector, append it to the # list of suppliers, so we can process all suppliers in one block of code. sector_list.append((residual_sector, residual_equation.RHS())) for supplier, eqn in sector_list: local_name = 'SUP_' + supplier.FullCode self.AddVariable(local_name, 'Supply from {0}'.format(supplier.LongName), eqn) # Push this local variable into the supplying sector # If we are in the same country, use 'SUP_{CODE}' # If we are in different countries, use 'SUP_{FULLCODE}' supply_name = self.GetSupplierTerm(supplier) if supply_name not in supplier.EquationBlock: supplier.AddVariable(supply_name, 'Supply to {0}'.format(self.FullCode), '') if self.IsSharedCurrencyZone(supplier): supplier.AddTermToEquation(supply_name, self.GetVariableName(local_name)) supplier.AddCashFlow('+' + supply_name) else: model = self.GetModel() if model.ExternalSector is None: raise LogicError( 'Must create ExternalSector if we have cross-currency suppliers' ) full_local_name = self.GetVariableName(local_name) model.ExternalSector._SendMoney(self, full_local_name) term = model.ExternalSector._ReceiveMoney( supplier, self, full_local_name) supplier.AddTermToEquation(supply_name, term) supplier.AddCashFlow(term) return
def AddCashFlow(self, term, eqn=None, desc=None, is_income=True): """ Add a cash flow to the sector. Will add to the financial asset equation (F), and the income equation (INC) if is_income is True. Except: There is a list of exclusions to which cash flows are not considered income. That setting will override the is_income parameter. This allows us to carve out exceptions to the standard behaviour, which generally is to assume that cash flows are associated with income. :param term: str :param eqn: str :param desc: str :param is_income: bool :return: None """ term = term.strip() if len(term) == 0: return term_obj = Term(term) if not term_obj.IsSimple: # pragma: no cover - Not implemented; cannot hit the line below. raise LogicError( 'Must supply a single variable as the term to AddCashFlow') # term = term.replace(' ', '') # if not (term[0] in ('+', '-')): # term = '+' + term # if len(term) < 2: # raise ValueError('Invalid cash flow term') self.EquationBlock['F'].AddTerm(term) if is_income: # Need to see whether it is excluded mod = self.GetModel() for obj, excluded in mod.IncomeExclusions: if obj.ID == self.ID: if term_obj.Term == excluded: is_income = False break if is_income: self.EquationBlock['INC'].AddTerm(term) if eqn is None: return # Remove the +/- from the term term = term_obj.Term if term in self.GetVariables(): rhs = self.EquationBlock[term].RHS() if rhs == '' or rhs == '0.0': self.SetEquationRightHandSide(term, eqn) else: self.AddVariable(term, desc, eqn)
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 _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 _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 AddTerm(self, term): """ Add a term to the equation. May be a string or Term object. :param term: Term :return: None """ term = Term(term) if len(self.TermList) > 0: if term.IsBlob: raise LogicError('Cannot add a blob to non-empty equation') for other in self.TermList: if term.Term == other.Term: # Already exists; just add the constants together. other.Constant += term.Constant return # Otherwise, append self.TermList.append(term)
def _GenerateEquations(self): CentralBank._GenerateEquations(self) ext = self.GetModel().ExternalSector if ext is None: msg = 'Must Create an ExternalSector in order to use {0}'.format( type(self)) raise LogicError(msg) purchases = 'GOLDPURCHASES' currency = self.CurrencyZone.Currency desc = 'Net Purchases of Gold in TK; Forces EXT_FX_NET_TK to zero'.replace( 'TK', currency) currency_balance = ext['FX'].GetVariableName('NET_' + currency) # A somewhat recursive definition; has ugly convergence properties. # In fact, will not converge without the step adaptation used. # An alternative is to create a special function that is run as a final step # before solving. # It would get the final net supply terms, remove itself, and use that as the # supply variable. self.AddVariable(purchases, desc, '{0} - {1}'.format(purchases, currency_balance)) ext['GOLD'].SetGoldPurchases(self, purchases, self.InitialGoldStock)
def __init__(self, term, is_blob=False): """ Pass in a string (or possibly another term object), and is parsed. If is_blob is True, we do not do any parsing (other than squeezing out internal spaces). An equation is allowed one "blob" term, which is the first term. It may be followed by non-blob terms. As parsing improves, terms can be peeled off of the "blob." :param term: str :param is_blob: False """ if type(term) == Term: self.Constant = term.Constant self.Term = term.Term self.IsSimple = term.IsSimple # Ignore the is_blob input self.IsBlob = term.IsBlob return # Force to be a string; remove whitespace term_s = str(term).strip() # internal spaces do not matter term_s = term_s.replace(' ', '') if is_blob: # If we are a "blob", don't do any parsing. self.Constant = 1.0 self.Term = term_s self.IsSimple = True self.IsBlob = True return self.IsBlob = False # Rule #1: Eliminate '+' or '-' at front self.Constant = 1.0 if term_s.startswith('+'): term_s = term_s[1:] elif term_s.startswith('-'): self.Constant = -1.0 term_s = term_s[1:] # Rule #2: Allow matched "(" if term_s.startswith('('): if not term_s.endswith(')'): raise SyntaxError('Term does not have matching ) - ' + str(term)) # Remove brackets term_s = term_s[1:-1] # If we peeled the brackets, remove '+' or '-' again if term_s.startswith('+'): term_s = term_s[1:] elif term_s.startswith('-'): # Flip the sign self.Constant *= -1.0 term_s = term_s[1:] # We now cannot have embedded '+' or '-' signs. if '+' in term_s: raise LogicError('Term cannot contain interior "+" :' + str(term)) if '-' in term_s: raise LogicError('Term cannot contain interior "-" :' + str(term)) # Do we consist of anything besides a single name token? # If so, we are not simple. # (Will eventually allow for things like '2*x'.) if len(term_s) == 0: raise LogicError('Attempting to create an empty term object.') if is_python_3: g = tokenize.tokenize(BytesIO( term_s.encode('utf-8')).readline) # tokenize the string else: # pragma: no cover [Do my coverage on Python 3] g = tokenize.generate_tokens( BytesIO( term_s.encode('utf-8')).readline) # tokenize the string self.IsSimple = True g = tuple(g) if is_python_3: if not g[0][0] == ENCODING: # pragma: no cover raise LogicError('Internal error: tokenize behaviour changed') if not g[-1][0] == ENDMARKER: # pragma: no cover raise LogicError('Internal error: tokenize behaviour changed') if len(g) > 3: if len(g) == 5: # Allow variable*variable as a "simple" Variable. if g[1][0] == NAME and g[3][0] == NAME and g[2][0] == OP: if g[2][1] in ('*', '/'): self.Term = term_s return raise NotImplementedError('Non-simple parsing not done') # self.IsSimple = False else: if not g[1][0] == NAME: raise NotImplementedError('Non-simple parsing not done') # self.IsSimple = False self.Term = term_s else: # Python 2.7 # pragma: no cover # Missing the first term - augh if not g[-1][0] == ENDMARKER: # pragma: no cover raise LogicError('Internal error: tokenize behaviour changed') if len(g) > 3: if len(g) == 4: # Allow variable*variable as a "simple" Variable. if g[0][0] == NAME and g[2][0] == NAME and g[1][0] == OP: if g[1][1] in ('*', '/'): self.Term = term_s return raise NotImplementedError('Non-simple parsing not done') # self.IsSimple = False else: if not g[0][0] == NAME: raise NotImplementedError('Non-simple parsing not done') # self.IsSimple = False self.Term = term_s
def __init__(self, term, is_blob=False): """ Pass in a string (or possibly another term object), and is parsed. If is_blob is True, we do not do any parsing (other than squeezing out internal spaces). An equation is allowed one "blob" term, which is the first term. It may be followed by non-blob terms. As parsing improves, terms can be peeled off of the "blob." :param term: str :param is_blob: False """ if type(term) == Term: self.Constant = term.Constant self.Term = term.Term self.IsSimple = term.IsSimple # Ignore the is_blob input self.IsBlob = term.IsBlob return # Force to be a string; remove whitespace term_s = str(term).strip() # internal spaces do not matter term_s = term_s.replace(' ', '') if is_blob: # If we are a "blob", don't do any parsing. self.Constant = 1.0 self.Term = term_s self.IsSimple = True self.IsBlob = True return self.IsBlob = False # Rule #1: Eliminate '+' or '-' at front self.Constant = 1.0 if term_s.startswith('+'): term_s = term_s[1:] elif term_s.startswith('-'): self.Constant = -1.0 term_s = term_s[1:] # Rule #2: Allow matched "(" if term_s.startswith('('): if not term_s.endswith(')'): raise SyntaxError('Term does not have matching ) - ' + str(term)) # Remove brackets term_s = term_s[1:-1] # If we peeled the brackets, remove '+' or '-' again if term_s.startswith('+'): term_s = term_s[1:] elif term_s.startswith('-'): # Flip the sign self.Constant *= -1.0 term_s = term_s[1:] # We now cannot have embedded '+' or '-' signs. if '+' in term_s: raise LogicError('Term cannot contain interior "+" :' + str(term)) if '-' in term_s: raise LogicError('Term cannot contain interior "-" :' + str(term)) # Do we consist of anything besides a single name token? # If so, we are not simple. # (Will eventually allow for things like '2*x'.) if len(term_s) == 0: raise LogicError('Attempting to create an empty term object.') if is_python_3: g = tokenize.tokenize(BytesIO( term_s.encode('utf-8')).readline) # tokenize the string else: # pragma: no cover [Do my coverage on Python 3] g = tokenize.generate_tokens( BytesIO( term_s.encode('utf-8')).readline) # tokenize the string self.IsSimple = True g = tuple(g) if is_python_3: # Behaviour changed on me, so needed to clean up logic. # Remove "white space" tokens # Note: The change in behaviour happened between version 3.5 and 3.7. This new code # worked for me on version 3.5 as well as 3.7. cleaned = [ x for x in g if x[0] not in (ENCODING, NEWLINE, ENDMARKER) ] if len(cleaned) == 1: if cleaned[0][0] == NAME or cleaned[0][0] == NUMBER: self.Term = term_s return elif len(cleaned) == 3: # Allow if ((cleaned[0][0] in (NAME, NUMBER)) and (cleaned[1][0] == OP) and (cleaned[2][0] in (NAME, NUMBER)) and (cleaned[1][1] in ('*', '/'))): self.Term = term_s return # Puke otherwise raise NotImplementedError( 'Non-simple parsing not supported ' + '(Note: May fail for Python versions before 3.7 (?)): ' + term_s) # Old code. Will delete once I know new code works... # if not g[0][0] == ENCODING: # pragma: no cover # raise LogicError('Internal error: tokenize behaviour changed') # if not g[-1][0] == ENDMARKER: # pragma: no cover # raise LogicError('Internal error: tokenize behaviour changed') # # if len(g) > 3: # if len(g) == 4: # # Behaviour changed in Version 3.7? # if (not g[2][0] == NEWLINE) or (not g[1][0] == NAME): # raise NotImplementedError('Non-simple parsing not done') # self.Term = term_s # return # elif len(g) in (5, 6): # # For some reason, getting a newline? # # Allow variable*variable as a "simple" Variable. # if g[1][0] == NAME and g[3][0] == NAME and g[2][0] == OP: # if g[2][1] in ('*', '/'): # self.Term = term_s # return # raise NotImplementedError('Non-simple parsing not done') # # self.IsSimple = False # else: # if not g[1][0] == NAME: # raise NotImplementedError('Non-simple parsing not done') # # self.IsSimple = False # self.Term = term_s else: # Python 2.7 # pragma: no cover # Missing the first term - augh if not g[-1][0] == ENDMARKER: # pragma: no cover raise LogicError('Internal error: tokenize behaviour changed') if len(g) > 3: if len(g) == 4: # Allow variable*variable as a "simple" Variable. if g[0][0] == NAME and g[2][0] == NAME and g[1][0] == OP: if g[1][1] in ('*', '/'): self.Term = term_s return raise NotImplementedError('Non-simple parsing not done') # self.IsSimple = False else: if not g[0][0] == NAME: raise NotImplementedError('Non-simple parsing not done') # self.IsSimple = False self.Term = term_s