chriskelly / LifeFinances

Scripts for validating retirement plans using Monte Carlo analysis.
GNU Affero General Public License v3.0
9 stars 3 forks source link

Modularize simulation main loop #138

Closed chriskelly closed 1 year ago

chriskelly commented 1 year ago

The simulation is mostly a large block of code so many operations, that's it's challenging for someone new to understand what's being done, and also challenging to confirm it's working correctly since there aren't sub-functions to test. A new design is needed, likely a combination of Functional Programming (where calculations are broken out as independent functions) and Data-Driven Design (where the data for each time interval is stored in State objects rather than many separate variables). This would allow for easier testing and better data access for diagnostics.

Example functional design

def simulate(rabbit_population, fox_population, time_step):
    """Simulate the rabbit population over time."""
    birth_rate = 0.1
    death_rate = 0.05
    fox_attack_rate = 0.01

    def calculate_new_rabbit_population():
        """Calculate the new rabbit population based on the current population and the birth and death rates."""
        births = rabbit_population * birth_rate
        deaths = rabbit_population * death_rate
        new_population = rabbit_population + births - deaths
        return max(new_population, 0)

    def calculate_new_fox_population():
        """Calculate the new fox population based on the current population and the fox attack rate."""
        deaths = fox_population * fox_attack_rate
        new_population = fox_population - deaths
        return max(new_population, 0)

    for i in range(time_step):
        rabbit_population = calculate_new_rabbit_population()
        fox_population = calculate_new_fox_population()

    return rabbit_population, fox_population

Example Data-driven

class BallState:
    def __init__(self, position, velocity, acceleration):
        self.position = position
        self.velocity = velocity
        self.acceleration = acceleration

def update_ball_state(ball_state, time_step, gravity):
    # Update the velocity based on the acceleration and time step
    new_velocity = ball_state.velocity + ball_state.acceleration * time_step

    # Update the position based on the velocity and time step
    new_position = ball_state.position + new_velocity * time_step

    # Update the acceleration based on gravity
    new_acceleration = -gravity

    # Create a new BallState object with the updated state
    new_ball_state = BallState(new_position, new_velocity, new_acceleration)

    return new_ball_state

# Define the initial state of the ball
initial_position = 0.0
initial_velocity = 10.0
initial_acceleration = -9.8
ball_state = BallState(initial_position, initial_velocity, initial_acceleration)

# Define the simulation parameters
time_step = 0.1
total_time = 10.0
gravity = 9.8

# Simulate the behavior of the ball over time
for t in range(int(total_time / time_step)):
    ball_state = update_ball_state(ball_state, time_step, gravity)
    print(f"Time: {t * time_step:.1f}, Position: {ball_state.position:.1f}")

A challenge here will be how (and whether) to fluctuate between vertical and horizontal calculations. By vertical I mean items like income, where it is fast to generate a column/list of all income values for all time intervals independent of other variables. Horizontal calculations are those that can only be calculated within the context of a single time interval since they are dependent on other variables/previous outcomes. Allocation, which can be dependent on the previous time interval's net worth, would have to be a horizontal calculation. The new design patterns I'm suggesting would support horizontal calculations. Vertical calculations could still be done, but it would be outside the context of the main loop, and then require pulling that data into to each time interval. It's unclear whether converting all vertical calculations into horizontal would lead to slower performance, though it would lead to more consistency and easier to read code.