%%ampl_eval
reset ;
### SETS & PARAMETERS
## Construction work
param T := 16; # Planning horizon (in months)
set BUILD := {'B1','B2','B3'}; # Set of buildings under construction
set CHAR := {'area','start','duration'}; # Set of characteristics of buildings (area, start time, and construction duration)
set LINKS within {BUILD, 1..T}; # Set of building-period pairs for calculations
param buildData {BUILD, CHAR} >= 0; # Parameters for each building under construction (e.g., area, start, duration)
param buildCost {LINKS} >= 0; # Monthly construction costs for each building
param basePrice {LINKS} >= 0; # Monthly selling base Price per square meter (basePrice increases with construction progress)
param NomArea {BUILD} >= 0; # Maximum nominal sales capacity (in sq.m.) per month for each building
# Valid combinations of buildings (b) and time periods (t) where advertising demand can be considered
set LINKS_AD_DEMAND = {b in BUILD, t in buildData[b,'start']-1..(buildData[b,'start'] + buildData[b,'duration'])-1: t > 1};
## Demand (based on price and seasonal elasticity)
param nStep integer > 0; # Number of Price steps
param priceStep {1..nStep+1} >= 0; # Price step values – defining different price levels that can be set for the sale of the building area
param demandCoef {1..nStep} >= 0; # Coefficients representing price elasticity of demand for each price step. A higher coefficient means higher demand sensitivity to price changes.
param SeasonalEffect {1..T} >= 0; # Coefficient for seasonal elasticity of demand per month, representing the impact of seasonal trends on sales
# Limit on the amount of building area that can be sold in month t at price step n.
param limit {(b,t) in LINKS, n in 1..nStep} =
buildData[b,'area'] * NomArea[b] * SeasonalEffect[t] * demandCoef[n];
# The sales rate at which a certain amount of area can be sold, calculated as the product of base price and price step.
param rate {(b,t) in LINKS, n in 1..nStep} = basePrice[b,t] * priceStep[n] ;
# The incremental rate (or marginal rate) of revenue between consecutive price steps.
param marg_rate {(b,t) in LINKS, n in 1..nStep-1} =
((limit[b,t,n+1]-1) * rate[b,t,n+1] - limit[b,t,n] * rate[b,t,n]) /
(limit[b,t,n+1]-limit[b,t,n]) ;
## Advertising
param adEff {BUILD} >= 0; # Effectiveness of advertising in increasing sales per dollar invested
## Deposits
set DEP := {'I','III','VI','XII'}; # Types of deposit products (I: 1 month, III: 3 months, VI: 6 months, XII: 12 months)
# Deposit characteristics
set DEP_ATTR := {'term', 'freq_payment', 'refill', 'first_money', 'interest', 'capit_on'};
set AVAIL within {1..T, DEP}; # Availability of deposits for investment in each period
param depData {DEP, DEP_ATTR} >= 0; # Characteristics for each deposit product
# Link deposit opening periods to periods within its term
set DEP_LINKS = {(tp,d) in AVAIL, t in tp+1..tp + depData[d,'term']: t <= T+1};
# Link deposit opening periods to periods within its term (for replenishment and withdrawals)
set DEP_LINKS_ = {(tp,d,t) in DEP_LINKS: t <= T and t < tp + depData[d,'term']} ;
# Number of interest payments during the deposit term
param nInterestPayments{d in DEP} = depData[d,'term'] / depData [d, 'freq_payment'];
# Interest rate per payment period for each deposit type
param RatePerFreq{d in DEP} = (depData[d,'interest'] / 1200) * depData [d, 'freq_payment'] ;
## Cashflow & Investment schedule
param MoneyWithdrawn {1..T} >= 0 ; # Project's funds withdrawal schedule, indicating the amount of money
### DECISION VARIABLES
var SqmSold {LINKS, 1..nStep} >= 0; # Number of square meters sold for a building in a period at a given price step
var SaleDec {LINKS, 1..nStep} binary; # Binary variable indicating whether sales occurred at a given price step for a building in a period
var AdSpent {BUILD, 1..T} >= 0; # Amount of money spent on advertising for each building in each period
# AdDemand represents the additional demand generated due to advertising
# The equation multiplies the advertising spent by its effectiveness and divides it by the base price to model how much additional demand is created for each unit of advertising, adjusting for the price sensitivity of the market.
var AdDemand {(b,t) in LINKS_AD_DEMAND} =
(AdSpent[b,t-1] * # The amount of money spent on advertising for building b in the previous period (t-1)
adEff[b] / # how much demand increases per dollar spent on advertising. A higher value indicates more efficient advertising
basePrice[b,t]) * # The additional demand generated is inversely proportional to the base price. Lower base prices lead to a greater increase in demand for a given amount of advertising expenditure.
SeasonalEffect[t] ; # Coefficient for seasonal elasticity of demand per month, representing the impact of seasonal trends on sales
# Monthly profit from sales for each building in each period, considering price steps
var Monthly_Profit_From_Sales{(b,t) in LINKS} =
sum{n in 1..nStep-1} # Summation over all price steps (n) except the highest (nStep). This accumulates the total profit for each building (b) and period (t).
<< limit[b,t,n]; # Maximum amount of square meters that can be sold at price step n for building b in period t.
rate[b,t,n], # The sales rate (price per square meter) at the price step n.
marg_rate[b,t,n]>> # The marginal rate (additional profit contribution) at price step n.
SqmSold[b,t,n] + # Number of square meters sold for a building in a period at a given price step
if t > 1 then AdDemand[b,t] * basePrice[b,t]; # Sales from advertising
## Deposit variables
var IsDepositOpen {AVAIL} binary; # Binary variable indicating whether a deposit is opened
var DepositAmt {AVAIL} >= 0; # Amount invested in deposits
var ReplenishAmt {DEP_LINKS_} >= 0 ; # Amount replenished in the deposit during its term
var WithdrawalAmt{DEP_LINKS_} >= 0 ; # Amount withdrawn from the deposit during its term
# Define the accrued interest variable for each deposit product d starting in period tp.
var Accrued_Interest {(tp,d,t) in DEP_LINKS} = # % Income is calculated for each period t0. The range of t values for each t0 is within the boundaries t < t0 <= t + depData[d,'term']
# Check if the current period 't' is a scheduled interest payment period for deposit 'd'
if exists {k in 1..nInterestPayments[d]} t = tp + depData[d, 'freq_payment'] * k then
RatePerFreq[d] * DepositAmt[tp,d] + # Calculate interest for the base deposit amount
# Sum the interest from previous periods ('tt') for which interest was already accrued
# The sum includes the accrued interest over all periods 'tt' less than the current period 't'
sum{(tp,d,tt) in DEP_LINKS_: tt < t} RatePerFreq[d] *
# If the deposit 'd' has compounding (depData[d,'capit_on'] = 1), include interest accrued in previous periods
# If compounding is disabled, this part of the calculation is ignored (added as 0)
(if depData[d,'capit_on'] = 1 then Accrued_Interest[tp,d,tt] else 0 +
# If the previous period 'tt' is within one frequency payment period before 't',
# calculate the proportion of replenishment and withdrawal amounts to account for in the interest
if tt > t - depData[d, 'freq_payment'] then tt/depData[d, 'freq_payment'] *
(ReplenishAmt[tp,d,tt] # The amount replenished during period 'tt'
- WithdrawalAmt[tp,d,tt]) # The amount withdrawn during period 'tt'
# For periods 'tt' outside the current interest payment window, simply apply the full replenishment
# and withdrawal amounts without fractioning them based on the payment period frequency
else (ReplenishAmt[tp,d,tt] - WithdrawalAmt[tp,d,tt])) ;
# Define the total value of the deposit at the time of its closure (when its term ends)
# - tp: The period when the deposit was opened
# - d: The type of deposit product (e.g., 1-month, 3-month)
# - t: The period when the deposit matures (equals tp + deposit term)
# The condition `t = tp + depData[d,'term']` ensures this variable is only active when the deposit reaches its maturity.
var DepositClose {(tp,d,t) in DEP_LINKS: t = tp + depData[d,'term']} =
DepositAmt[tp,d] # Initial amount deposited when the deposit was opened
+ sum {(tp,d,tt) in DEP_LINKS} Accrued_Interest[tp,d,tt] # Add the total accrued interest over the term of the deposit.
+ sum {(tp,d,tt) in DEP_LINKS_: tt < t} # Add the total replenishment minus withdrawals over the term of the deposit, but only up to the period before the deposit closes.
(ReplenishAmt[tp,d,tt] # The amount replenished to the deposit during period tt.
- WithdrawalAmt[tp,d,tt]);
## Account Status for the Current Period
var Account {t in 1..T+1} = # The account balance at time period t, where t ranges from 1 to T+1 (including an extra period for end-of-planning balances)
sum {(b,t) in LINKS} ( # Loop over each building and time period (b,t) in the set of valid building-period pairs (LINKS)
Monthly_Profit_From_Sales[b,t] # Add the monthly profit from sales of building b in period t
- buildCost[b,t]) # Subtract the construction cost for building b in period t
- sum{b in BUILD: t <= T} AdSpent[b,t]# Subtract the advertising spent for building b in period t
- sum {(t,d) in AVAIL} DepositAmt[t,d] # Subtract the amount invested in deposits in period t for each deposit d available in that period
+ sum {(tp,d) in AVAIL: tp = t - depData[d,'term']} # Add the deposit amount from closed deposits, where tp is the opening period, and deposits close after depData[d,'term'] periods
DepositClose[tp,d,t] # Add the initial contribution of the deposit that is closing in the current period t
- sum {(tp,d,t) in DEP_LINKS_} # For each active deposit (tp, d), where tp is the opening period and t is the current period
(ReplenishAmt[tp,d,t] # Subtract the amount of money replenished into the deposit in the current period t
- WithdrawalAmt[tp,d,t]) # Subtract the amount withdrawn from the deposit in the current period t
- sum {i in 1..1: t <= T} MoneyWithdrawn[t]; # Subtract any external withdrawals (MoneyWithdrawn) in the current period t (e.g., funds withdrawn from the project for non-investment purposes)
### OBJECTIVE FUNCTION
maximize Total_revenue: sum{t in 1..T+1} Account[t] ; # Maximize total revenue by summing account balances over all periods
### CONSTRAINTS
## 1. Ensure total square footage sold does not exceed the building's area
AreaLimit_SqmSold {b in BUILD}:
sum {(b,t) in LINKS} (
(if t > 1 then AdDemand[b,t]) + sum{n in 1..nStep} SqmSold[b,t,n]) <= buildData [b,'area'];
## 2. Ensure only one price can be assigned for each period and product
PriceAssignment_Limit {(b,t) in LINKS}: sum {n in 1..nStep} SaleDec [b,t,n] <= 1;
## 3a. Constraint for monthly sales volume, considering demand, price elasticity, and seasonal effects
/*Min_MonthlySalesVolume {(b,t) in LINKS, n in 1..nStep-1}:
SqmSold[b,t,n] >= # SqmSold (square meters sold) <= Demand * Price Elasticity * Seasonal Elasticity
limit[b,t,n] * # Limit on the amount of building area that can be sold in month t at price step n
SaleDec[b,t,n] ; */ # Decision on how much to sell in period (h,t)
## 3b.
Max_MonthlySalesVolume {(b,t) in LINKS, n in 1..nStep-1}:
SqmSold[b,t,n] <=
limit[b,t,n+1] *
SaleDec[b,t,n];
## 3c. Advertising can increase demand by no more than 50% of the volume of basic demand.
# Link AdDemand & SqmSold with SaleDec
Max_MonthlySalesVolume_ {(b,t) in LINKS}:
(if t > 1 then AdDemand[b,t]) + sum{n in 1..nStep-1}(SqmSold[b,t,n]
- 1.5 * limit[b,t,n+1] * SaleDec[b,t,n]) <= 0 ;
## 4. Ensure that prices remain stable or increase throughout the sales period
stablePrice {(b,t) in LINKS: t > buildData[b,'start']-1}:
sum {n in 1..nStep} SaleDec[b,t,n] * priceStep[n] * basePrice[b,t] >= # Current period sales price.
sum {n in 1..nStep} SaleDec[b,t-1,n] * priceStep[n] * basePrice[b,t-1]; # Previous period sales price.
## 5. Ensure that the cumulative account balance is non-negative for all periods
Bal_Account_Pos {t in 1..T+1}: sum{tt in 1..t} Account[tt] >= 0;
### Deposit Constraints
## 6. Ensure the deposit amount is at least the initial required amount if a deposit is open
Min_Deposit_Amount {(t,d) in AVAIL}: DepositAmt[t,d] >= IsDepositOpen[t,d] * depData[d,'first_money'];
## 7. To limit the maximum amount that can be withdrawn from a deposit in a given period (t)
s.t. Max_Withdrawal{(tp,d,t) in DEP_LINKS_}:
WithdrawalAmt[tp,d,t] <= # The amount withdrawn at time t should not exceed:
#DepositAmt[tp,d] + # The original deposited amount is usually used as a reference, but it may be excluded here for a specific reason
sum{(tp,d,tt) in DEP_LINKS_: tt <= t} ( # The sum of accrued interest up to time t, which will be the main component determining how much can be withdrawn
Accrued_Interest[tp,d,tt] # The interest accrued on the deposit up to the current period tt
#+ ReplenishAmt[tp,d,tt] # Optionally, replenish amounts could be included, depending on whether replenishments are allowed in the withdrawal calculation
- WithdrawalAmt[tp,d,tt]) ; # Subtract previous withdrawals up to time tt to ensure the total withdrawn does not exceed the accrued interest and available balance