DyPy Examples

Programming Effort

You’re a programmer working hard to triage bugs in your software before a big release coming up. In your bug tracker, bugs are split into 5 categories: “Core”, “UX/UI”, “Network”, “Database”, and “API”. The release is in 12 days, but your team manager wants to make sure to get some fixes in each, so they ask you to spend no more than 5 days on any single area and at least one day on each, but other than that, they just want you to fix the most bugs possible. Often, you get some benefit from continued work on one area, so you fix more bugs the longer the time you spend on one task, but you also know some areas a bit better than others and burn out a bit if you spend too much time in any one area. Based on that, and the prioritization of tickets in the tracker, you estimate the number of bugs you fix in each category as a function of time as follows:

Bugs closed by days of effort in each area
Category 1 Day 2 Days 3 Days 4 Days 5 Days
Core 2 5 7 8 10
UI/UX 3 6 8 9 9
Network 1 2 4 7 9
Database 2 3 6 8 11
API 4 6 8 9 10

Build a dynamic program that recommends the appropriate days of effort for each category.

Solution

Using DyPy, we’ll set up each category of work as a stage since we’ll do them sequentially, and then build an objective function that provides the number of bugs closed for a given number of days of effort.

# we have 12 days available to us for work
state_variable = dypy.StateVariable("Days Available for Category", values=range(1, 13))
# but we can only spend between 1 and 5 days sworking on a single category
decision_variable = dypy.DecisionVariable("Time on Category", options=decision_options)

def objective_function(stage, days_available_for_category, time_on_category):
    """
        When the objective is called, the solver makes the stage, state variables,
        and decision variable available to it. The keyword argument names here
        match the names of the state variables and decision variables (which will be
        passed as keyword arguments, so order isn't important, but name is). The name
        is automatically derived by lowercasing the name of the variable and replacing
        spaces with underscores. It will remove digits from the front if they are present
        so that the name is a valid python identifier. You can also manually provide a
        kwarg name - if you wish to do this, see the API documentation.

        In this example, we can think of these variables as providing:
            1. stage - this will give access to the DyPy.stage object
            2. state - this just provides access to the state value being assessed, not the object
            3. decision - this just provides access to the decision value being assessed, not the object
    """

    # we are given benefits, so defining here - each category is a row, each column is
    # number of days, and the value is how much benefit we get from working on that category
    # for that long
    benefit_list = [
        # column 0 means no time spent on it, so we get no benefit
        [0, 2, 5, 7, 8, 10],
        [0, 3, 6, 8, 9, 9],
        [0, 1, 2, 4, 7, 9],
        [0, 2, 3, 6, 8, 11],
        [0, 4, 6, 8, 9, 10],
    ]

    if time_on_category > days_available_for_category:  # can't spend more time than we have available
        return stage.parent_dp.exclusion_value  # so exclude these options - sets to high positive or negative value
    else:
        return benefit_list[stage.number][time_on_category]

# define the dynamic program and tell it we have four timesteps
dynamic_program = dypy.DynamicProgram(timestep_size=1, time_horizon=5,
                                      objective_function=objective_function, calculation_function=dypy.MAXIMIZE,
                                      prior=dypy.SimplePrior)

dynamic_program.decision_variable = decision_variable
dynamic_program.add_state_variable(state_variable)

# each category will be a stage, in effect - tell it to create all five stages as empty
dynamic_program.build_stages(name_prefix="Category")  # assigns names by default, but we can override them
stage_names = ["Core", "UI/UX", "Network", "Database", "API"]  # make a list of names in the same order they're used in our objective function
for i, stage in enumerate(dynamic_program.stages):  # and use it to set the value of .name for each stage
    dynamic_program.stages[i].name = stage_names[i]

dynamic_program.run()  # run the dynamic program - results are logged to python logger `dypy` and decisions are set on each stage as .decision_amount