Testing Strategy Optimization¶
Problem Statement¶
Smartly decide which machines to run tests on and what kind of testing environment to simulate.
Full example scenario
A DevOps engineer wants to optimize the testing strategy for a software application that must be tested on four operating systems — Linux64, Armlinux64, MacOS, and Windows — each exactly once. There are 10 testing environments, numbered 1-10, and 40 testing machines with 10 dedicated to each OS, where each machine can only handle a subset of the testing environments. Which {machine, testing environment} combination should be chosen for each operating system, in order to maximize the total score?
The score for a given combination is the number of days since that machine/environment pair’s last testing date, multiplied by its modifier. The number of days matters because you do not want to keep testing on the same machine over and over — repeatedly leaning on the same machine risks uneven wear, degraded reliability, and results that no longer reflect a fresh environment, so the model favors machine/environment pairs that have sat idle the longest. The modifier is not spelled out in the data, but a multiplier layered on top of idle-days is a natural way to encode something else that matters besides recency — for example, how capable a machine is (a higher-spec rig is worth prioritizing sooner), how critical the environment it is wired to is (a production-like environment may be worth refreshing more urgently), or a general reliability/priority weighting from past runs. Whatever its exact business meaning, the model treats it the same way: a multiplier on top of days idle, so a fresher-but-lower- priority pair can still lose out to an older-but-higher-priority one.
Each operating system must be tested exactly once, on one of its OS-specific machines.
Each testing environment can be chosen at most once across all operating systems.
A {machine, testing environment} combination can only be chosen if that machine actually supports the given environment.
The compatibility and scoring data can be found in
testing_strategy_optimization.csv, with the columnsOS,Testing Environment,Machine,Testing Date,Modifier.
What an optimal solution looks like
The compatibility grid — which environment each machine is wired to, and how long it has sat idle.
Every OS has its own 10 dedicated machines, numbered #1 – #10. Each machine is wired to exactly one testing environment (colored badge), so picking a machine also picks its environment. Showing #1 – #3 and #10 here; #4 – #9 follow the same pattern.
| OS | Machine # | ||||
|---|---|---|---|---|---|
| #1 | #2 | #3 | … | #10 | |
| Linux64 | Env #1 32d idle |
Env #1 18d idle |
Env #2 23d idle |
… | Env #4 3d idle |
| Armlinux64 | Env #1 32d idle |
Env #2 18d idle |
Env #3 23d idle |
… | Env #2 3d idle |
| MacOS | Env #2 32d idle |
Env #1 18d idle |
Env #4 23d idle |
… | Env #1 3d idle |
| Windows | Env #4 32d idle |
Env #4 18d idle |
Env #3 23d idle |
… | Env #1 3d idle |
✓ Results — the plan the optimizer actually picked.
| OS | Machine # | Environment | Days idle | Modifier | Score |
|---|---|---|---|---|---|
| Armlinux64 | #5 | Env #1 | 21 | 2 | 42 |
| MacOS | #5 | Env #2 | 21 | 2 | 42 |
| Linux64 | #5 | Env #3 | 21 | 2 | 42 |
| Windows | #2 | Env #4 | 18 | 2 | 36 |
| Max total score | 162 | ||||
Built with Modeler¶
The problem above is stated as a single shot: you hand over the whole problem and get a model back. Modeler takes the other route — you start with the business question you actually have, and it works with you until the problem is pinned down, the data is in, the code runs, and the answer holds up when the problem changes.
The tabs below walk the same testing-strategy problem through that route, one Modeler capability at a time.
Instead of writing the whole prompt up front, you start with the business question you actually have. Modeler searches the knowledge base, asks one clarifying question at a time — always the most important one first — and tells you which parts of the model are still missing. The options it offers come from examples in the knowledge base and from how similar problems are usually modeled in your industry, so you can answer with a number instead of prose.
I’m a DevOps engineer. Before every release we need to test our application on several operating systems, and we have a pool of testing machines to run those tests on, plus a number of testing environment “slots” we can assign work to. Each machine can only be plugged into certain slots (not all combinations work), and I want to pick, for each OS, which machine and which slot to run it on, so as to maximize some kind of overall score for the choices we make — the score depends on how long it’s been since that particular machine/slot combo was last used for testing and a modifier value tied to it. Can you help me build this as an optimization model?
This has the shape of a resource assignment problem — OS, machine, and slot form one combined decision, with two separate uniqueness constraints in play (slot and machine). Let me check the knowledge base for how similar assignment problems are formulated before asking anything.
Good — the knowledge base points at the standard resource-assignment pattern. The one thing that changes the entire variable and constraint structure is how “recency” and “modifier” actually combine into the number being maximized, so that has to be the first question.
Good start. Here is my first question:
Question 1 of several: How is the assignment score calculated?
You mentioned it depends on two things: (1) how long ago the machine-slot pair was last used, and (2) a modifier value. Which of the following best describes how those two factors are combined into a single score?
The score is the product of the two factors:
days_since_last_use x modifierThe score is a weighted sum:
w1 * days_since_last_use + w2 * modifierThe modifier is applied on top of a base score derived from recency (e.g., a lookup table maps recency to a base score, then multiplied by the modifier)
The score is just the modifier value, and recency is used only as a tiebreaker
Something else — please describe
Option 1 — the score is the product: days_since_last_use * modifier. Here’s the rest of the picture: we have 4 operating systems (Linux64, ArmLinux64, MacOS, Windows), and each one must be tested exactly once. There are 10 testing environment slots, numbered 1-10, and each slot can be used by at most one OS overall. We have 40 physical testing machines, 10 dedicated to each OS, and each machine is only wired up to a specific subset of the 10 slots — so the machine and slot choice for an OS are linked, not independent; you can only pick a (machine, slot) pair if that machine actually supports that slot. One more requirement: maximize the total score summed over the 4 assignments we make (one per OS).
That resolves the score formula and volunteers the entire rest of the structure at once: OS count, slot uniqueness, machine-to-slot wiring as the source of validity, and a single additive objective. Nothing about the business rules is still open — what remains is purely a data-shape question (how the compatibility and score data will actually arrive), which decides variable and constraint construction just as much as anything asked so far.
The specification draft is saved. Here is the next question:
Question 2: How will the input data be provided to the model?
Specifically, for the valid (machine, slot) pairs along with their
days_since_last_use and modifier values — which of the
following best describes the expected input format?
As pandas DataFrames passed directly into the solver class or function (data is generated or loaded by the caller)
As CSV or JSON files that the model reads from disk at runtime
As plain Python lists or dictionaries hardcoded or constructed in the calling code
The model should include a built-in data generator that creates realistic sample data for the 4 OSes / 40 machines / 10 slots setup, so it can be run standalone
A combination — for example, a built-in sample dataset for testing, but the solve function accepts DataFrames for production use
Modeler updates the specification after every answer, even while it is still incomplete — so you can always see what it has understood and what it is still waiting on. Here, the one open item left is not a business rule but a data-shape choice, which is exactly what the next tab walks through.
With the score formula settled, the one thing still open was a data-shape question — how the compatibility and score data would actually arrive. Attach the real file and Modeler answers that question and reads the real structure in one step: entity names, column headers, and the shape of what’s actually wired up, rather than guessing from a description.
testing_strategy_optimization.csv:
OS,Testing Environment,Machine,Testing Date,Modifier
Linux64,1,Linux1,2024-06-01,1
Linux64,1,Linux2,2024-06-15,2
Linux64,2,Linux3,2024-06-10,1
Linux64,2,Linux4,2024-06-05,1
Linux64,3,Linux5,2024-06-12,2
Linux64,3,Linux6,2024-06-08,1
Linux64,3,Linux7,2024-06-20,2
Linux64,4,Linux8,2024-06-18,1
Linux64,4,Linux9,2024-06-25,2
Linux64,4,Linux10,2024-06-30,1
Armlinux64,1,ArmLinux1,2024-06-01,1
Armlinux64,2,ArmLinux2,2024-06-15,2
Armlinux64,3,ArmLinux3,2024-06-10,1
Armlinux64,4,ArmLinux4,2024-06-05,1
Armlinux64,1,ArmLinux5,2024-06-12,2
Armlinux64,2,ArmLinux6,2024-06-08,1
Armlinux64,3,ArmLinux7,2024-06-20,2
Armlinux64,4,ArmLinux8,2024-06-18,1
Armlinux64,1,ArmLinux9,2024-06-25,2
Armlinux64,2,ArmLinux10,2024-06-30,1
MacOS,2,MacOS1,2024-06-01,1
MacOS,1,MacOS2,2024-06-15,2
MacOS,4,MacOS3,2024-06-10,1
MacOS,3,MacOS4,2024-06-05,1
MacOS,2,MacOS5,2024-06-12,2
MacOS,1,MacOS6,2024-06-08,1
MacOS,4,MacOS7,2024-06-20,2
MacOS,3,MacOS8,2024-06-18,1
MacOS,2,MacOS9,2024-06-25,2
MacOS,1,MacOS10,2024-06-30,1
Windows,4,Windows1,2024-06-01,1
Windows,4,Windows2,2024-06-15,2
Windows,3,Windows3,2024-06-10,1
Windows,3,Windows4,2024-06-05,1
Windows,2,Windows5,2024-06-12,2
Windows,2,Windows6,2024-06-08,1
Windows,2,Windows7,2024-06-20,2
Windows,1,Windows8,2024-06-18,1
Windows,1,Windows9,2024-06-25,2
Windows,1,Windows10,2024-06-30,1
Option 5 — a combination: the solve function/class should accept the data as pandas DataFrames (machine/slot compatibility, days-since-last-use, and modifier), and it’s fine to also have a small built-in sample dataset for standalone testing. Here’s the real data file we use today, covering all 4 OSes: attached, testing_strategy_optimization.csv, with columns OS, Testing Environment, Machine, Testing Date, Modifier. “Testing Environment” is the slot number, “Testing Date” is the date the machine/slot pair was last used for testing (so days_since_last_use = today minus that date), and the rows only list (machine, slot) pairs that are actually valid/wired up for that OS’s machines.
Reading the attachment: 40 rows, 4 OSes, columns already line up with what the specification expects. Only 4 distinct slot numbers actually appear in this file even though up to 10 were mentioned — that’s not a data problem, it just means slots 5-10 have no wired machines in this batch and will never be chosen. One thing still needs pinning down before implementation: “today” is a moving target, so the exact reference point for computing days_since_last_use has to be a real decision, not an assumption.
The specification has been updated with all the details from your CSV file. Here is the next question:
Question 3: How should the reference date for computing
days_since_last_use be handled?
In production, “today” is the natural reference point, but this affects reproducibility (the score of a given pair changes every day). Which approach would you prefer?
Always use today’s actual date at runtime (
datetime.date.today()) — scores are always fresh and reflect real elapsed timeAccept an optional
reference_dateparameter in the solve function, defaulting to today if not provided — gives full control for testing and reproducibilityHardcode the reference date in the model (e.g., always use the date the model was built) — simpler but less flexible
Pre-compute
days_since_last_useoutside the model and pass it in as a column in the DataFrame — the model never deals with dates directly
Where your files live
Everything in the conversation — the file you attached and every artifact Modeler generates from it — lands in the conversation workspace as an attachment. Each one opens in a side panel next to the chat, so you can read the specification while you keep answering questions, or scroll the code while you ask for a change. From that panel you can copy the contents or download the file.
By the end of this conversation the workspace holds six items:
Attachment |
What it is |
|---|---|
|
The OS/machine/slot compatibility and scoring data you attached. |
|
The specification, rewritten after every answer. |
|
The |
|
The |
|
Output of the test runs, including the solver logs. |
|
The mathematical formulation, generated on request. |
Starting from files you already have
The same upload path runs in reverse. Attach the files of an existing
optimization project — a specification, a model script, a test file, or any
subset — and ask Modeler to check the project. It completes the
specification where it is thin (asking clarifying questions, as above), fills
in a missing implementation or test suite, reconciles code that has drifted
from the specification, and runs the tests. Point it at a model written for
another solver or another language and it recovers a specification from that
file, asks you to confirm it, then reimplements the model in gurobipy.
The specification is the artifact the questions are building toward, and the one everything else is generated from. Modeler rewrites it after every answer — even while it is still incomplete — so it doubles as the running record of what has been agreed and what is still open. It follows a fixed seven-section structure and is written entirely in business terms: no mathematics, so the person who owns the problem can read it and say whether it is right.
1. Overview
Before each software release, a DevOps team must test their application on a fixed set of operating systems. They maintain a pool of physical testing machines and a numbered set of testing environment slots. Each machine is only physically wired to a subset of the available slots, so not every (machine, slot) combination is valid. The goal is to assign, for each operating system, exactly one (machine, slot) pair — chosen from the valid pairs for that OS — while ensuring no slot is used more than once across all OS assignments, and maximizing the total assignment score.
2. Objective
Maximize the total assignment score across all OS assignments, where the score for each selected (machine, slot) pair is:
score = days_since_last_use × modifier
Favoring pairs that have not been used recently helps balance load and wear across the testing infrastructure.
3. Constraints
C1 — Each OS must be assigned exactly once. Every operating system must be assigned exactly one valid (machine, slot) pair. No OS may be tested twice or left untested.
C2 — Each slot used at most once. A testing environment slot can be used by at most one OS across all assignments.
C3 — Each machine used at most once. A physical machine can be assigned to at most one OS (each machine is dedicated to a single OS by design, but is still only used once per release cycle).
C4 — Valid (machine, slot) pairs only. An OS may only be assigned to a (machine, slot) pair that appears in the compatibility list for that OS. Assignments to unsupported machine-slot combinations are not allowed.
4. Input
Entity |
Fields |
Values |
|---|---|---|
Compatibility and score |
os, machine, slot, testing_date, modifier |
40 valid (OS, machine, slot) triples: 4 OSes (Linux64, Armlinux64, MacOS, Windows), 40 machines (10 per OS), slots numbered 1-10 (only 1-4 have wired machines in this dataset) |
Reference date (optional) |
reference_date |
Defaults to today’s date; used to compute days_since_last_use = reference_date − testing_date |
Every OS has candidate pairs on all 4 wired slots, so on paper there is no shortage of options — but each slot can only go to one OS overall, which is what makes the assignment a genuine trade-off rather than four independent choices.
5. Output
The optimal assignment: for each OS, its assigned machine and slot.
The individual score for each assignment (days_since_last_use × modifier).
The total assignment score across all 4 OSes.
6. Test scenarios
ID |
Scenario |
Acceptance criteria |
|---|---|---|
T1 |
Baseline — the full 40-row real dataset, reference date 2024-07-03. |
A feasible, optimal solution: all 4 OSes assigned, no two OSes share a slot or a machine, every assigned pair is valid, and each score equals days_since_last_use × modifier. |
T2 |
Slot uniqueness enforced — two OSes both prefer the same slot. |
The two OSes are assigned to different slots, and the total score equals the sum of their best individually-achievable scores on separate slots. |
T3 |
Infeasible — two OSes share the only slot that is wired to either of them. |
Reported as infeasible; no assignment is returned. |
T4 |
Single OS, single valid (machine, slot) pair. |
The OS is trivially assigned to that machine and slot, with the score computed exactly from the input data. |
T5 |
Global optimum vs. greedy — the highest-scoring individual choice for one OS would block the only slot available to another. |
The model finds the globally optimal combination rather than greedily maximizing one OS’s score at the expense of feasibility. |
7. Out of scope
Assigning more than one OS to the same machine in the same release cycle.
Partial or fractional assignments.
Time-window or scheduling constraints (e.g., tests must run in a certain order or within a time budget).
Dynamic re-assignment during a release cycle.
Costs or resource consumption beyond the slot uniqueness constraint.
With the specification settled, Modeler writes the gurobipy
implementation. This is the version that came out of the run-and-fix loop in
the Cloud solve tab, after Modeler ran the tests against it.
"""
OS Test Assignment Optimizer
This module implements an optimization model for assigning operating systems to
(machine, slot) pairs before a software release. The goal is to maximize the total
assignment score, where each score is computed as:
score = days_since_last_use * modifier
Subject to:
- Every OS is assigned exactly one (machine, slot) pair
- Each testing environment slot is used by at most one OS
- Each machine is used at most once
- Only valid (machine, slot) pairs for each OS are considered
"""
import datetime
import gurobipy as gp
from gurobipy import GRB
import pandas as pd
# ---------------------------------------------------------------------------
# Sample dataset (mirrors testing_strategy_optimization.csv for standalone demonstration)
# ---------------------------------------------------------------------------
SAMPLE_DATA = pd.DataFrame([
{"os": "Linux64", "slot": 1, "machine": "Linux1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "Linux64", "slot": 1, "machine": "Linux2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "Linux64", "slot": 2, "machine": "Linux3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "Linux64", "slot": 2, "machine": "Linux4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "Linux64", "slot": 3, "machine": "Linux5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "Linux64", "slot": 3, "machine": "Linux6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "Linux64", "slot": 3, "machine": "Linux7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "Linux64", "slot": 4, "machine": "Linux8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "Linux64", "slot": 4, "machine": "Linux9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "Linux64", "slot": 4, "machine": "Linux10", "testing_date": datetime.date(2024, 6, 30), "modifier": 1},
{"os": "Armlinux64", "slot": 1, "machine": "ArmLinux1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "Armlinux64", "slot": 2, "machine": "ArmLinux2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "Armlinux64", "slot": 3, "machine": "ArmLinux3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "Armlinux64", "slot": 4, "machine": "ArmLinux4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "Armlinux64", "slot": 1, "machine": "ArmLinux5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "Armlinux64", "slot": 2, "machine": "ArmLinux6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "Armlinux64", "slot": 3, "machine": "ArmLinux7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "Armlinux64", "slot": 4, "machine": "ArmLinux8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "Armlinux64", "slot": 1, "machine": "ArmLinux9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "Armlinux64", "slot": 2, "machine": "ArmLinux10","testing_date": datetime.date(2024, 6, 30), "modifier": 1},
{"os": "MacOS", "slot": 2, "machine": "MacOS1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "MacOS", "slot": 1, "machine": "MacOS2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "MacOS", "slot": 4, "machine": "MacOS3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "MacOS", "slot": 3, "machine": "MacOS4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "MacOS", "slot": 2, "machine": "MacOS5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "MacOS", "slot": 1, "machine": "MacOS6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "MacOS", "slot": 4, "machine": "MacOS7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "MacOS", "slot": 3, "machine": "MacOS8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "MacOS", "slot": 2, "machine": "MacOS9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "MacOS", "slot": 1, "machine": "MacOS10", "testing_date": datetime.date(2024, 6, 30), "modifier": 1},
{"os": "Windows", "slot": 4, "machine": "Windows1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "Windows", "slot": 4, "machine": "Windows2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "Windows", "slot": 3, "machine": "Windows3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "Windows", "slot": 3, "machine": "Windows4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "Windows", "slot": 2, "machine": "Windows5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "Windows", "slot": 2, "machine": "Windows6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "Windows", "slot": 2, "machine": "Windows7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "Windows", "slot": 1, "machine": "Windows8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "Windows", "slot": 1, "machine": "Windows9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "Windows", "slot": 1, "machine": "Windows10", "testing_date": datetime.date(2024, 6, 30), "modifier": 1},
])
class OSTestAssignmentOptimizer:
"""
Optimization model for assigning operating systems to (machine, slot) pairs
before a software release, maximizing the total assignment score.
Usage:
with gp.Env(empty=True) as env:
env.setParam('OutputFlag', 0)
env.start()
with OSTestAssignmentOptimizer(env) as optimizer:
optimizer.set_data(df, reference_date=datetime.date(2024, 7, 3))
optimizer.build_model()
optimizer.solve()
solution = optimizer.get_solution()
optimizer.display_results()
"""
def __init__(self, env):
"""Initialize the optimizer with a Gurobi environment."""
self.env = env
self.model = None
# Data attributes
self.df = None
self.reference_date = None
self.os_list = []
self.candidates = {} # (os, machine, slot) -> score
# Decision variable dict
self._assign = {}
def __enter__(self):
"""Enter context manager."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager and dispose model to free Gurobi resources."""
if self.model is not None:
self.model.dispose()
return False
# ------------------------------------------------------------------
# Data loading
# ------------------------------------------------------------------
def set_data(self, df: pd.DataFrame, reference_date: datetime.date = None):
"""
Load the compatibility and score data.
Parameters
----------
df : pd.DataFrame
Columns: os (str), machine (str), slot (int),
testing_date (datetime.date or parseable str), modifier (float)
reference_date : datetime.date, optional
The date used to compute days_since_last_use.
Defaults to today's date.
"""
self.df = df.copy()
self.reference_date = reference_date or datetime.date.today()
# Normalize testing_date to datetime.date
if not pd.api.types.is_datetime64_any_dtype(self.df["testing_date"]):
self.df["testing_date"] = pd.to_datetime(self.df["testing_date"]).dt.date
# Compute days_since_last_use and score for each valid combination
self.df["days_since_last_use"] = self.df["testing_date"].apply(
lambda d: (self.reference_date - d).days
)
self.df["score"] = self.df["days_since_last_use"] * self.df["modifier"]
self.os_list = self.df["os"].unique().tolist()
# Build candidate dict: (os, machine, slot) -> score
self.candidates = {
(row.os, row.machine, row.slot): row.score
for row in self.df.itertuples(index=False)
}
# ------------------------------------------------------------------
# Model building
# ------------------------------------------------------------------
def build_model(self):
"""Build the Gurobi MIP model."""
self.model = gp.Model("OSTestAssignment", env=self.env)
self._create_variables()
self._add_constraints()
self._set_objective()
def _create_variables(self):
"""
Create one binary decision variable per valid (os, machine, slot) triple.
x[os, machine, slot] = 1 if this triple is selected for the assignment.
"""
self._assign = self.model.addVars(
self.candidates.keys(),
vtype=GRB.BINARY,
name="assign"
)
def _add_constraints(self):
"""Add all business constraints to the model."""
# C1: Each OS must be assigned exactly one (machine, slot) pair
self.model.addConstrs(
(
gp.quicksum(
self._assign[os, machine, slot]
for (o, machine, slot) in self.candidates
if o == os
) == 1
for os in self.os_list
),
name="C1_os_assigned_once"
)
# C2: Each testing environment slot used by at most one OS
all_slots = self.df["slot"].unique().tolist()
self.model.addConstrs(
(
gp.quicksum(
self._assign[os, machine, slot]
for (os, machine, s) in self.candidates
if s == slot
) <= 1
for slot in all_slots
),
name="C2_slot_used_once"
)
# C3: Each machine used at most once
all_machines = self.df["machine"].unique().tolist()
self.model.addConstrs(
(
gp.quicksum(
self._assign[os, machine, slot]
for (os, m, slot) in self.candidates
if m == machine
) <= 1
for machine in all_machines
),
name="C3_machine_used_once"
)
def _set_objective(self):
"""Maximize the total assignment score (days_since_last_use * modifier)."""
self.model.setObjective(
self._assign.prod(self.candidates),
GRB.MAXIMIZE
)
# ------------------------------------------------------------------
# Solving
# ------------------------------------------------------------------
def solve(self):
"""Run the Gurobi optimizer."""
self.model.optimize()
return self.model.Status
# ------------------------------------------------------------------
# Solution retrieval
# ------------------------------------------------------------------
def get_solution(self) -> dict:
"""
Extract and return the optimization solution.
Returns
-------
dict with keys:
status (int): Gurobi status code
is_optimal (bool): True if an optimal solution was found
objective_value (float or None): Total score, or None if not optimal
assignments (dict): OS -> {machine, slot, score, days_since_last_use, modifier}
"""
status = self.model.Status
is_optimal = (status == GRB.OPTIMAL)
if not is_optimal:
return {
"status": status,
"is_optimal": False,
"objective_value": None,
"assignments": {}
}
# Build a lookup for extra fields
row_lookup = {
(row.os, row.machine, row.slot): row
for row in self.df.itertuples(index=False)
}
assignments = {}
for (os, machine, slot), var in self._assign.items():
if var.X > 0.5:
row = row_lookup[(os, machine, slot)]
assignments[os] = {
"machine": machine,
"slot": slot,
"score": row.score,
"days_since_last_use": row.days_since_last_use,
"modifier": row.modifier,
}
return {
"status": status,
"is_optimal": True,
"objective_value": self.model.ObjVal,
"assignments": assignments,
}
# ------------------------------------------------------------------
# Display
# ------------------------------------------------------------------
def display_results(self):
"""Print a formatted summary of the optimization solution."""
solution = self.get_solution()
print("\n" + "=" * 60)
print(" OS TEST ASSIGNMENT — OPTIMIZATION RESULTS")
print("=" * 60)
if not solution["is_optimal"]:
print(f" No optimal solution found. Status: {solution['status']}")
print("=" * 60)
return
print(f" Reference date : {self.reference_date}")
print(f" Total score : {solution['objective_value']:.2f}")
print("-" * 60)
print(f" {'OS':<15} {'Machine':<15} {'Slot':>5} {'Days':>6} {'Mod':>5} {'Score':>8}")
print("-" * 60)
for os_name, info in sorted(solution["assignments"].items()):
print(
f" {os_name:<15} {info['machine']:<15} {info['slot']:>5} "
f"{info['days_since_last_use']:>6} {info['modifier']:>5} {info['score']:>8.1f}"
)
print("=" * 60)
# ---------------------------------------------------------------------------
# Standalone entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
reference_date = datetime.date(2024, 7, 3)
with gp.Env(empty=True) as env:
env.setParam("OutputFlag", 1)
env.start()
with OSTestAssignmentOptimizer(env) as optimizer:
optimizer.set_data(SAMPLE_DATA, reference_date=reference_date)
optimizer.build_model()
optimizer.solve()
optimizer.display_results()
Alongside the implementation, Modeler writes a pytest suite with one test
per scenario in the specification. Each test references its scenario
identifier, and uses small hard-coded compatibility data and a short solver
time limit so the suite runs in well under a second.
"""
Unit tests for the OS Test Assignment Optimizer.
Tests cover all scenarios described in the specification:
T1 - Basic optimization with real data
T2 - Slot uniqueness enforced
T3 - Infeasible case
T4 - Single OS edge case
T5 - Score maximization correctness (global optimum vs. greedy)
"""
import datetime
import time
import pytest
import pandas as pd
import gurobipy as gp
from gurobipy import GRB
from testing_strategy_optimization_modeler import OSTestAssignmentOptimizer
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_env():
"""Create a quiet, time-limited Gurobi environment for testing."""
env = gp.Env(empty=True)
env.setParam("OutputFlag", 0)
env.setParam("TimeLimit", 10)
env.start()
return env
def solve_with_data(df: pd.DataFrame, reference_date: datetime.date) -> dict:
"""Convenience: build, solve, and return solution dict."""
with make_env() as env:
with OSTestAssignmentOptimizer(env) as optimizer:
optimizer.set_data(df, reference_date=reference_date)
optimizer.build_model()
optimizer.solve()
return optimizer.get_solution()
# ---------------------------------------------------------------------------
# T1: Basic optimization with full real dataset
# ---------------------------------------------------------------------------
REAL_DATA = pd.DataFrame([
{"os": "Linux64", "slot": 1, "machine": "Linux1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "Linux64", "slot": 1, "machine": "Linux2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "Linux64", "slot": 2, "machine": "Linux3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "Linux64", "slot": 2, "machine": "Linux4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "Linux64", "slot": 3, "machine": "Linux5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "Linux64", "slot": 3, "machine": "Linux6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "Linux64", "slot": 3, "machine": "Linux7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "Linux64", "slot": 4, "machine": "Linux8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "Linux64", "slot": 4, "machine": "Linux9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "Linux64", "slot": 4, "machine": "Linux10", "testing_date": datetime.date(2024, 6, 30), "modifier": 1},
{"os": "Armlinux64", "slot": 1, "machine": "ArmLinux1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "Armlinux64", "slot": 2, "machine": "ArmLinux2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "Armlinux64", "slot": 3, "machine": "ArmLinux3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "Armlinux64", "slot": 4, "machine": "ArmLinux4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "Armlinux64", "slot": 1, "machine": "ArmLinux5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "Armlinux64", "slot": 2, "machine": "ArmLinux6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "Armlinux64", "slot": 3, "machine": "ArmLinux7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "Armlinux64", "slot": 4, "machine": "ArmLinux8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "Armlinux64", "slot": 1, "machine": "ArmLinux9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "Armlinux64", "slot": 2, "machine": "ArmLinux10","testing_date": datetime.date(2024, 6, 30), "modifier": 1},
{"os": "MacOS", "slot": 2, "machine": "MacOS1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "MacOS", "slot": 1, "machine": "MacOS2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "MacOS", "slot": 4, "machine": "MacOS3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "MacOS", "slot": 3, "machine": "MacOS4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "MacOS", "slot": 2, "machine": "MacOS5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "MacOS", "slot": 1, "machine": "MacOS6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "MacOS", "slot": 4, "machine": "MacOS7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "MacOS", "slot": 3, "machine": "MacOS8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "MacOS", "slot": 2, "machine": "MacOS9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "MacOS", "slot": 1, "machine": "MacOS10", "testing_date": datetime.date(2024, 6, 30), "modifier": 1},
{"os": "Windows", "slot": 4, "machine": "Windows1", "testing_date": datetime.date(2024, 6, 1), "modifier": 1},
{"os": "Windows", "slot": 4, "machine": "Windows2", "testing_date": datetime.date(2024, 6, 15), "modifier": 2},
{"os": "Windows", "slot": 3, "machine": "Windows3", "testing_date": datetime.date(2024, 6, 10), "modifier": 1},
{"os": "Windows", "slot": 3, "machine": "Windows4", "testing_date": datetime.date(2024, 6, 5), "modifier": 1},
{"os": "Windows", "slot": 2, "machine": "Windows5", "testing_date": datetime.date(2024, 6, 12), "modifier": 2},
{"os": "Windows", "slot": 2, "machine": "Windows6", "testing_date": datetime.date(2024, 6, 8), "modifier": 1},
{"os": "Windows", "slot": 2, "machine": "Windows7", "testing_date": datetime.date(2024, 6, 20), "modifier": 2},
{"os": "Windows", "slot": 1, "machine": "Windows8", "testing_date": datetime.date(2024, 6, 18), "modifier": 1},
{"os": "Windows", "slot": 1, "machine": "Windows9", "testing_date": datetime.date(2024, 6, 25), "modifier": 2},
{"os": "Windows", "slot": 1, "machine": "Windows10", "testing_date": datetime.date(2024, 6, 30), "modifier": 1},
])
REFERENCE_DATE_T1 = datetime.date(2024, 7, 3)
def test_T1_basic_optimization_is_optimal():
"""T1: Real dataset — solution must be optimal."""
start = time.time()
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert time.time() - start < 10, "Test exceeded time limit"
assert solution["is_optimal"] is True
def test_T1_basic_optimization_four_assignments():
"""T1: Real dataset — exactly 4 assignments (one per OS)."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
assert len(solution["assignments"]) == 4
assert set(solution["assignments"].keys()) == {"Linux64", "Armlinux64", "MacOS", "Windows"}
def test_T1_basic_optimization_no_slot_conflict():
"""T1: Real dataset — no two OSes share the same slot."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
used_slots = [info["slot"] for info in solution["assignments"].values()]
assert len(used_slots) == len(set(used_slots)), "Slot conflict detected"
def test_T1_basic_optimization_no_machine_conflict():
"""T1: Real dataset — no two OSes share the same machine."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
used_machines = [info["machine"] for info in solution["assignments"].values()]
assert len(used_machines) == len(set(used_machines)), "Machine conflict detected"
def test_T1_basic_optimization_valid_pairs_only():
"""T1: Real dataset — every assigned (machine, slot) pair is valid for its OS."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
valid_set = set(
zip(REAL_DATA["os"], REAL_DATA["machine"], REAL_DATA["slot"])
)
for os_name, info in solution["assignments"].items():
assert (os_name, info["machine"], info["slot"]) in valid_set
def test_T1_basic_optimization_score_formula():
"""T1: Real dataset — each individual score equals days_since_last_use * modifier."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
for os_name, info in solution["assignments"].items():
expected = info["days_since_last_use"] * info["modifier"]
assert abs(info["score"] - expected) < 1e-6
def test_T1_basic_optimization_positive_objective():
"""T1: Real dataset — objective value must be positive."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
assert solution["objective_value"] > 0
# ---------------------------------------------------------------------------
# T2: Slot uniqueness enforced
# ---------------------------------------------------------------------------
T2_DATA = pd.DataFrame([
{"os": "OS_A", "slot": 1, "machine": "Machine_A1", "testing_date": datetime.date(2025, 1, 1), "modifier": 10},
{"os": "OS_A", "slot": 2, "machine": "Machine_A2", "testing_date": datetime.date(2020, 1, 1), "modifier": 1},
{"os": "OS_B", "slot": 1, "machine": "Machine_B1", "testing_date": datetime.date(2025, 1, 1), "modifier": 10},
{"os": "OS_B", "slot": 2, "machine": "Machine_B2", "testing_date": datetime.date(2020, 1, 1), "modifier": 1},
])
REFERENCE_DATE_T2 = datetime.date(2026, 1, 1)
def test_T2_slot_uniqueness_is_optimal():
"""T2: Slot conflict scenario — solution must be optimal."""
start = time.time()
solution = solve_with_data(T2_DATA, REFERENCE_DATE_T2)
assert time.time() - start < 10, "Test exceeded time limit"
assert solution["is_optimal"] is True
def test_T2_slot_uniqueness_different_slots():
"""T2: OS_A and OS_B must be assigned to different slots."""
solution = solve_with_data(T2_DATA, REFERENCE_DATE_T2)
assert solution["is_optimal"] is True
slot_a = solution["assignments"]["OS_A"]["slot"]
slot_b = solution["assignments"]["OS_B"]["slot"]
assert slot_a != slot_b
def test_T2_slot_uniqueness_objective():
"""T2: Total score must equal 3650 + 2192 = 5842."""
solution = solve_with_data(T2_DATA, REFERENCE_DATE_T2)
assert solution["is_optimal"] is True
# OS_A on slot 1: (2026-01-01 - 2025-01-01).days = 365, modifier=10 -> 3650
# OS_B on slot 2: (2026-01-01 - 2020-01-01).days = 2192, modifier=1 -> 2192
# OR vice versa. Both are valid; total is 3650 + 2192 = 5842
assert abs(solution["objective_value"] - 5842) < 1e-3
# ---------------------------------------------------------------------------
# T3: Infeasible case
# ---------------------------------------------------------------------------
T3_DATA = pd.DataFrame([
{"os": "OS_A", "slot": 1, "machine": "Machine_A1", "testing_date": datetime.date(2025, 1, 1), "modifier": 1},
{"os": "OS_B", "slot": 1, "machine": "Machine_B1", "testing_date": datetime.date(2025, 1, 1), "modifier": 1},
])
REFERENCE_DATE_T3 = datetime.date(2026, 1, 1)
def test_T3_infeasible_not_optimal():
"""T3: Two OSes sharing the only slot — model must be infeasible."""
start = time.time()
solution = solve_with_data(T3_DATA, REFERENCE_DATE_T3)
assert time.time() - start < 10, "Test exceeded time limit"
assert solution["is_optimal"] is False
def test_T3_infeasible_no_objective():
"""T3: Infeasible case — objective_value must be None."""
solution = solve_with_data(T3_DATA, REFERENCE_DATE_T3)
assert solution["objective_value"] is None
def test_T3_infeasible_empty_assignments():
"""T3: Infeasible case — assignments must be empty."""
solution = solve_with_data(T3_DATA, REFERENCE_DATE_T3)
assert solution["assignments"] == {}
# ---------------------------------------------------------------------------
# T4: Single OS edge case
# ---------------------------------------------------------------------------
T4_DATA = pd.DataFrame([
{"os": "OS_A", "slot": 1, "machine": "Machine_A1", "testing_date": datetime.date(2025, 7, 1), "modifier": 3},
])
REFERENCE_DATE_T4 = datetime.date(2026, 1, 1)
def test_T4_single_os_is_optimal():
"""T4: Single OS — solution must be optimal."""
start = time.time()
solution = solve_with_data(T4_DATA, REFERENCE_DATE_T4)
assert time.time() - start < 10, "Test exceeded time limit"
assert solution["is_optimal"] is True
def test_T4_single_os_correct_assignment():
"""T4: Single OS — must be assigned to Machine_A1 on slot 1."""
solution = solve_with_data(T4_DATA, REFERENCE_DATE_T4)
assert solution["is_optimal"] is True
assert "OS_A" in solution["assignments"]
assert solution["assignments"]["OS_A"]["machine"] == "Machine_A1"
assert solution["assignments"]["OS_A"]["slot"] == 1
def test_T4_single_os_correct_score():
"""T4: Single OS — score must equal 184 * 3 = 552."""
solution = solve_with_data(T4_DATA, REFERENCE_DATE_T4)
assert solution["is_optimal"] is True
# (2026-01-01 - 2025-07-01).days = 184
expected_days = (datetime.date(2026, 1, 1) - datetime.date(2025, 7, 1)).days
expected_score = expected_days * 3
assert abs(solution["objective_value"] - expected_score) < 1e-3
# ---------------------------------------------------------------------------
# T5: Score maximization correctness (global vs. greedy)
# ---------------------------------------------------------------------------
T5_DATA = pd.DataFrame([
{"os": "OS_A", "slot": 1, "machine": "Machine_A1", "testing_date": datetime.date(2024, 1, 1), "modifier": 2},
{"os": "OS_A", "slot": 2, "machine": "Machine_A2", "testing_date": datetime.date(2025, 1, 1), "modifier": 1},
{"os": "OS_B", "slot": 1, "machine": "Machine_B1", "testing_date": datetime.date(2025, 1, 1), "modifier": 1},
])
REFERENCE_DATE_T5 = datetime.date(2026, 1, 1)
def test_T5_global_optimum_is_optimal():
"""T5: Global optimum check — solution must be optimal."""
start = time.time()
solution = solve_with_data(T5_DATA, REFERENCE_DATE_T5)
assert time.time() - start < 10, "Test exceeded time limit"
assert solution["is_optimal"] is True
def test_T5_global_optimum_correct_slots():
"""T5: OS_A must be on slot 2, OS_B must be on slot 1 (greedy would fail)."""
solution = solve_with_data(T5_DATA, REFERENCE_DATE_T5)
assert solution["is_optimal"] is True
assert solution["assignments"]["OS_A"]["slot"] == 2
assert solution["assignments"]["OS_B"]["slot"] == 1
def test_T5_global_optimum_objective():
"""T5: Total score must equal 730 (365 + 365)."""
solution = solve_with_data(T5_DATA, REFERENCE_DATE_T5)
assert solution["is_optimal"] is True
# OS_A on slot 2: (2026-01-01 - 2025-01-01).days=365, modifier=1 -> 365
# OS_B on slot 1: (2026-01-01 - 2025-01-01).days=365, modifier=1 -> 365
assert abs(solution["objective_value"] - 730) < 1e-3
Generating code and tests is the easy half. Modeler also runs the suite in an isolated environment created fresh for this conversation, solving against Gurobi in the cloud — you do not need Gurobi installed locally, and you do not need a license of your own to see the model solve. It reads the output and reports back, so what you get back is a model that has been executed, not one that merely looks plausible.
Option 2 — accept an optional reference_date parameter defaulting to today’s date if not provided, so we get reproducible tests but still get fresh scores in production. That covers everything I have — please finalize the specification, then implement it in gurobipy, write a pytest suite covering the test scenarios, run the tests, and report the results.
No open questions left to ask — every field the implementation needs
(the score formula, the three uniqueness constraints, the compatibility
data, the reference-date default) is already pinned down, and this
problem is small and clean enough that there’s no genuinely open
structural choice left either. Writing the OSTestAssignmentOptimizer
class with one binary variable per valid (OS, machine, slot) triple,
then a pytest suite covering T1-T5, then running both the script and
the suite.
The run
All 19 tests pass in 0.33 seconds on the first run — no failures to chase down here. Here is the full unedited solver output, plus a report of everything that was produced.
$ python os_test_assignment.py
Restricted license - for non-production use only - expires 2027-11-29
Gurobi Optimizer version 13.0.2 build v13.0.2rc1 (armlinux64 - "Debian GNU/Linux 13 (trixie)")
CPU model: ARM64
Thread count: 2 physical cores, 2 logical processors, using up to 2 threads
Optimize a model with 48 rows, 40 columns and 120 nonzeros (Max)
Model fingerprint: 0xc69277fd
Model has 40 linear objective coefficients
Variable types: 0 continuous, 40 integer (40 binary)
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [3e+00, 4e+01]
Bounds range [1e+00, 1e+00]
RHS range [1e+00, 1e+00]
Found heuristic solution: objective 88.0000000
Presolve removed 40 rows and 24 columns
Presolve time: 0.00s
Presolved: 8 rows, 16 columns, 32 nonzeros
Found heuristic solution: objective 150.0000000
Variable types: 0 continuous, 16 integer (16 binary)
Root relaxation: objective 1.620000e+02, 0 iterations, 0.00 seconds (0.00 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
* 0 0 0 162.0000000 162.00000 0.00% - 0s
Explored 1 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
Thread count was 2 (of 2 available processors)
Solution count 3: 162 150 88
Optimal solution found (tolerance 1.00e-04)
Best objective 1.620000000000e+02, best bound 1.620000000000e+02, gap 0.0000%
$ pytest test_os_test_assignment.py -v
============================= test session starts ==============================
collected 19 items
test_os_test_assignment.py::test_T1_basic_optimization_is_optimal PASSED
test_os_test_assignment.py::test_T1_basic_optimization_four_assignments PASSED
test_os_test_assignment.py::test_T1_basic_optimization_no_slot_conflict PASSED
test_os_test_assignment.py::test_T1_basic_optimization_no_machine_conflict PASSED
test_os_test_assignment.py::test_T1_basic_optimization_valid_pairs_only PASSED
test_os_test_assignment.py::test_T1_basic_optimization_score_formula PASSED
test_os_test_assignment.py::test_T1_basic_optimization_positive_objective PASSED
test_os_test_assignment.py::test_T2_slot_uniqueness_is_optimal PASSED
test_os_test_assignment.py::test_T2_slot_uniqueness_different_slots PASSED
test_os_test_assignment.py::test_T2_slot_uniqueness_objective PASSED
test_os_test_assignment.py::test_T3_infeasible_not_optimal PASSED
test_os_test_assignment.py::test_T3_infeasible_no_objective PASSED
test_os_test_assignment.py::test_T3_infeasible_empty_assignments PASSED
test_os_test_assignment.py::test_T4_single_os_is_optimal PASSED
test_os_test_assignment.py::test_T4_single_os_correct_assignment PASSED
test_os_test_assignment.py::test_T4_single_os_correct_score PASSED
test_os_test_assignment.py::test_T5_global_optimum_is_optimal PASSED
test_os_test_assignment.py::test_T5_global_optimum_correct_slots PASSED
test_os_test_assignment.py::test_T5_global_optimum_objective PASSED
============================== 19 passed in 0.33s ==============================
All 19 tests pass. One thing worth noting from the solver log: the root relaxation already lands on 162 with zero simplex iterations, and the branch-and-bound tree never opens beyond the root node — this assignment structure is totally unimodular, so Gurobi’s LP relaxation is integral from the start. Presolve also removes the entire C3 (machine-uniqueness) block: since every machine belongs to exactly one OS in this data, machine-uniqueness is implied by slot-uniqueness and never actually binds. Here is a full summary of everything that was built:
Results
Optimal plan — reference date 2024-07-03, total score 162:
OS |
Machine |
Slot |
Days since last use |
Modifier |
Score |
|---|---|---|---|---|---|
Armlinux64 |
ArmLinux5 |
1 |
21 |
2 |
42 |
MacOS |
MacOS5 |
2 |
21 |
2 |
42 |
Linux64 |
Linux5 |
3 |
21 |
2 |
42 |
Windows |
Windows2 |
4 |
18 |
2 |
36 |
Three of the four OSes land on their absolute best-possible score of 42; Windows comes within 6 points of its own ceiling (also 42, on slot 2 — but that slot is already taken by MacOS), landing on slot 4 instead.
Files created
File |
Description |
|---|---|
|
Input compatibility and scoring dataset |
|
Complete problem specification |
|
Gurobipy implementation ( |
|
Pytest suite covering scenarios T1-T5 |
Test results
Test |
Scenario |
Result |
|---|---|---|
T1 |
Baseline — 40 rows, 4 OSes, total score 162 |
PASSED |
T2 |
Two OSes both prefer the same slot; model separates them |
PASSED |
T3 |
Two OSes sharing their only wired slot is infeasible |
PASSED |
T4 |
Single OS with a single valid (machine, slot) pair |
PASSED |
T5 |
Global optimum found over the greedy per-OS choice |
PASSED |
Let me know if you would like any changes, such as a scheduling restriction between two OSes, or generating the mathematical formulation.
That log lands in the workspace as console.log alongside the code, so the
solve is auditable rather than asserted.
The loop can end one of two ways. Either every test passes, as here — or Modeler concludes a failure reflects a genuine contradiction between the specification, the implementation and the tests. In that case it stops instead of guessing, tells you what it found, and asks which side is the one to correct. That second path is what the Changing the model tab demonstrates, when a later change surfaces exactly this kind of contradiction.
What the cloud environment gives you, and what it does not
The environment is built for validating a model, not for running production
workloads. Python 3 is available with gurobipy, gurobipy-pandas,
numpy, pandas, scipy and pytest pre-installed. Within that:
Solves use a Gurobi trial license, capped at 2,000 variables and 2,000 constraints. This model, at 40 columns and 48 rows, is nowhere near the cap — but a full-scale version covering hundreds of OS variants across a large test-lab environment pool could be.
Execution is limited to 60 seconds per command, with tight CPU and memory limits. This is why the generated tests use small hard-coded data and a short solver time limit.
Additional packages cannot be installed, there is no internet access, and Compute Server and Instant Cloud cannot be used.
So treat a passing suite as evidence that the model is right, not as a performance measurement. Download the implementation and run it against your own license and your own full data set for that. See Solving limits for the details.
Business problems do not hold still. The specification above puts time-window or scheduling constraints between OSes out of scope — a reasonable simplification, until DevOps tells you that Linux64 and Windows share physical rack space and a network switch, and testing them back-to-back causes real throughput contention. Ask for the change in plain language and Modeler updates the specification first, then propagates it to the implementation and the tests, in that order.
One more requirement: Linux64 and Windows share physical rack space and a network switch between their machine pools. When their tests land in back-to-back testing-environment slots, we get network throughput contention between the two runs. I want the model to guarantee Linux64’s slot and Windows’s slot are never back-to-back. One detail that might matter: the 4 environment slots run in a continuous round-robin loop each day — after environment 4 finishes, environment 1 starts immediately, so the loop wraps from slot 4 back to slot 1. Please update the specification first, then the implementation, then the tests, in that order, and tell me if anything about the current test scenarios — or the rule itself — becomes ambiguous under this change.
This is a scope change, not a data correction: “time-window or scheduling constraints” is currently listed under Out of Scope. Updating the specification first — add a new C5 constraint. The wraparound detail is significant: with only 4 slots in a round-robin loop, “back-to-back” has to include the (4,1) pair, not just consecutive numbers, otherwise the rule silently misses the one seam where the loop closes. The current T1 optimum puts Linux64 on slot 3 and Windows on slot 4 — adjacent — so T1’s expected assignment and objective need re-deriving under the new rule before anything else changes. Then the code, then the tests.
That moves the scheduling restriction from out of scope into the model, as a circular non-adjacency rule. I have updated the specification, implemented it, and re-run the full suite — 29 tests pass in 0.51 seconds:
One thing worth flagging: I implemented the circular reading (slot 4 and slot 1 count as adjacent) based on the round-robin detail you gave me, rather than a linear reading that only forbids consecutive numbers. That is a real interpretive choice with a different optimal answer attached — worth double-checking before you treat 134 as final.
Specification, then code, then tests
The specification changes first. Section 3 gains a new constraint:
### C5: Linux64 and Windows Must Not Be Assigned to Back-to-Back Slots (Circular)
Linux64 and Windows share physical rack space and a network switch. To prevent
network throughput contention, their assigned testing environment slots must not
be adjacent in the circular round-robin execution sequence. Two slots are
considered adjacent if one immediately precedes the other in the sorted
round-robin order, including the wrap from the last slot back to the first slot.
Formally, given the sorted list of distinct slot numbers present in the data,
two slots are adjacent if they are consecutive positions in that sorted list
(with the last position wrapping to the first). For the reference dataset with
slots 1-4, the adjacent pairs are: (1,2), (2,3), (3,4), and (4,1).
This constraint is enforced only when both Linux64 and Windows appear in the
input data; it is vacuously satisfied otherwise.
The implementation follows the specification, not the other way around. A new
_add_no_backtoback_constraint method is added and called from
_add_constraints, one tight pairwise inequality per circularly-adjacent
slot pair — no Big-M and no auxiliary variables:
@@ _add_constraints(self) @@
# C3: Each machine used at most once
all_machines = self.df["machine"].unique().tolist()
self.model.addConstrs(
(
gp.quicksum(
self._assign[os, machine, slot]
for (os, m, slot) in self.candidates
if m == machine
) <= 1
for machine in all_machines
),
name="C3_machine_used_once"
)
+ # C5: Linux64 and Windows must not be in adjacent slots (circular)
+ self._add_no_backtoback_constraint()
+ def _add_no_backtoback_constraint(self):
+ """
+ C5: Linux64 and Windows must not be assigned to back-to-back testing
+ environment slots in the circular round-robin execution order.
+
+ Adjacency is determined by the sorted order of distinct slot numbers
+ present in the input data, with the last slot wrapping back to the first.
+ Skipped entirely if either Linux64 or Windows is absent from the data.
+ """
+ if _C5_OS_A not in self.os_list or _C5_OS_B not in self.os_list:
+ return
+
+ all_slots = sorted(self.df["slot"].unique().tolist())
+ n = len(all_slots)
+
+ for i in range(n):
+ s_a = all_slots[i]
+ s_b = all_slots[(i + 1) % n] # circular: last slot wraps to first
+
+ linux_on_sa = gp.quicksum(self._assign[o, m, s] for (o, m, s) in self.candidates if o == _C5_OS_A and s == s_a)
+ linux_on_sb = gp.quicksum(self._assign[o, m, s] for (o, m, s) in self.candidates if o == _C5_OS_A and s == s_b)
+ windows_on_sb = gp.quicksum(self._assign[o, m, s] for (o, m, s) in self.candidates if o == _C5_OS_B and s == s_b)
+ windows_on_sa = gp.quicksum(self._assign[o, m, s] for (o, m, s) in self.candidates if o == _C5_OS_B and s == s_a)
+
+ self.model.addConstr(linux_on_sa + windows_on_sb <= 1, name=f"C5_no_backtoback_L{s_a}_W{s_b}")
+ self.model.addConstr(linux_on_sb + windows_on_sa <= 1, name=f"C5_no_backtoback_L{s_b}_W{s_a}")
T1 gains a non-adjacency assertion against the new optimum, and the suite gains a dedicated scenario:
def test_T1_no_backtoback_linux_windows():
"""T1: Real dataset — Linux64 and Windows must not be in adjacent slots (C5, circular)."""
solution = solve_with_data(REAL_DATA, REFERENCE_DATE_T1)
assert solution["is_optimal"] is True
linux_slot = solution["assignments"]["Linux64"]["slot"]
windows_slot = solution["assignments"]["Windows"]["slot"]
all_slots = sorted(REAL_DATA["slot"].unique().tolist())
assert not slots_are_adjacent_circular(linux_slot, windows_slot, all_slots), (
f"C5 violated: Linux64 on slot {linux_slot}, Windows on slot {windows_slot} are adjacent."
)
def test_T6a_c5_overrides_greedy_correct_slots():
"""T6a: Linux64 must be on slot 1, Windows on slot 3 (only non-adjacent pair with higher score)."""
solution = solve_with_data(T6A_DATA, REFERENCE_DATE_T6)
assert solution["is_optimal"] is True
assert solution["assignments"]["Linux64"]["slot"] == 1
assert solution["assignments"]["Windows"]["slot"] == 3
Modeler re-runs the whole suite, not just the new tests — 29/29 pass on the first run in 0.51 seconds. The optimal objective drops from 162 to 134.
Where it stops and asks
Modeler had already picked a reading — circular, including the wraparound — and flagged it as worth double-checking, rather than presenting 134 as an unquestionable fact.
Before you finalize — the circular/wraparound reading is a bigger assumption than I expected. Can you check: does the linear-only reading (adjacency only for consecutive numbers, no wraparound from slot 4 back to slot 1) actually give a different optimal assignment and objective than the circular reading you implemented? I want real numbers for both before I sign off on which one is right.
This is exactly the kind of fork that should be surfaced with real numbers, not assumed. Solving both versions on the real dataset: linear-only forbids just (1,2), (2,3), (3,4); circular also forbids (4,1). If the linear optimum happens to land on the (1,4) pair — the one combination only the circular rule forbids — the two readings will diverge for real, not just in theory.
Confirmed — the two readings diverge, and it isn’t a rounding artifact:
Reading |
Objective |
Linux64 |
Windows |
|---|---|---|---|
Linear (no wrap): adjacency only for consecutive numbers |
140 |
Linux2, slot 1 |
Windows2, slot 4 |
Circular (implemented): wrap from slot 4 back to slot 1 |
134 |
Linux2, slot 1 |
Windows4, slot 3 |
The linear optimum puts Linux64 on slot 1 (its own best score, 36) and
Windows on slot 4 (36) — that pair satisfies the linear rule since
|1 - 4| = 3. But slot 4 and slot 1 are exactly the wraparound pair
— the one seam the circular rule exists to catch. Under the circular
rule that pairing is forbidden, so Windows has to vacate slot 4; it
drops to slot 3 (28, down from 36). That frees slot 4 for Armlinux64,
which shifts there from slot 3 (28, up from 26) to fill the gap.
Linux64 and MacOS are untouched either way.
Decision needed: if slot 4 genuinely hands off straight into slot 1 with no gap, circular is correct and 134 stands. If there is a cooldown between the end of one loop and the start of the next, linear is correct and the objective moves to 140, with the (slot 1, slot 4) assignment becoming valid again.
Circular is correct — there’s no cooldown gap in the round-robin loop, slot 4 hands off straight into slot 1 every day, so they do run back-to-back. Keep the circular implementation, objective 134 stands.
This is the same “stop and ask” behavior as any other tab: not a test scenario whose expected answer became ambiguous, but a modeling choice inside the new constraint that had two defensible readings with different numeric consequences. Modeler surfaced both, with real numbers attached, rather than quietly keeping the one it had already coded.
The optimum reshuffles, and the cost is real
OS |
Slot (no C5) |
Slot (C5, linear — rejected) |
Slot (C5, circular — kept) |
Change |
|---|---|---|---|---|
Linux64 |
3 |
1 |
1 |
Moves to slot 1 (its own best) under either reading |
Armlinux64 |
1 |
3 |
4 |
Moves to slot 3 under linear, on to slot 4 under circular |
MacOS |
2 |
2 |
2 |
Unchanged throughout |
Windows |
4 |
4 |
3 |
Unchanged under linear, moves to slot 3 under circular |
Objective |
162 |
140 |
134 |
28 points lower than the unconstrained baseline |
Adding any version of the non-adjacency rule immediately breaks up the baseline’s Linux64/Windows pairing, since slots 3 and 4 are adjacent under both readings — Linux64 moves to slot 1 (its own individual best) no matter which reading wins. What the two readings disagree on is who backfills the rest: under the linear reading Windows simply keeps its own best slot, 4, and Armlinux64 slots into 3; the circular reading forbids the (slot 1, slot 4) wraparound pair Linux64 and Windows would otherwise form, so Windows is pushed to slot 3 and Armlinux64 all the way to slot 4. The rejected linear reading would have cost only 22 points versus the true baseline — the extra 6 points the circular rule gives up (140 → 134) is the real, measurable price of treating the round-robin wrap as a genuine adjacency, not a hand-wavy “should be close enough.”
The specification, the implementation and the tests never mention mathematics — that is deliberate, so the business owner can read them. But you may still need the formulation: to review the model, to document it, or to hand it to someone who thinks in constraints rather than in code. Ask at any point and Modeler derives one from the current specification and implementation, in Markdown with LaTeX, and saves it to the workspace with everything else.
Sets and indices
\(o \in O\): operating systems, \(O = \{\text{Linux64}, \text{Armlinux64}, \text{MacOS}, \text{Windows}\}\).
\(m \in M\): physical testing machines.
\(s \in S\): testing environment slots.
\(V \subseteq O \times M \times S\): the set of valid (OS, machine, slot) triples — only combinations where the machine is physically wired to the slot for that OS.
Parameters
\(d_{o,m,s}\): days since last use for the valid triple \((o,m,s) \in V\), computed as \(\text{reference\_date} - \text{testing\_date}_{o,m,s}\).
\(\mu_{o,m,s}\): modifier value associated with the valid triple \((o,m,s) \in V\).
\(c_{o,m,s} = d_{o,m,s} \cdot \mu_{o,m,s}\): assignment score for triple \((o,m,s) \in V\).
Decision variables
Where \(x_{o,m,s} = 1\) if operating system \(o\) is assigned to machine \(m\) on testing environment slot \(s\), and 0 otherwise. The variable is only defined for valid triples — invalid (OS, machine, slot) combinations are excluded structurally, not through an explicit constraint.
Objective function
Maximize the total assignment score across all OS assignments:
Constraints
C1 — Each OS must be assigned exactly once.
C2 — Each testing environment slot used at most once.
C3 — Each machine used at most once.
C4 — Valid pairs only. Enforced structurally: decision variables are only created for \((o,m,s) \in V\), so every triple outside \(V\) is implicitly fixed at 0.
Complete model
Additional notes
This is a Binary Integer Program. Because every constraint matrix involves only 0/1 coefficients in a set-partitioning / set-packing structure, the LP relaxation is tight — Gurobi’s presolve and root-node heuristics typically close the gap without any branching, exactly as seen in the Cloud solve tab.
The score \(c_{o,m,s} = d_{o,m,s} \cdot \mu_{o,m,s}\) is computed as a data preparation step before the model is built. The reference date used to compute \(d_{o,m,s}\) defaults to today’s date but can be overridden for reproducibility.
A feasible solution exists if and only if there is a simultaneous system of distinct representatives for the OS set across both the slot and machine dimensions. If any OS has no valid pair, or the slot/machine conflicts make such a simultaneous assignment impossible, Gurobi reports infeasibility — exactly what scenario T3 exercises.
On the 40-row reference dataset, \(|V| = 40\) binary variables are created — one per row of the input data — against 4 C1 equalities, 4 C2 inequalities, and 40 C3 inequalities: 48 constraint rows before presolve. Presolve removes the entire C3 block on this data, since every machine belongs to exactly one OS and machine-uniqueness is already implied by slot-uniqueness.
Note that this formulates the model before the Linux64 / Windows back-to-back scheduling requirement introduced in the Changing the model tab. Add constraint C5 as described there — for each pair of circularly adjacent slots \((s_a, s_b)\), forbid \(x_{\text{Linux64},\,\cdot,\,s_a} + x_{\text{Windows},\,\cdot,\,s_b} \leq 1\) and the symmetric pair with Linux64 and Windows swapped — to recover the version formulated after that change.
### Sets and indices
- $o \in O$: operating systems, $O = \{\text{Linux64}, \text{Armlinux64}, \text{MacOS}, \text{Windows}\}$.
- $m \in M$: physical testing machines.
- $s \in S$: testing environment slots.
- $V \subseteq O \times M \times S$: the set of valid (OS, machine, slot)
triples — only combinations where the machine is physically wired to the
slot for that OS.
### Parameters
- $d_{o,m,s}$: days since last use for the valid triple $(o,m,s) \in V$,
computed as $\text{reference\_date} - \text{testing\_date}_{o,m,s}$.
- $\mu_{o,m,s}$: modifier value associated with the valid triple
$(o,m,s) \in V$.
- $c_{o,m,s} = d_{o,m,s} \cdot \mu_{o,m,s}$: assignment score for triple
$(o,m,s) \in V$.
### Decision variables
$$
x_{o,m,s} \in \{0, 1\}, \qquad \forall\, (o,m,s) \in V
$$
Where $x_{o,m,s} = 1$ if operating system $o$ is assigned to machine $m$ on
testing environment slot $s$, and 0 otherwise. The variable is only defined
for valid triples — invalid (OS, machine, slot) combinations are excluded
structurally, not through an explicit constraint.
### Objective function
Maximize the total assignment score across all OS assignments:
$$
\max \quad \sum_{(o,\,m,\,s)\, \in\, V} c_{o,m,s} \cdot x_{o,m,s}
\;=\; \max \quad \sum_{(o,\,m,\,s)\, \in\, V} d_{o,m,s} \cdot \mu_{o,m,s} \cdot x_{o,m,s}
$$
### Constraints
**C1 — Each OS must be assigned exactly once.**
$$
\sum_{(o,\,m,\,s)\, \in\, V} x_{o,m,s} = 1 \qquad \forall\, o \in O
$$
**C2 — Each testing environment slot used at most once.**
$$
\sum_{\substack{(o,\,m,\,s')\, \in\, V \\ s' = s}} x_{o,m,s'} \leq 1 \qquad \forall\, s \in S
$$
**C3 — Each machine used at most once.**
$$
\sum_{\substack{(o,\,m',\,s)\, \in\, V \\ m' = m}} x_{o,m',s} \leq 1 \qquad \forall\, m \in M
$$
**C4 — Valid pairs only.** Enforced structurally: decision variables are only
created for $(o,m,s) \in V$, so every triple outside $V$ is implicitly fixed
at 0.
### Complete model
$$
\begin{aligned}
\max \quad & \sum_{(o,\,m,\,s)\, \in\, V} d_{o,m,s} \cdot \mu_{o,m,s} \cdot x_{o,m,s} \\[10pt]
\text{s.t.} \quad
& \sum_{(o,\,m,\,s)\, \in\, V} x_{o,m,s} = 1
&& \forall\, o \in O
& \text{(C1)} \\[6pt]
& \sum_{\substack{(o,\,m,\,s')\, \in\, V \\ s' = s}} x_{o,m,s'} \leq 1
&& \forall\, s \in S
& \text{(C2)} \\[6pt]
& \sum_{\substack{(o,\,m',\,s)\, \in\, V \\ m' = m}} x_{o,m',s} \leq 1
&& \forall\, m \in M
& \text{(C3)} \\[6pt]
& x_{o,m,s} \in \{0, 1\}
&& \forall\, (o,m,s) \in V
\end{aligned}
$$
### Additional notes
- This is a Binary Integer Program. Because every constraint matrix involves
only 0/1 coefficients in a set-partitioning / set-packing structure, the LP
relaxation is tight — Gurobi's presolve and root-node heuristics typically
close the gap without any branching, exactly as seen in the Cloud solve
tab.
- The score $c_{o,m,s} = d_{o,m,s} \cdot \mu_{o,m,s}$ is computed as a data
preparation step before the model is built. The reference date used to
compute $d_{o,m,s}$ defaults to today's date but can be overridden for
reproducibility.
- A feasible solution exists if and only if there is a simultaneous system of
distinct representatives for the OS set across both the slot and machine
dimensions. If any OS has no valid pair, or the slot/machine conflicts make
such a simultaneous assignment impossible, Gurobi reports infeasibility —
exactly what scenario T3 exercises.
- On the 40-row reference dataset, $|V| = 40$ binary variables are created —
one per row of the input data — against 4 C1 equalities, 4 C2 inequalities,
and 40 C3 inequalities: 48 constraint rows before presolve. Presolve
removes the entire C3 block on this data, since every machine belongs to
exactly one OS and machine-uniqueness is already implied by
slot-uniqueness.
Note that this formulates the model before the Linux64 / Windows back-to-back
scheduling requirement introduced in the Changing the model tab. Add
constraint C5 as described there — for each pair of circularly adjacent slots
$(s_a, s_b)$, forbid $x_{\text{Linux64},\,\cdot,\,s_a} + x_{\text{Windows},\,\cdot,\,s_b} \leq 1$
and the symmetric pair with Linux64 and Windows swapped — to recover the
version formulated after that change.