{ "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
objectivefeasible
sample_id
13.0True
01.0True
23.0False
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
kindloweruppernamesubscriptsdescriptionsubstituted_valuevalue
id
0binary0.01.0<NA>[]<NA><NA>1.0
1binary0.01.0<NA>[]<NA><NA>0.0
2binary0.01.0<NA>[]<NA><NA>0.0
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
kindloweruppernamesubscriptsdescriptionsubstituted_valuevalue
id
0binary0.01.0<NA>[]<NA><NA>0.0
1binary0.01.0<NA>[]<NA><NA>0.0
2binary0.01.0<NA>[]<NA><NA>1.0
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
objectivefeasible
sample_id
14.0False
03.0False
\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 }