class RealPropertyLoanSBA(FinancialRecoveryProgram): """A class to represent an SBA real property loan program. Methods: __init__ process(self, entity, callbacks = None): setLoanAmount(self, entity): writeDeadline(self, entity): writeApplied(self, entity): writeDeniedCredit(self, entity): writeInspected(self, entity): writeFirstDisbursement(self, entity): writeSecondDisbursement(self, entity): writeOnlyDisbursement(self, entity): Inheritance: financial.FinancialRecoveryProgram """ def __init__(self, env, duration, inspectors=float('inf'), officers=float('inf'), budget=float('inf'), max_loan=float('inf'), min_credit=0, debt_income_ratio=0.2, loan_term=30.0, interest_rate=0.04, declaration=0, deadline=60): """Initiate SBA real property loan recovery program. Keyword Arguments: env -- simpy.Envionment() object duration -- io.ProbabilityDistribution() object inspectors -- Integer, indicating number of building inspectors assigned to the programs officers -- Number of program staff that reviews and approves loan applications budget -- Integer or float, indicating the initial budget available from the recovery program. max_loan -- The maximum amount ($) of loan that any one entity can receive debt_income_ratio -- Monthly SBA loan payment / entity income ; used to estimate loan amount. min_credit -- A FICO-like credit score used as threshold for approving loan. declaration -- A value indicating how many days after the event a federal disaster was declared. deadline -- A value indicating how many days after the federal disaster declaration was made the applications must be submitted. """ FinancialRecoveryProgram.__init__(self, env, duration, budget) # Define staff/personnel specific to this class self.officers = Resource(self.env, capacity=officers) self.inspectors = Resource(self.env, capacity=inspectors) # New attributes self.min_credit = min_credit self.debt_income_ratio = debt_income_ratio self.loan_term = loan_term # years self.max_loan = max_loan self.interest_rate = interest_rate # annual rate self.deadline = deadline self.declaration = declaration def process(self, entity, callbacks=None): """Define process for entity to submit request for SBA loan. entity -- An entity object from the entities.py module, for example entities.Household(). callbacks -- a generator function containing processes to start after the completion of this process. Returns or Attribute Changes: entity.sba_put -- Records sim time of loan request entity.sba_get -- Records sim time of loan reciept entity.sba_amount -- The amount of loan requested. entity.story -- Append natural language sentences to entities story. """ # Exception handling in case interrupted by another process. try: # Check to see declaration has occurred; if not, wait if self.env.now < self.declaration: yield self.env.timeout(self.declaration - self.env.now) # Record time application submitted. entity.sba_put = self.env.now # Call function to set entity.sba_amount entity.sba_amount = self.setLoanAmount(entity) # Ensure entity does not have enough funds. if entity.sba_amount <= 0: return # Don't qualify for or need SBA loan, end process # Check to see if missed application deadline if self.env.now > self.deadline: self.writeDeadline(entity) return # Application rejected, end process self.writeApplied(entity) # Request a loan processor. officer_request = self.officers.request() yield officer_request # # Yield process timeout for duration needed for officer to process application. yield self.env.timeout(self.duration.rvs()) if entity.credit < self.min_credit: self.writeDeniedCredit(entity) return # Release loan officer so that they can process other loans. self.officers.release(officer_request) # If approved (enough credit), request an inspector. Then release it. # %%% This increases duration by amount of time it takes # to get an inspector. Duration of 1 day assumed, currently. %%%% inspector_request = self.inspectors.request() yield inspector_request yield self.env.timeout(1) # Assumed 1 day inspection duration. self.inspectors.release(inspector_request) # Update loan amount (in case other processes in parallel) entity.sba_amount = self.setLoanAmount(entity) self.writeInspected(entity) if entity.sba_amount <= 0: self.writeWithdraw(entity, 'SBA') return # If loan amount is greater than $25k, it requires collateral and more paperwork if entity.sba_amount > 25000: # Receives $25k immediately as initial disbursement yield self.budget.get(25000) yield entity.recovery_funds.put(25000) self.writeFirstDisbursement(entity) # # %%%% EVENTUALLY MAKE WAIT FOR A BUILDING PERMIT TO BE ISSUED %%% # %%% FOR NOW: Yield another timeout equal to initial process application duration %%% # yield self.env.timeout(self.duration.rvs()) # Update loan amount (in case other processes in parallel) entity.sba_amount = self.setLoanAmount(entity) if entity.sba_amount <= 0: self.writeWithdraw(entity, 'SBA') return yield self.budget.get(entity.sba_amount - 25000) yield entity.recovery_funds.put(entity.sba_amount - 25000) self.writeSecondDisbursement(entity) else: # Add loan amount to entity's money to repair. yield entity.recovery_funds.put(entity.sba_amount) self.writeOnlyDisbursement(entity) # Record time full loan is approved. entity.sba_get = self.env.now # Handle any interrupt from another process. except Interrupt as i: self.writeGaveUp(entity, 'SBA') if callbacks is not None: yield self.env.process(callbacks) else: pass def setLoanAmount(self, entity): required_loan = max( 0, entity.property.damage_value - entity.claim_amount - entity.fema_amount) # Just in case entity doesn't have income attribute (e.g., landlord) try: monthly_rate = self.interest_rate / 12 qualified_monthly_payment = (entity.income / 12) * self.debt_income_ratio qualified_loan = -1.0 * np.pv(monthly_rate, self.loan_term * 12, qualified_monthly_payment) except AttributeError: qualified_loan = required_loan * 0.44 # Result of regression analysis of past SBA loans return min(required_loan, qualified_loan, self.max_loan) def writeDeadline(self, entity): if entity.write_story: entity.story.append( '{0} applied for a ${1:,.0f} SBA loan {2} days after the event. Their application was rejected because it was submitted after the {3}-day deadline after the disaster declaration made on day {4}. ' .format(entity.name.title(), entity.sba_amount, entity.sba_put, self.deadline, self.declaration)) def writeApplied(self, entity): if entity.write_story: required_loan = max( 0, entity.property.damage_value - entity.claim_amount - entity.fema_amount) applied_loan = min(required_loan, self.max_loan) entity.story.append( '{0} applied for a ${1:,.0f} SBA loan {2} days after the event.' .format(entity.name.title(), applied_loan, entity.sba_put)) def writeDeniedCredit(self, entity): if entity.write_story: entity.story.append( '{0}\'s SBA loan application was denied because {0} had a credit score of {1}. ' .format(entity.name.title(), entity.credit)) def writeInspected(self, entity): if entity.write_story: entity.story.append( "SBA inspected {0}\'s home on day {1} after the event. ". format(entity.name.title(), self.env.now)) def writeFirstDisbursement(self, entity): if entity.write_story: entity.story.append( "{0} received an initial SBA loan disbursement of $25,000 {1} days after the event. " .format(entity.name.title(), self.env.now)) def writeSecondDisbursement(self, entity): if entity.write_story: entity.story.append( "{0} received a second SBA loan disbursement of ${1:,.0f} {2} days after the event. " .format(entity.name.title(), (entity.sba_amount - 25000), self.env.now)) def writeOnlyDisbursement(self, entity): if entity.write_story: entity.story.append( "{0} received a SBA loan of ${1:,.0f} {2} days after the event. " .format(entity.name.title(), entity.sba_amount, self.env.now))
class Block: """ Provides simulation-specific behavior of a basic block. Entities are accepted via the process_entity entry-point, and standard processing steps are executed. Actual processing behavior is to be provided by subclasses, implementing the actual_processing method. """ def __init__(self, env: Environment, name: str, block_capacity=float("inf")): self.name = name self.env: Environment = env self.overall_count_in = 0 on_enter_or_exit_method_callable = Callable[ [Entity, Optional[Block], Optional[Block]], None ] self.do_on_enter_list: List[on_enter_or_exit_method_callable] = [] self.do_on_exit_list: List[on_enter_or_exit_method_callable] = [] self.entities: List[Entity] = [] self.successors: List[Block] = [] self.block_resource = Resource(env=env, capacity=block_capacity) self.state_manager = StateManager(self.on_block_state_change) self.env.process(self.late_state_evaluation()) def on_enter(self, entity: Entity): """called when process_entity starts""" for method in self.do_on_enter_list: method(entity, None, self) def on_exit(self: "Block", entity: Entity, successor: "Block"): """called when an entities leaves the block""" self.entities.remove(entity) for method in self.do_on_exit_list: method(entity, self, successor) self.on_block_change() self.on_entity_movement(entity, successor) def process_entity(self, entity: Entity): """main entry point for entities coming from predecessors""" entity.time_of_last_arrival = self.env.now self.on_enter(entity) self.overall_count_in += 1 yield self.env.process(self._process_entity(entity)) def _process_entity(self, entity: Entity): """internal entry point for start of entity processing without calling on_enter or setting entity.time_of_last_arrival. useful e.g. for wip init""" self.entities.append(entity) self.on_block_change() # processing self.state_manager.increment_state_count(States.busy) entity.current_process = self.env.process(self.actual_processing(entity)) yield entity.current_process # might be used for interrupt # wait for successor successor = self.find_successor(entity) req = successor.block_resource.request() self.state_manager.increment_state_count(States.blocked) self.state_manager.decrement_state_count(States.busy) yield req # wait until the chosen successor is ready # leaving self.block_resource.release(entity.block_resource_request) entity.block_resource_request = req # remember to be released self.on_exit(entity, successor) self.state_manager.decrement_state_count(States.blocked) self.env.process(successor.process_entity(entity)) def find_successor(self, entity: Entity): """find next block to send entity to""" return self.successors[0] def on_block_change(self): """called on block change""" def on_block_state_change(self, state, new_value): """called when state of the block changes""" def on_entity_movement(self, entity: Entity, successor: "Block"): """called on entity movement from this block to the successor""" def late_state_evaluation(self): """schedule evaluation on sim start, when the visualizer has been loaded""" yield self.env.timeout(0) self.state_manager.evaluate_state_count() def actual_processing(self, entity: Entity): """to be implemented by concrete subclasses""" raise NotImplementedError()
class FinancialRecoveryProgram(object): """The base class for operationalizing financial recovery programs. All such programs staff and budget implemented as simpy resources or containers. All other classes of financial recovery programs should inherit from this class, either directly or indirectly. The process for FinancialRecoveryProgram is useless and should only be used as an example of how to implement a process in a subclass of FinancialRecoveryProgram. Methods: __init__ process(self, entity = None, callbacks = None): writeCompleted(self, entity): writeGaveUp(self, entity, recovery_program): writeWithdraw(self, entity, recovery_program): """ def __init__(self, env, duration, staff=float('inf'), budget=float('inf')): """Initiate financial recovery program attributes. Keyword Arguments: env -- simpy.Envionment() object duration -- io.ProbabilityDistribution() object staff -- Integer, indicating number of staff assigned to the programs budget -- Integer or float, indicating the initial budget available from the recovery program. Attribute Changes: self.staff -- A simpy.Resource() object with a capacity == staff arg self.budget -- A simpy.Container() object with a initial value == budget arg self.duration -- A function that is used to calculate random durations for the program process """ self.env = env self.staff = Resource(self.env, capacity=staff) self.budget = Container(self.env, init=budget) self.duration = duration def process(self, entity=None, callbacks=None): """Define generic financial recovery program process for entity. entity -- An entity object from the entities.py module, for example entities.Household(). callbacks -- a generator function containing processes to start after the completion of this process. Returns or Attribute Changes: entity.story -- Entity's story list. """ ### ### The contents of this method are an example of what can be done ### in a subclass of this class. It demonstrates the use of SimPy ### Resources and Containiners. The results of the function itself are ### useless. It is meant to help create your own function after creating ### a subclass that inherits from this class. ### # Request staff staff_request = self.staff.request() yield staff_request # Yield timeout equivalent to program's process duration yield self.env.timeout(self.duration.rvs()) # Release release staff after process duation is complete. self.staff.release(staff_request) cost = 1 # Get out amount equal to cost. yield self.budget.get(cost) # Put back amount equal to cost. yield self.budget.put(cost) self.writeCompleted(entity) if callbacks is not None: yield self.env.process(callbacks) else: pass def writeCompleted(self, entity): if entity.write_story: entity.story.append( "{0} process completed for {1} after {2} days, leaving a program budget of ${3:,.0f}. " .format(self.__class__, entity.name.title(), self.env.now, self.budget.level)) def writeGaveUp(self, entity, recovery_program): if entity.write_story: entity.story.append( "{0} gave up waiting for recovery funds from {1} {2} days after the event. " .format(entity.name.title(), recovery_program, self.env.now)) def writeWithdraw(self, entity, recovery_program): #If true, write interrupt outcome to story. if entity.write_story: entity.story.append( '{0} withdrew their application to {1} {2} days after the event because enough recovery funds were found from other sources. ' .format(entity.name.title(), recovery_program, self.env.now))