ommx.v1.SampleSet#
ommx.v1.Solution
represents a single solution returned by a solver. However, some solvers, often called samplers, can return multiple solutions. To accommodate this, OMMX provides two data structures for representing multiple solutions:
Data Structure |
Description |
---|---|
A list of multiple solutions for decision variable IDs |
|
Evaluations of objective and constraints with decision variables |
Samples
corresponds to State
and SampleSet
corresponds to Solution
. This notebook explains how to use SampleSet
.
Creating a SampleSet#
Let’s consider a simple optimization problem:
from ommx.v1 import DecisionVariable, Instance
x = [DecisionVariable.binary(i) for i in range(3)]
instance = Instance.from_components(
decision_variables=x,
objective=x[0] + 2*x[1] + 3*x[2],
constraints=[sum(x) == 1],
sense=Instance.MAXIMIZE,
)
Normally, solutions are provided by a solver, commonly referred to as a sampler, but for simplicity, we prepare them manually here. ommx.v1.Samples
can hold multiple samples, each expressed as a set of values associated with decision variable IDs, similar to ommx.v1.State
.
Each sample is assigned an ID. Some samplers issue their own IDs for logging, so OMMX allows specifying sample IDs. If omitted, IDs are assigned sequentially starting from 0
.
A helper function ommx.v1.to_samples
can convert to ommx.v1.Samples
.
from ommx.v1 import to_samples
from ommx.v1.sample_set_pb2 import Samples
# When specifying Sample ID
samples = to_samples({
0: {0: 1, 1: 0, 2: 0}, # x1 = 1, x2 = x3 = 0
1: {0: 0, 1: 0, 2: 1}, # x3 = 1, x1 = x2 = 0
2: {0: 1, 1: 1, 2: 0}, # x1 = x2 = 1, x3 = 0 (infeasible)
})# ^ sample ID
assert isinstance(samples, Samples)
# When automatically assigning Sample ID
samples = to_samples([
{0: 1, 1: 0, 2: 0}, # x1 = 1, x2 = x3 = 0
{0: 0, 1: 0, 2: 1}, # x3 = 1, x1 = x2 = 0
{0: 1, 1: 1, 2: 0}, # x1 = x2 = 1, x3 = 0 (infeasible)
])
assert isinstance(samples, Samples)
While ommx.v1.Solution
is obtained via Instance.evaluate
, ommx.v1.SampleSet
can be obtained via Instance.evaluate_samples
.
sample_set = instance.evaluate_samples(samples)
sample_set.summary
objective | feasible | |
---|---|---|
sample_id | ||
1 | 3.0 | True |
0 | 1.0 | True |
2 | 3.0 | False |
The summary
attribute displays each sample’s objective value and feasibility in a DataFrame format. For example, the sample with sample_id=2
is infeasible and shows feasible=False
. The table is sorted with feasible samples appearing first, and within them, those with better bjective values (depending on whether Instance.sense
is maximization or minimization) appear at the top.
Note
For clarity, we explicitly pass ommx.v1.Samples
created by to_samples
to evaluate_samples
, but you can omit it because to_samples
would be called automatically.
Extracting individual samples#
You can use SampleSet.get
to retrieve each sample as an ommx.v1.Solution
by specifying the sample ID:
from ommx.v1 import Solution
solution = sample_set.get(sample_id=0)
assert isinstance(solution, Solution)
print(f"{solution.objective=}")
solution.decision_variables
solution.objective=1.0
kind | lower | upper | name | subscripts | description | substituted_value | value | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
0 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 1.0 |
1 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
2 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
Retrieving the best solution#
SampleSet.best_feasible
returns the best feasible sample, meaning the one with the highest objective value among all feasible samples:
solution = sample_set.best_feasible()
print(f"{solution.objective=}")
solution.decision_variables
solution.objective=3.0
kind | lower | upper | name | subscripts | description | substituted_value | value | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
0 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
1 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
2 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 1.0 |
Of course, if the problem is a minimization, the sample with the smallest objective value will be returned. If no feasible samples exist, an error will be raised.
sample_set_infeasible = instance.evaluate_samples([
{0: 1, 1: 1, 2: 0}, # Infeasible since x0 + x1 + x2 = 2
{0: 1, 1: 0, 2: 1}, # Infeasible since x0 + x1 + x2 = 2
])
# Every samples are infeasible
display(sample_set_infeasible.summary)
try:
sample_set_infeasible.best_feasible()
assert False # best_feasible() should raise RuntimeError
except RuntimeError as e:
print(e)
objective | feasible | |
---|---|---|
sample_id | ||
1 | 4.0 | False |
0 | 3.0 | False |
No feasible solution found in SampleSet
Note
OMMX does not provide a method to determine which infeasible solution is the best, as many different criteria can be considered. Implement it yourself if needed.