At the start of month 1, SupplyChain Dynamics has $50,000 in available funds. At the beginning of months 1, 2, 3, and 4, the company receives specific revenues from its logistics operations, after which it must pay operational expenses(see table below). Any remaining funds can be allocated to short-term investments with the following options (Interest is earned after paying operational expenses):  
- Invest for one month at an interest rate of 0.2% per month.  
- Invest for two months at an interest rate of 0.8% per month.  
- Invest for three months at an interest rate of 1.5% per month.  
- Invest for four months at an interest rate of 3% per month.  

How can SupplyChain Dynamics allocate its funds to maximize the cash on hand at the beginning of month 5 while meeting its operational obligations?

| Month | Revenue ($) | Operational Expenses ($) |  
|-------|-------------|---------------------------|  
| 1     | 20,000      | 15,000                    |  
| 2     | 25,000      | 35,000                    |  
| 3     | 30,000      | 22,000                    |  
| 4     | 35,000      | 28,000                    |

Problem type: LP (Small: 14 variables, 8 constraints, 26 nonzeros)

Mathematical model:
Set:
1. MonthsInvest:
The set of months when investment decisions can be made.
T_I = \{1, 2, 3, 4\}
2. MonthsBalance:
The set of months for which end-of-month uninvested cash balance is tracked.
T_B = \{1, 2, 3, 4\}
3. InvestmentDurations:
The set of possible investment durations in months.
D = \{1, 2, 3, 4\}  # representing 1-month, 2-month, 3-month, and 4-month investment options respectively.

Parameter:
1. InitialFunds:
# The amount of cash available at the start of month 1.
 50000
2. Revenue:
[20000, 25000, 30000, 35000]  # in dollars for months 1, 2, 3, and 4 respectively.
3. OperationalExpenses:
The operational expenses paid at the beginning of month $t$, for $t \in T_I$.
[15000, 35000, 22000, 28000]  # in dollars for months 1, 2, 3, and 4 respectively.
4. MonthlyInterestRate:
[0.002, 0.008, 0.015, 0.030]  # in dollars for months 1, 2, 3, and 4 respectively.
# The return factor for an investment of duration $d$ is $(1 + d \times \text{MIR}[d])$.

Decision Variable:
1. InvestAmount:
Continuous variable, $X_{t,d} \ge 0$, representing the amount of money invested at the beginning of month $t$ for a duration of $d$ months, for all $t \in T_I$ and $d \in D$ such that $t+d \le 5$.
2. CashOnHand:
Continuous variable, $C_t \ge 0$, representing the uninvested cash held at the end of month $t$, carried over to the start of month $t+1$.
Defined for $t \in T_B$. (i.e., $C_1, C_2, C_3, C_4$)

Objective:
1.  Maximize the total cash on hand at the beginning of month 5.
This includes cash carried over from the end of month 4 and all investments maturing at the start of month 5.
max:
$Z = C_4 + \sum_{\substack{t \in T_I, d \in D \\ t+d=5}} X_{t,d} (1 + d \times \text{MIR}[d])$

Constraint:
1. Cash Balance Constraint:
For each month $t \in T_I$:
The funds available at the beginning of month $t$ (after receiving revenue, paying expenses, and accounting for maturing investments and cash from the previous month) must equal the funds allocated in month $t$ (new investments plus cash carried over).
    - If $t=1$:
        $\text{InitialFunds} + \text{Rev}[1] - \text{Exp}[1] = \sum_{\substack{d \in D \\ 1+d \le 5}} X_{1,d} + C_1$

    - If $t \in T_I \text{ and } t > 1$:
        $C_{t-1} + \sum_{\substack{d' \in D \\ t-d' \ge 1}} X_{t-d', d'} (1 + d' \times \text{MIR}[d']) + \text{Rev}[t] - \text{Exp}[t] = \sum_{\substack{d \in D \\ t+d \le 5}} X_{t,d} + C_t$

2. Non-negativity Constraint:
All decision variables must be non-negative.
$X_{t,d} \ge 0$ for all defined $t,d$.
$C_t \ge 0$ for $t \in T_B$.

Type:
Continuous, Linear, Linear
LP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_fund_allocation(
    initial_funds=50000.0,
    revenues={1: 20000.0, 2: 25000.0, 3: 30000.0, 4: 35000.0},
    expenses={1: 15000.0, 2: 35000.0, 3: 22000.0, 4: 28000.0},
    monthly_interest_rates={
        1: 0.002,
        2: 0.008,
        3: 0.015,
        4: 0.030
    }
):
    """
    Solves the SupplyChain Dynamics fund allocation problem.
    """
    try:
        # --- 1. Model Data ---
        months_invest = [1, 2, 3, 4]
        months_balance = [1, 2, 3, 4]
        investment_durations = [1, 2, 3, 4]

        # --- 2. Create Gurobi Model ---
        model = gp.Model("SupplyChainFundAllocation")
        model.Params.LogToConsole = 0

        # --- 3. Decision Variables ---
        X = model.addVars([(t, d) for t in months_invest for d in investment_durations if t + d <= 5],
                          name="InvestAmount", lb=0.0, vtype=GRB.CONTINUOUS)
        C = model.addVars(months_balance, name="CashOnHandEndOfMonth", lb=0.0, vtype=GRB.CONTINUOUS)

        # --- 4. Objective Function ---
        cash_at_month_5_start = C[4] + gp.quicksum(
            X[t, d] * (1 + d * monthly_interest_rates[d])
            for t in months_invest for d in investment_durations if (t, d) in X and t + d == 5)
        model.setObjective(cash_at_month_5_start, GRB.MAXIMIZE)

        # --- 5. Constraints ---
        for t in months_invest:
            if t == 1:
                funds_in = initial_funds + revenues[t] - expenses[t]
            else:
                cash_from_last_month = C[t - 1]
                maturing_investments_value = gp.quicksum(
                    X[t_invest, d_invest] * (1 + d_invest * monthly_interest_rates[d_invest])
                    for t_invest in months_invest for d_invest in investment_durations
                    if (t_invest, d_invest) in X and t_invest + d_invest == t)
                funds_in = cash_from_last_month + maturing_investments_value + revenues[t] - expenses[t]

            new_investments_made_this_month = gp.quicksum(X[t, d] for d in investment_durations if (t, d) in X)
            cash_carried_over = C[t]
            model.addConstr(funds_in == new_investments_made_this_month + cash_carried_over, name=f"CashBalance_Month{t}")
            if t == 1:
                model.addConstr(expenses[t] <= initial_funds + revenues[t], name=f"Expenses_Month{t}")
            else:
                model.addConstr(expenses[t] <= C[t - 1] + revenues[t], name=f"Expenses_Month{t}")

        # --- 6. Solve Model ---
        model.optimize()

        # --- 7. Return Results ---
        if model.Status == GRB.OPTIMAL:
            return {"status": "optimal", "obj": model.ObjVal}
        else:
            return {"status": f"{model.Status}"}

    except gp.GurobiError as e:
        return {"status": f"Gurobi error: {e}"}
    except Exception as e:
        return {"status": f"An unexpected error occurred: {e}"}


if __name__ == "__main__":
    result = solve_fund_allocation()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 65542