HLPL / VerTeX Modelling Language Documentation

← examples

# 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.