# Office Allocation and Time Slot Scheduling
# Assigns staff members to offices and time slots based on their preferences (bids)
# Subject to capacity, duration, and scheduling constraints
# Demonstrates advanced set functions with dynamic exclusion ranges
model office_allocation
set D = loadDays() # Days in the planning period
set T = loadTimes() # Available time slots
set O = loadOffices() # Available offices
set S = loadStaff() # Staff members
set L = loadLots() # Duration options (time blocks)
# Staff preferences and requirements
const r = loadDurations(), forall s in S, forall d in D # Required duration for staff s on day d
const b = loadBids(), forall s in S, forall d in D, forall l in L # Bid value for duration l
# Decision variables
bin x, forall s in S, forall d in D forall o in O, forall t in T # 1 if staff s uses office o at time t on day d
var 0 <= y <= 1, forall s in S, forall d in D, forall o in O, forall l in L # 1 if staff s is assigned duration l in office o on day d
# Objective: Maximize total satisfaction based on bid values
max sum_{s in S}{sum_{d in D}{sum_{o in O}{sum_{l in L}{b_{s,d,l} * y_{s,d,o,l}}}}}
# Each staff member can be assigned at most one duration per office per day
constr sum_{l in L}{y_{s,d,o,l}} <= 1, forall s in S, forall d in D, forall o in O
# Each staff member can use at most one office at any given time
constr sum_{o in O}{x_{s,d,o,t}} <= 1, forall s in S, forall d in D, forall t in T
# Each office can be used by at most one staff member at any given time
constr sum_{s in S}{x_{s,d,o,t}} <= 1, forall d in D, forall t in T, forall o in O
# Total time slots assigned cannot exceed required duration
constr sum_{o in O}{sum_{t in T}{x_{s,d,o,t}}} <= r_{s,d}, forall s in S, forall d in D
# Link time slot assignments to duration assignments
# If assigned duration l, must use exactly r * l time slots
constr sum_{o in O}{sum_{t in T}{x_{s,d,o,t}}} <= sum_{o in O}{sum_{l in L}{r_{s,d} * y_{s,d,o,l}}}, forall s in S, forall d in D
# Duration block constraint with dynamic exclusion range
# If assigned duration l in office o, must occupy at least l consecutive time slots
# Uses set function g() to exclude time slots outside the valid range
constr sum_{k in T setminus {g(l, s, d, |T|)}}{x_{s,d,o,k}} >= l * y_{s,d,o,l},
forall s in S, forall d in D, forall o in O, forall l in L
end
import elytica
import json
import random
# Global data dictionary
data = None
def g(r, s, d, T):
"""
Set function: Returns time slots that are OUTSIDE the valid window
for a staff member's scheduled block.
This is used with 'setminus' to constrain assignments to valid time slots.
For a duration r starting at time t, valid slots are [t, t+r-1].
This function returns all OTHER slots to exclude them.
"""
result = []
t = data["starts"][int(s)][int(d)] # Get start time for staff s on day d
for j in range(1, int(T) + 1):
k = int(t) + int(r) - 1 # Calculate end time of block
if j < int(t) or j > k: # If outside valid range
result.append(j)
return result
def defineStarts():
"""
Generate random start times for each staff member on each day.
Returns nested dictionary: {staff_id: {day_id: start_time}}
"""
results = {}
for staff in data["staff"]:
result = {}
for day in data["day"]:
result[day] = random.randint(1, len(data["times"]))
results[staff] = result
return results
def loadDurations():
"""
Generate random duration requirements for each staff member on each day.
Duration is expressed in number of time slots needed.
"""
results = []
for staff in data["staff"]:
result = []
for day in data["day"]:
result.append(random.randint(1, len(data["lots"])))
results.append(result)
return json.dumps(results)
def loadBids():
"""
Generate bid values for each staff member's preference for different duration blocks.
Higher bids indicate stronger preference for longer blocks.
"""
results = []
for staff in data["staff"]:
result = []
for day in data["day"]:
lot_result = []
for lot in data["lots"]:
# Bid value increases with lot size plus random factor
lot_result.append(random.uniform(1.0, 2.0) * lot)
result.append(lot_result)
results.append(result)
return json.dumps(results)
def loadStaff():
"""Return list of staff member IDs"""
return json.dumps(data["staff"])
def loadDays():
"""Return list of day IDs"""
return json.dumps(data["day"])
def loadTimes():
"""Return list of time slot IDs"""
return json.dumps(data["times"])
def loadOffices():
"""Return list of office IDs"""
return json.dumps(data["offices"])
def loadLots():
"""Return list of duration lot sizes"""
return json.dumps(data["lots"])
def getResults():
"""
Extract solution and format for visualization.
Returns list of assignments with day, office, time, staff name, and color.
"""
results = []
for s in data["staff"]:
for d in data["day"]:
for o in data["offices"]:
for t in data["times"]:
xname = "x" + ','.join([str(z) for z in [s, d, o, t]])
xval = elytica.get_variable_value("office_allocation", xname)
if xval > 0.5:
# Generate unique color for each staff member
hs = hash(str(s))
hsr = hs % 256
hsg = (hs >> 8) % 256
hsb = (hs >> 16) % 256
col = '#%02x%02x%02x' % (hsr, hsg, hsb)
entry = {
"day": d,
"office": o,
"time": t,
"staff_name": data["names"][s - 1],
"staff_id": s,
"color": col
}
results.append(entry)
return results
def getTimeframes():
"""Define time slot labels for visualization"""
return [
{"id": 0, "name": "Time", "color": "#defeee"},
{"id": 1, "name": "10:00 - 11:00", "color": "#d0f0e0"},
{"id": 2, "name": "11:00 - 12:00", "color": "#d0f0e0"},
{"id": 3, "name": "12:00 - 13:00", "color": "#d0f0e0"},
{"id": 4, "name": "13:00 - 14:00", "color": "#d0f0e0"},
{"id": 5, "name": "14:00 - 15:00", "color": "#d0f0e0"},
{"id": 6, "name": "15:00 - 16:00", "color": "#d0f0e0"},
{"id": 7, "name": "16:00 - 17:00", "color": "#d0f0e0"}
]
def main():
global data
# Initialize problem data
data = {
"staff": [i for i in range(1, 5)], # 4 staff members
"day": [1, 2, 3, 4, 5], # 5 days
"lots": [1, 2, 3, 4], # Duration options: 1-4 time slots
"offices": [i for i in range(1, 5)], # 4 offices
"times": [1, 2, 3, 4, 5, 6, 7], # 7 time slots per day
"names": [f"Staff_{i}" for i in range(1, 5)], # Staff names
"starts": {}
}
# Generate random start times
data["starts"] = defineStarts()
# Initialize and solve the model
elytica.init_model("office_allocation")
elytica.set_gap_limit("office_allocation", 0.5) # Allow 50% optimality gap for faster solve
elytica.run_model("office_allocation")
print("\n" + "="*70)
print("OFFICE ALLOCATION SOLUTION")
print("="*70)
# Get and display results
floor_assignments = getResults()
timeframes = getTimeframes()
print(f"\nTotal Assignments: {len(floor_assignments)}")
print("\nSample Assignments:")
print("-"*70)
# Display first 10 assignments
for assignment in floor_assignments[:10]:
print(f" Day {assignment['day']}, Office {assignment['office']}, "
f"Time {assignment['time']}: {assignment['staff_name']}")
if len(floor_assignments) > 10:
print(f" ... and {len(floor_assignments) - 10} more assignments")
# Get objective value
obj_value = elytica.get_best_primal_bound("office_allocation")
print("\n" + "-"*70)
print(f"Total Satisfaction Score (Objective Value): {obj_value:.2f}")
print("="*70)
# Prepare results for output
results = {
"objective_value": obj_value,
"floor": floor_assignments,
"timeframes": timeframes,
"summary": {
"num_staff": len(data["staff"]),
"num_days": len(data["day"]),
"num_offices": len(data["offices"]),
"num_time_slots": len(data["times"]),
"total_assignments": len(floor_assignments)
}
}
elytica.write_results(json.dumps(results, indent=2))
# Optionally save the model for debugging
# elytica.save_model("office_allocation")
return 0
Read this through MCP: read with path: "examples/office-allocation", or resource hlpl://examples/office-allocation.