{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ommx.v1.SampleSet\n",
"=================\n",
"\n",
"[`ommx.v1.Solution`](https://jij-inc.github.io/ommx/python/ommx/autoapi/ommx/v1/index.html#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:\n",
"\n",
"| Data Structure | Description |\n",
"|:---------------|:------------|\n",
"| [`ommx.v1.Samples`](https://jij-inc.github.io/ommx/python/ommx/autoapi/ommx/v1/sample_set_pb2/index.html#ommx.v1.sample_set_pb2.Samples) | A list of multiple solutions for decision variable IDs |\n",
"| [`ommx.v1.SampleSet`](https://jij-inc.github.io/ommx/python/ommx/autoapi/ommx/v1/index.html#ommx.v1.SampleSet) | Evaluations of objective and constraints with decision variables |\n",
"\n",
"`Samples` corresponds to `State` and `SampleSet` corresponds to `Solution`. This notebook explains how to use `SampleSet`.\n",
"\n",
"Creating a SampleSet\n",
"--------------------\n",
"\n",
"Let's consider a simple optimization problem:\n",
"\n",
"$$\n",
"\\begin{align*}\n",
" \\max &\\quad x_1 + 2 x_2 + 3 x_3 \\\\\n",
" \\text{s.t.} &\\quad x_1 + x_2 + x_3 = 1 \\\\\n",
" &\\quad x_1, x_2, x_3 \\in \\{0, 1\\}\n",
"\\end{align*}\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from ommx.v1 import DecisionVariable, Instance\n",
"\n",
"x = [DecisionVariable.binary(i) for i in range(3)]\n",
"\n",
"instance = Instance.from_components(\n",
" decision_variables=x,\n",
" objective=x[0] + 2*x[1] + 3*x[2],\n",
" constraints=[sum(x) == 1],\n",
" sense=Instance.MAXIMIZE,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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`.\n",
"\n",
"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`.\n",
"\n",
"A helper function `ommx.v1.to_samples` can convert to `ommx.v1.Samples`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from ommx.v1 import to_samples\n",
"from ommx.v1.sample_set_pb2 import Samples\n",
"\n",
"# When specifying Sample ID\n",
"samples = to_samples({\n",
" 0: {0: 1, 1: 0, 2: 0}, # x1 = 1, x2 = x3 = 0\n",
" 1: {0: 0, 1: 0, 2: 1}, # x3 = 1, x1 = x2 = 0\n",
" 2: {0: 1, 1: 1, 2: 0}, # x1 = x2 = 1, x3 = 0 (infeasible)\n",
"})# ^ sample ID\n",
"assert isinstance(samples, Samples)\n",
"\n",
"# When automatically assigning Sample ID\n",
"samples = to_samples([\n",
" {0: 1, 1: 0, 2: 0}, # x1 = 1, x2 = x3 = 0\n",
" {0: 0, 1: 0, 2: 1}, # x3 = 1, x1 = x2 = 0\n",
" {0: 1, 1: 1, 2: 0}, # x1 = x2 = 1, x3 = 0 (infeasible)\n",
"])\n",
"assert isinstance(samples, Samples)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"While `ommx.v1.Solution` is obtained via `Instance.evaluate`, `ommx.v1.SampleSet` can be obtained via `Instance.evaluate_samples`."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" objective | \n",
" feasible | \n",
"
\n",
" \n",
" sample_id | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" 1 | \n",
" 3.0 | \n",
" True | \n",
"
\n",
" \n",
" 0 | \n",
" 1.0 | \n",
" True | \n",
"
\n",
" \n",
" 2 | \n",
" 3.0 | \n",
" False | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" objective feasible\n",
"sample_id \n",
"1 3.0 True\n",
"0 1.0 True\n",
"2 3.0 False"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_set = instance.evaluate_samples(samples)\n",
"sample_set.summary"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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.\n",
"\n",
"```{note}\n",
"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.\n",
"```\n",
"\n",
"Extracting individual samples\n",
"----------------------------\n",
"You can use `SampleSet.get` to retrieve each sample as an `ommx.v1.Solution` by specifying the sample ID:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"solution.objective=1.0\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" kind | \n",
" lower | \n",
" upper | \n",
" name | \n",
" subscripts | \n",
" description | \n",
" substituted_value | \n",
" value | \n",
"
\n",
" \n",
" id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" binary | \n",
" 0.0 | \n",
" 1.0 | \n",
" <NA> | \n",
" [] | \n",
" <NA> | \n",
" <NA> | \n",
" 1.0 | \n",
"
\n",
" \n",
" 1 | \n",
" binary | \n",
" 0.0 | \n",
" 1.0 | \n",
" <NA> | \n",
" [] | \n",
" <NA> | \n",
" <NA> | \n",
" 0.0 | \n",
"
\n",
" \n",
" 2 | \n",
" binary | \n",
" 0.0 | \n",
" 1.0 | \n",
" <NA> | \n",
" [] | \n",
" <NA> | \n",
" <NA> | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" kind lower upper name subscripts description substituted_value value\n",
"id \n",
"0 binary 0.0 1.0 [] 1.0\n",
"1 binary 0.0 1.0 [] 0.0\n",
"2 binary 0.0 1.0 [] 0.0"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from ommx.v1 import Solution\n",
"\n",
"solution = sample_set.get(sample_id=0)\n",
"assert isinstance(solution, Solution)\n",
"\n",
"print(f\"{solution.objective=}\")\n",
"solution.decision_variables"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Retrieving the best solution\n",
"---------------------------\n",
"`SampleSet.best_feasible` returns the best feasible sample, meaning the one with the highest objective value among all feasible samples:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"solution.objective=3.0\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" kind | \n",
" lower | \n",
" upper | \n",
" name | \n",
" subscripts | \n",
" description | \n",
" substituted_value | \n",
" value | \n",
"
\n",
" \n",
" id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" binary | \n",
" 0.0 | \n",
" 1.0 | \n",
" <NA> | \n",
" [] | \n",
" <NA> | \n",
" <NA> | \n",
" 0.0 | \n",
"
\n",
" \n",
" 1 | \n",
" binary | \n",
" 0.0 | \n",
" 1.0 | \n",
" <NA> | \n",
" [] | \n",
" <NA> | \n",
" <NA> | \n",
" 0.0 | \n",
"
\n",
" \n",
" 2 | \n",
" binary | \n",
" 0.0 | \n",
" 1.0 | \n",
" <NA> | \n",
" [] | \n",
" <NA> | \n",
" <NA> | \n",
" 1.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" kind lower upper name subscripts description substituted_value value\n",
"id \n",
"0 binary 0.0 1.0 [] 0.0\n",
"1 binary 0.0 1.0 [] 0.0\n",
"2 binary 0.0 1.0 [] 1.0"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"solution = sample_set.best_feasible()\n",
"\n",
"print(f\"{solution.objective=}\")\n",
"solution.decision_variables"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" objective | \n",
" feasible | \n",
"
\n",
" \n",
" sample_id | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" 1 | \n",
" 4.0 | \n",
" False | \n",
"
\n",
" \n",
" 0 | \n",
" 3.0 | \n",
" False | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" objective feasible\n",
"sample_id \n",
"1 4.0 False\n",
"0 3.0 False"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"No feasible solution found in SampleSet\n"
]
}
],
"source": [
"sample_set_infeasible = instance.evaluate_samples([\n",
" {0: 1, 1: 1, 2: 0}, # Infeasible since x0 + x1 + x2 = 2\n",
" {0: 1, 1: 0, 2: 1}, # Infeasible since x0 + x1 + x2 = 2\n",
"])\n",
"\n",
"# Every samples are infeasible\n",
"display(sample_set_infeasible.summary)\n",
"\n",
"try:\n",
" sample_set_infeasible.best_feasible()\n",
" assert False # best_feasible() should raise RuntimeError\n",
"except RuntimeError as e:\n",
" print(e)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```{note}\n",
"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.\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}