<jupyter_start><jupyter_text><jupyter_code>!pip install pulp -q
import numpy as np
import pandas as pd
import pulp
from decimal import *<jupyter_output><empty_output><jupyter_text>ImplementationTo implement the LP solution for ATM replenishment process, we use the procedure from [[1]](https://www.igi-global.com/gateway/article/251842) reference as a main implementation.Terminology1. ***demand_day*** the day in which the demand amount is needed based on the forecasting methods.2. ***holding_day*** the number of day that the demand amount has been hold for.3. ***loading_day*** the day in which the demand amount of money is loaded into the ATM.<jupyter_code># initial parameters
number_of_days = 5 # period of replenishment
cash_interest_rate = Decimal(1) / Decimal(100) # 1% per day
loading_cost = 5 # loading cost per times
demand_amount_per_day = [Decimal(e) for e in [100,200,100,300,100]] # amount to replenish from demand forecasting
# calculate an amount with interest table
amount_df = pd.DataFrame(demand_amount_per_day, columns=['amount'])
amount_df['demand_day'] = [e + 1 for e in amount_df.index.to_list()]
amount_df['key'] = 0

print("Amount by each demand day")
display(amount_df.set_index(['demand_day'])[['amount']].T)

# create a period table
period_table = pd.DataFrame([Decimal(e) for e in range(number_of_days)], columns = ['holding_day'])
period_table['key'] = 0

# cross join the amount and period tables
amount_df = amount_df.merge(period_table, on='key', how='outer')
amount_df.drop(['key'], axis=1, inplace=True)
amount_df = amount_df[
                      (amount_df['demand_day'] > amount_df['holding_day'] )
]
amount_df['amount_with_interest'] = amount_df['amount'] * (1 + cash_interest_rate)**(amount_df['holding_day'])
amount_df['interest_cost'] = amount_df['amount_with_interest'] - amount_df['amount']

# calculate accumulated interests at each loading day
amount_df['loading_day'] = (amount_df['demand_day'] - amount_df['holding_day'])
amount_df = amount_df.sort_values(['loading_day','holding_day'])
amount_df['accumulated_cost'] = amount_df.groupby('loading_day')['interest_cost'].apply(np.cumsum)
amount_df['total_cost'] = amount_df['accumulated_cost'] + loading_cost

# shows results
print("\nInterest cost at each demand day if we loaded money at day 1")
display(pd.crosstab(index = amount_df['demand_day'],
                    columns = amount_df['holding_day'],
                    values= amount_df['interest_cost'], aggfunc='sum').fillna(0.0))

print("\nAccumulated interest cost at each loading day")
display(pd.crosstab(index = amount_df['loading_day'],
                    columns = amount_df['demand_day'],
                    values= amount_df['accumulated_cost'], aggfunc='sum').fillna(0.0))

print("\nTotal cost at each loading day")
total_costs = pd.crosstab(index = amount_df['loading_day'],
                    columns = amount_df['demand_day'],
                    values= amount_df['total_cost'], aggfunc='sum').fillna(0.0)
display(total_costs)

# convert decimal to float because pulp lib is not compatible with a decimal type.
for c in total_costs.columns:
    total_costs[c] = total_costs[c].astype(float)
total_costs_arr = np.array(total_costs)<jupyter_output>Amount by each demand day<jupyter_text>Illustration of the LP solution on a case<jupyter_code># an usage example
x = pulp.LpVariable("x", 0, 3)
y = pulp.LpVariable("y", 0, 1)
prob = pulp.LpProblem("myProblem", pulp.LpMinimize)
prob += x + y <= 2 # add a constraint
prob += -4 * x + y # add an objective
status = prob.solve()
print(f"The model state is {pulp.LpStatus[status]}")
print(f"The value of x is {pulp.value(x)}")<jupyter_output>The model state is Optimal
The value of x is 2.0<jupyter_text>ATM Replenishment<jupyter_code># create decision variables
variable_names = [str(i)+str(j) for i in range(1, number_of_days+1) for j in range(1, number_of_days+1)]
variable_names
dv_variables = pulp.LpVariable.matrix("x", variable_names, cat = 'Integer', lowBound = 0, upBound=1)
allocation = np.array(dv_variables).reshape(number_of_days, number_of_days)
print('The example of decision variables')
display(allocation[:5][:5])
# initial model
model = pulp.LpProblem("ATM Replenishment", pulp.LpMinimize)

# add objective function
obj_func = pulp.lpSum(allocation * total_costs_arr)
model += obj_func

# add constrints
model += pulp.lpSum(allocation[0]) == 1 # need first day loading date
model += pulp.lpSum(allocation[i][-1] for i in range(number_of_days)) == 1 # make sure load until last day
# Assigning the next trigger date based on the first replenishment date. 
# For example, if the observed period is 5 days and the first schedule is X13 then force the next order to start with X4{4|5}.
for i in range(number_of_days):
    for j in range(number_of_days):
        if i == 0 and j == 0:
            const = pulp.lpSum(allocation[i+1][j+1:]) == pulp.lpSum(allocation[i][j])
            model += const
        elif i == j:
            const = pulp.lpSum(allocation[i+1][j+1:]) == pulp.lpSum([e[j] for e in allocation[:i+1]])
            model += const
        if i > number_of_days - 2:
            break

print(model)
# Solve model
model.solve()

# Display results
print(f"Model status : {pulp.LpStatus[model.status]}")
print(f"Total cost: {model.objective.value()}")
for v in model.variables():
    try:
        if v.value() == 1:
            print(f"{v.name} = {v.value()}")
    except:
        print('error could not found value')<jupyter_output>Model status : Optimal
Total cost: 15.01
x_13 = 1.0
x_45 = 1.0<jupyter_text>Reference- [1] <NAME> & Toroslu, Ismail & <NAME>. (2020). [Comparison of Integer Linear Programming and Dynamic Programming Approaches for ATM Cash Replenishment Optimization Problem.](https://www.igi-global.com/gateway/article/251842) International Journal of Applied Metaheuristic Computing. 11. 120-132. 10.4018/IJAMC.2020070107.<jupyter_code><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12765598650927487, 'fraction_numerical': 0.021247892074198988, 'mean_word_length': 4.797653958944282, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 22, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '580601', 'license': 'MIT', 'n_tokens_mistral': 2053, 'n_tokens_neox': 1906, 'n_words': 535, 'path': 'ipynb/atm_replenishment.ipynb', 'repo_name': 'Pathairush/ATM_optimization', 'size': '5945'}