diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index e3be0ae..200fea7 100644 --- a/GAMSPy_integration_example/trnsport_cuopt.ipynb +++ b/GAMSPy_integration_example/trnsport_cuopt.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2025-07-17T10:21:15.883029Z", @@ -42,14 +42,66 @@ } ], "source": [ - "# remove -q to debug issues with pip installs\n", - "!pip install --upgrade -q --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-cuda-runtime-cu12==12.8.* nvidia-nvjitlink-cu12\n", - "!pip install -q gamspy\n", + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "import subprocess\n", - "import sys\n", - "!wget -nc -nv -O cuopt-link-release.zip \"https://github.com/GAMS-dev/cuoptlink-builder/releases/download/v0.0.1/cuopt-link-release.zip\"\n", - "gams_base_path = subprocess.check_output([sys.executable, '-m', 'gamspy', 'show', 'base']).decode('utf-8').strip()\n", - "subprocess.run(f\"unzip -o cuopt-link-release.zip -d {gams_base_path}\", shell=True, check=True)" + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index e3b6aec..6c28a1a 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -1,168 +1,225 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [], - "gpuType": "T4" - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - }, - "accelerator": "GPU" + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "fMaKbZo6Afgd" + }, + "source": [ + "# Production Planning Problem Example with PuLP\n", + "\n", + "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", + "\n", + "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", + "\n", + "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", + "\n", + "If you are running this in the cuOpt container, you are good to go!\n", + "\n", + "\n", + "## 1. Install Dependencies\n", + "\n", + "To make sure we are good to go, let's install PuLP and cuOpt.\n", + "\n", + "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", + "\n", + "\n", + "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." + ] }, - "cells": [ - { - "cell_type": "markdown", - "source": [ - "# Production Planning Problem Example with PuLP\n", - "\n", - "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", - "\n", - "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", - "\n", - "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", - "\n", - "If you are running this in the cuOpt container, you are good to go!\n", - "\n", - "\n", - "## 1. Install Dependencies\n", - "\n", - "To make sure we are good to go, let's install PuLP and cuOpt.\n", - "\n", - "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", - "\n", - "\n", - "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." - ], - "metadata": { - "id": "fMaKbZo6Afgd" - } - }, - { - "cell_type": "code", - "source": [ - "!pip install pulp==3.2.0" - ], - "metadata": { - "id": "T2L7jTld2Qqj" - }, - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ], - "metadata": { - "collapsed": true, - "id": "tFLzH53z2Qoc" - }, - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "source": [ - "## 2. Problem Setup\n", - "\n", - "Let's consider the following problem:\n", - "\n", - "A factory produces two products (x₁ and x₂) with the following constraints: \n", - "- Profit: \\$20 per unit of x₁, \\$120 per unit of x₂ \n", - "- Resources: \n", - " - Material: 3 units/kg per x₁, 2 units/kg per x₂ (max 240 kg available) \n", - " - Labor: 2 hours per x₁, 4 hours per x₂ (max 180 hours available) \n", - "- Special machine: Optional \\$1000 fixed cost to enable production of x₂ (requires minimum 10 units of x₂ if used)\n", - "\n", - "Key Features: \n", - "1. Mixed variables: \n", - " - Integer variables for product quantities (x₁, x₂) \n", - " - Binary variable for machine activation (y) \n", - "\n", - "2. Conditional logic: \n", - " - The constraint `3*x1 + 2*x2 <= 240` correlates to the cost of materials\n", - " - The constraint `2*x1 + 4*x2 <= 180 ` correlates to the cost of labor\n", - " - The constraint `x2 >= 5*y` enforces that if the machine is used (y=1), at least 5 units of x₂ must be produced. \n", - " - The constraints `x1 >= 1` and `x2 >= 1` prevent trivial solutions, enforcing that we have both x1 and x2 in the solution.\n", - "\n", - "\n", - "3. Cost-benefit tradeoff: \n", - " The $1000 machine cost in the objective function creates a break-even analysis challenge. \n", - "\n", - "This formulation demonstrates how MIP models can handle both discrete decisions (machine usage) and continuous production quantities while optimizing complex business decisions.\n" - ], - "metadata": { - "id": "VeTiQIUJEQbR" - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "0Xw4x3_W14TU" - }, - "outputs": [], - "source": [ - "from pulp import *\n", - "\n", - "# Define the problem\n", - "problem = LpProblem(\"Production_Planning\", LpMaximize)\n", - "\n", - "# Decision variables\n", - "x1 = LpVariable('x1', lowBound=0, cat='Integer') # Product 1 units\n", - "x2 = LpVariable('x2', lowBound=0, cat='Integer') # Product 2 units\n", - "y = LpVariable('y', cat='Binary') # Machine usage flag\n", - "\n", - "# Objective function: Maximize profit\n", - "problem += 20.0*x1 + 120.0*x2 + 1000.0*y, \"Total_Profit\"\n", - "\n", - "# Constraints\n", - "problem += 3.0*x1 + 2.0*x2 <= 240.0, \"Material_limit_x2\"\n", - "problem += 2.0*x1 + 4.0*x2 <= 180.0, \"Labor_limit_x2\"\n", - "problem += x2 >= 5.0*y, \"Minimum_x₂_if_machine_used\"\n", - "problem += x1 >= 1.0, \"Prevent_trivial_solution_x1\"\n", - "problem += x2 >= 1.0, \"Prevent_trivial_solution_x2\"\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "source": [ - "## 3. Problem Solution\n", - "\n", - "PuLP calls on the cuOpt solver, which finds the optimal values of x1, x2, and y that maximize the profit while satisfying the constraints." - ], - "metadata": { - "id": "OG02AqK2LpZ1" - } - }, - { - "cell_type": "code", - "source": [ - "\n", - "# Solve the problem using CUOPT\n", - "problem.solve(CUOPT(msg=0))\n", - "\n", - "# Print results\n", - "print(\"Status:\", LpStatus[problem.status])\n", - "print(\"x1 =\", round(x1.varValue))\n", - "print(\"x2 =\", round(x2.varValue))\n", - "print(\"y =\", round(y.varValue))\n", - "print(\"Total Profit =\", round(value(problem.objective)))" - ], - "metadata": { - "id": "UL0TM5pTLp_m" - }, - "execution_count": null, - "outputs": [] - } - ] -} \ No newline at end of file + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T2L7jTld2Qqj" + }, + "outputs": [], + "source": [ + "!pip install pulp==3.3.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true, + "id": "tFLzH53z2Qoc" + }, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VeTiQIUJEQbR" + }, + "source": [ + "## 2. Problem Setup\n", + "\n", + "Let's consider the following problem:\n", + "\n", + "A factory produces two products (x₁ and x₂) with the following constraints: \n", + "- Profit: \\$20 per unit of x₁, \\$120 per unit of x₂ \n", + "- Resources: \n", + " - Material: 3 units/kg per x₁, 2 units/kg per x₂ (max 240 kg available) \n", + " - Labor: 2 hours per x₁, 4 hours per x₂ (max 180 hours available) \n", + "- Special machine: Optional \\$1000 fixed cost to enable production of x₂ (requires minimum 10 units of x₂ if used)\n", + "\n", + "Key Features: \n", + "1. Mixed variables: \n", + " - Integer variables for product quantities (x₁, x₂) \n", + " - Binary variable for machine activation (y) \n", + "\n", + "2. Conditional logic: \n", + " - The constraint `3*x1 + 2*x2 <= 240` correlates to the cost of materials\n", + " - The constraint `2*x1 + 4*x2 <= 180 ` correlates to the cost of labor\n", + " - The constraint `x2 >= 5*y` enforces that if the machine is used (y=1), at least 5 units of x₂ must be produced. \n", + " - The constraints `x1 >= 1` and `x2 >= 1` prevent trivial solutions, enforcing that we have both x1 and x2 in the solution.\n", + "\n", + "\n", + "3. Cost-benefit tradeoff: \n", + " The $1000 machine cost in the objective function creates a break-even analysis challenge. \n", + "\n", + "This formulation demonstrates how MIP models can handle both discrete decisions (machine usage) and continuous production quantities while optimizing complex business decisions.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0Xw4x3_W14TU" + }, + "outputs": [], + "source": [ + "from pulp import *\n", + "\n", + "# Define the problem\n", + "problem = LpProblem(\"Production_Planning\", LpMaximize)\n", + "\n", + "# Decision variables\n", + "x1 = LpVariable('x1', lowBound=0, cat='Integer') # Product 1 units\n", + "x2 = LpVariable('x2', lowBound=0, cat='Integer') # Product 2 units\n", + "y = LpVariable('y', cat='Binary') # Machine usage flag\n", + "\n", + "# Objective function: Maximize profit\n", + "problem += 20.0*x1 + 120.0*x2 + 1000.0*y, \"Total_Profit\"\n", + "\n", + "# Constraints\n", + "problem += 3.0*x1 + 2.0*x2 <= 240.0, \"Material_limit_x2\"\n", + "problem += 2.0*x1 + 4.0*x2 <= 180.0, \"Labor_limit_x2\"\n", + "problem += x2 >= 5.0*y, \"Minimum_x₂_if_machine_used\"\n", + "problem += x1 >= 1.0, \"Prevent_trivial_solution_x1\"\n", + "problem += x2 >= 1.0, \"Prevent_trivial_solution_x2\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OG02AqK2LpZ1" + }, + "source": [ + "## 3. Problem Solution\n", + "\n", + "PuLP calls on the cuOpt solver, which finds the optimal values of x1, x2, and y that maximize the profit while satisfying the constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UL0TM5pTLp_m" + }, + "outputs": [], + "source": [ + "\n", + "# Solve the problem using CUOPT\n", + "problem.solve(CUOPT(msg=0))\n", + "\n", + "# Print results\n", + "print(\"Status:\", LpStatus[problem.status])\n", + "print(\"x1 =\", round(x1.varValue))\n", + "print(\"x2 =\", round(x2.varValue))\n", + "print(\"y =\", round(y.varValue))\n", + "print(\"Total Profit =\", round(value(problem.objective)))" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/PuLP_integration_example/Simple_LP_pulp.ipynb b/PuLP_integration_example/Simple_LP_pulp.ipynb index 729b7ca..59c6c8a 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -1,144 +1,201 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [], - "gpuType": "T4" - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - }, - "accelerator": "GPU" + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "v2o08jmQi5lz" + }, + "source": [ + "# Simple Linear Programming (LP) Example with PuLP\n", + "\n", + "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", + "\n", + "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple LP problem with cuOpt.\n", + "\n", + "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", + "\n", + "If you are running this in the cuOpt container, you are good to go!\n", + "\n", + "This example is adapted from CVXPY. You can look at the original example [here](https://www.cvxpy.org/examples/basic/linear_program.html)\n", + "\n", + "## 1. Install Dependencies\n", + "\n", + "To make sure we are good to go, let's install PuLP and cuOpt.\n", + "\n", + "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", + "\n", + "\n", + "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt.\n" + ] }, - "cells": [ - { - "cell_type": "markdown", - "source": [ - "# Simple Linear Programming (LP) Example with PuLP\n", - "\n", - "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", - "\n", - "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple LP problem with cuOpt.\n", - "\n", - "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", - "\n", - "If you are running this in the cuOpt container, you are good to go!\n", - "\n", - "This example is adapted from CVXPY. You can look at the original example [here](https://www.cvxpy.org/examples/basic/linear_program.html)\n", - "\n", - "## 1. Install Dependencies\n", - "\n", - "To make sure we are good to go, let's install PuLP and cuOpt.\n", - "\n", - "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", - "\n", - "\n", - "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt.\n" - ], - "metadata": { - "id": "v2o08jmQi5lz" - } - }, - { - "cell_type": "code", - "source": [ - "!pip install pulp==3.2.0" - ], - "metadata": { - "id": "QSq2W3W7ojKI" - }, - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ], - "metadata": { - "id": "sb7vBllkojMN" - }, - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "source": [ - "## 2. Problem Setup\n", - "\n", - "This optimization problem defines a randomly generated linear program (LP) with 10 decision variables and 15 inequality constraints. The objective is to minimize a linear function of the variables, defined by a vector c, subject to linear inequality constraints of the form Ax≤b, where the matrix A and vector b are constructed to ensure feasibility using random values.\n" - ], - "metadata": { - "id": "h5GVfdwxPkrL" - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "fhrdgpmdiD_6" - }, - "outputs": [], - "source": [ - "# Import packages.\n", - "from pulp import *\n", - "import numpy as np\n", - "\n", - "# Generate a random non-trivial linear program.\n", - "m = 15\n", - "n = 10\n", - "np.random.seed(1)\n", - "s0 = np.random.randn(m)\n", - "lamb0 = np.maximum(-s0, 0)\n", - "s0 = np.maximum(s0, 0)\n", - "x0 = np.random.randn(n)\n", - "A = np.random.randn(m, n)\n", - "b = A @ x0 + s0\n", - "c = -A.T @ lamb0\n", - "\n", - "# Define and solve the Pulp problem\n", - "prob = LpProblem(\"LP_example\", LpMinimize)\n", - "x = [LpVariable(f\"x{i}\", lowBound=None) for i in range(n)]\n", - "prob += lpSum([c[i] * x[i] for i in range(n)]), \"Objective\"\n", - "\n", - "for i in range(m):\n", - " prob += lpSum([A[i, j] * x[j] for j in range(n)]) <= b[i], f\"Constraint_{i}\"" - ] - }, - { - "cell_type": "markdown", - "source": [ - "## 3. Problem Solution\n", - "The problem is solved using the CUOPT solver, and the solution yields both the minimum objective value and the corresponding optimal variable values x. This setup demonstrates how to programmatically generate and solve a non-trivial LP using PuLP and NumPy." - ], - "metadata": { - "id": "GIFHcgTMP9qW" - } - }, - { - "cell_type": "code", - "source": [ - "status = prob.solve(CUOPT(msg=0))\n", - "\n", - "# Print results\n", - "print(\"\\nThe optimal value is\", value(prob.objective))\n", - "x_vals = np.array([x[i].varValue for i in range(n)])\n", - "np.set_printoptions(precision=8, suppress=True)\n", - "print(\"A solution x is\")\n", - "print(x_vals)" - ], - "metadata": { - "id": "b1OShyAqqVu8" - }, - "execution_count": null, - "outputs": [] - } - ] + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QSq2W3W7ojKI" + }, + "outputs": [], + "source": [ + "!pip install pulp==3.3.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sb7vBllkojMN" + }, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h5GVfdwxPkrL" + }, + "source": [ + "## 2. Problem Setup\n", + "\n", + "This optimization problem defines a randomly generated linear program (LP) with 10 decision variables and 15 inequality constraints. The objective is to minimize a linear function of the variables, defined by a vector c, subject to linear inequality constraints of the form Ax≤b, where the matrix A and vector b are constructed to ensure feasibility using random values.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fhrdgpmdiD_6" + }, + "outputs": [], + "source": [ + "# Import packages.\n", + "from pulp import *\n", + "import numpy as np\n", + "\n", + "# Generate a random non-trivial linear program.\n", + "m = 15\n", + "n = 10\n", + "np.random.seed(1)\n", + "s0 = np.random.randn(m)\n", + "lamb0 = np.maximum(-s0, 0)\n", + "s0 = np.maximum(s0, 0)\n", + "x0 = np.random.randn(n)\n", + "A = np.random.randn(m, n)\n", + "b = A @ x0 + s0\n", + "c = -A.T @ lamb0\n", + "\n", + "# Define and solve the Pulp problem\n", + "prob = LpProblem(\"LP_example\", LpMinimize)\n", + "x = [LpVariable(f\"x{i}\", lowBound=None) for i in range(n)]\n", + "prob += lpSum([c[i] * x[i] for i in range(n)]), \"Objective\"\n", + "\n", + "for i in range(m):\n", + " prob += lpSum([A[i, j] * x[j] for j in range(n)]) <= b[i], f\"Constraint_{i}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GIFHcgTMP9qW" + }, + "source": [ + "## 3. Problem Solution\n", + "The problem is solved using the CUOPT solver, and the solution yields both the minimum objective value and the corresponding optimal variable values x. This setup demonstrates how to programmatically generate and solve a non-trivial LP using PuLP and NumPy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b1OShyAqqVu8" + }, + "outputs": [], + "source": [ + "status = prob.solve(CUOPT(msg=0))\n", + "\n", + "# Print results\n", + "print(\"\\nThe optimal value is\", value(prob.objective))\n", + "x_vals = np.array([x[i].varValue for i in range(n)])\n", + "np.set_printoptions(precision=8, suppress=True)\n", + "print(\"A solution x is\")\n", + "print(x_vals)" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/PuLP_integration_example/Simple_MIP_pulp.ipynb b/PuLP_integration_example/Simple_MIP_pulp.ipynb index 28bf795..25038dc 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -1,148 +1,205 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "fMaKbZo6Afgd" - }, - "source": [ - "# Simple Mixed Integer Programming (MIP) Example with PuLP\n", - "\n", - "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", - "\n", - "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", - "\n", - "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", - "\n", - "If you are running this in the cuOpt container, you are good to go!\n", - "\n", - "\n", - "## 1. Install Dependencies\n", - "\n", - "To make sure we are good to go, let's install PuLP and cuOpt.\n", - "\n", - "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", - "\n", - "\n", - "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "T2L7jTld2Qqj" - }, - "outputs": [], - "source": [ - "pip install pulp==3.2.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true, - "id": "tFLzH53z2Qoc" - }, - "outputs": [], - "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "VeTiQIUJEQbR" - }, - "source": [ - "## 2. Problem Setup\n", - "\n", - "In this example, the goal is to minimize the objective function 2x+3y, where x is an integer variable and y is a continuous variable constrained to be non-negative. The problem is subject to two constraints: x+y≥10 and x≤15." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "0Xw4x3_W14TU" - }, - "outputs": [], - "source": [ - "from pulp import *\n", - "\n", - "# Define the problem\n", - "problem = LpProblem(\"Integer_Optimization\", LpMinimize)\n", - "\n", - "# Define variables\n", - "x = LpVariable('x', cat='Integer') # Integer\n", - "y = LpVariable('y', lowBound=0.0) # Non-negative\n", - "\n", - "# Objective function\n", - "problem += 2.0 * x + 3.0 * y, \"Objective\"\n", - "\n", - "# Constraints\n", - "problem += x + y >= 10.0, \"Constraint1\"\n", - "problem += x <= 15.0, \"Constraint2\"\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OG02AqK2LpZ1" - }, - "source": [ - "## 3. Problem Solution\n", - "\n", - "PuLP calls on the cuOpt solver, which finds the optimal values of x and y that minimize the objective while satisfying the constraints." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "UL0TM5pTLp_m" - }, - "outputs": [], - "source": [ - "\n", - "# Solve the problem using CUOPT\n", - "status = problem.solve(CUOPT(msg=0))\n", - "\n", - "# Print results\n", - "print(\"Status:\", LpStatus[status])\n", - "print(\"Optimal Value:\", value(problem.objective))\n", - "print(\"x =\", x.varValue)\n", - "print(\"y =\", y.varValue)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "QPvNP2ZMH9ik" - }, - "source": [ - "We can see that cuOpt quickly solves the problem, with the final solution being x = 10.0\n", - "y = 0.0" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "name": "python" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "fMaKbZo6Afgd" + }, + "source": [ + "# Simple Mixed Integer Programming (MIP) Example with PuLP\n", + "\n", + "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", + "\n", + "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", + "\n", + "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", + "\n", + "If you are running this in the cuOpt container, you are good to go!\n", + "\n", + "\n", + "## 1. Install Dependencies\n", + "\n", + "To make sure we are good to go, let's install PuLP and cuOpt.\n", + "\n", + "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", + "\n", + "\n", + "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T2L7jTld2Qqj" + }, + "outputs": [], + "source": [ + "!pip install pulp==3.3.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true, + "id": "tFLzH53z2Qoc" + }, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VeTiQIUJEQbR" + }, + "source": [ + "## 2. Problem Setup\n", + "\n", + "In this example, the goal is to minimize the objective function 2x+3y, where x is an integer variable and y is a continuous variable constrained to be non-negative. The problem is subject to two constraints: x+y≥10 and x≤15." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0Xw4x3_W14TU" + }, + "outputs": [], + "source": [ + "from pulp import *\n", + "\n", + "# Define the problem\n", + "problem = LpProblem(\"Integer_Optimization\", LpMinimize)\n", + "\n", + "# Define variables\n", + "x = LpVariable('x', cat='Integer') # Integer\n", + "y = LpVariable('y', lowBound=0.0) # Non-negative\n", + "\n", + "# Objective function\n", + "problem += 2.0 * x + 3.0 * y, \"Objective\"\n", + "\n", + "# Constraints\n", + "problem += x + y >= 10.0, \"Constraint1\"\n", + "problem += x <= 15.0, \"Constraint2\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OG02AqK2LpZ1" + }, + "source": [ + "## 3. Problem Solution\n", + "\n", + "PuLP calls on the cuOpt solver, which finds the optimal values of x and y that minimize the objective while satisfying the constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UL0TM5pTLp_m" + }, + "outputs": [], + "source": [ + "\n", + "# Solve the problem using CUOPT\n", + "status = problem.solve(CUOPT(msg=0))\n", + "\n", + "# Print results\n", + "print(\"Status:\", LpStatus[status])\n", + "print(\"Optimal Value:\", value(problem.objective))\n", + "print(\"x =\", x.varValue)\n", + "print(\"y =\", y.varValue)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QPvNP2ZMH9ik" + }, + "source": [ + "We can see that cuOpt quickly solves the problem, with the final solution being x = 10.0\n", + "y = 0.0" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index 74ba17b..994f6eb 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -1,219 +1,276 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "Aa5AQ8pLJsqF" - }, - "source": [ - "# Sudoku Example with PuLP\n", - "\n", - "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", - "\n", - "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", - "\n", - "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", - "\n", - "If you are running this in the cuOpt container, you are good to go!\n", - "\n", - "This example is borrowed from PuLP. You can find it on their website __[here](https://coin-or.github.io/pulp/CaseStudies/a_sudoku_problem.html)__\n", - "\n", - "\n", - "## 1. Install Dependencies\n", - "\n", - "To make sure we are good to go, let's install PuLP and cuOpt.\n", - "\n", - "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", - "\n", - "\n", - "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "LcKWoNcAmHK9" - }, - "outputs": [], - "source": [ - " !pip install pulp==3.2.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "CGnaZHCZk4hj" - }, - "outputs": [], - "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "mIX0AKStL4It" - }, - "source": [ - "## 2. Problem Setup\n", - "\n", - "In this problem, we will use solve the following Sudoku problem\n", - "\n", - "![Screenshot 2025-06-11 at 11.12.39 AM.png](data:image/png;base64,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JBcvXpTWrVtL8+bNk61fKl8nPnz99dfyzjvvJHtBHkACJEACkUhgxYoV0qJFi5dW3WeXwsKFC196IX5JAiRAApFMAG//yYnPBrdgwYLJXYvfkwAJkEDEEihevHiydffZh5shQwaPi40bN07Kly/vsc/ujT179siQIUPk448/lho1athdfILybt68KZ07d5bevXv75M9JcIEg7njw4IGHC6hevXoyaNCgIJYQ2KV69OghUVFRMnz48MAuEOSzBg8eLLdu3ZKpU6cG+cr+X+6TTz6RLVu2GCfiFTVz5szGthMr6Lf5/PPPZcGCBZInTx4nVPAo002/+ePHj0v//v0N/Xzp1/LZ4BpX/f+VqlWryptvvum929btJ0+eqPJef/11adCgga1lJ1bYTz/9pHa/9tprjuvz22+/eaiYL18+x3WCQjAguXPndoUu0GfixIny8OFDV+gzd+5cqGRInTp15C9/+Yux7cTKuXPnVLH4rb/66qtOqOBRppt+896NUA9Fk9jw2aWQxPncTQIkQAIk4CMBGlwfQfEwEiABEkgpARrclBLk+SRAAiTgIwEaXB9B8TASIAESSCkBGtyUEuT5JEACJOAjARpcH0HxMBIgARJIKQEa3JQS5PkkQAIk4COBgMfh+nh9j8NOnjwpjx8/9tinNzA+E+NX7ZQjR47Izp07BQEoihQpoiZPFC5c2E4VWJaPBDA54cqVK8kejTG+bhgvmqyiPCAiCdhqcBs2bCg3btxIFDQGVm/bti3R74K9848//pB3331X1q5d63HpV155Rc3GGjZsmKRKlcrjO244S2D58uXSq1evZJXATLZJkyYlexwPIAEnCNjmUrh7926Sxtbuivfr188wtlmzZpUKFSpImjRp5Pnz5zJy5EjxnvFjt34sjwRIIDwJ2NbCPXXqlCKYP3/+RA1a9uzZbSF8//59mT9/vioLrer169cLpuh988038vbbb6v9sbGx0rVrV1v0YSG+EUCs0VKlSiV68GeffabuY7p06eS9995L9BjuJAE3ELDd4JYtW9bReeuIdxAfA1ixb9KkiTK22MA6frBPnz6VX3/91Q33hjqYCBQqVEjw8ZbNmzfLd999p3YjiFG1atW8D+E2CbiGgG0G98yZM6rSpUuXVq3JHTt2yIsXLyQ6OloaN25sm88UYSbRqoWUKVNGLfEHUdthbCEVK1ZU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)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "lQdRtS9Bjdym" - }, - "outputs": [], - "source": [ - "\"\"\"\n", - "The Sudoku Problem Formulation for the PuLP Modeller\n", - "\n", - "Authors: Antony Phillips, Dr Stuart Mitchell\n", - "edited by Nathan Sudermann-Merx\n", - "\"\"\"\n", - "\n", - "# Import PuLP modeler functions\n", - "from pulp import *\n", - "\n", - "# All rows, columns and values within a Sudoku take values from 1 to 9\n", - "VALS = ROWS = COLS = range(1, 10)\n", - "\n", - "# The boxes list is created, with the row and column index of each square in each box\n", - "Boxes = [\n", - " [(3 * i + k + 1, 3 * j + l + 1) for k in range(3) for l in range(3)]\n", - " for i in range(3)\n", - " for j in range(3)\n", - "]\n", - "\n", - "# The prob variable is created to contain the problem data\n", - "prob = LpProblem(\"Sudoku Problem\")\n", - "\n", - "# The decision variables are created\n", - "choices = LpVariable.dicts(\"Choice\", (VALS, ROWS, COLS), cat=\"Binary\")\n", - "\n", - "# We do not define an objective function since none is needed\n", - "\n", - "# A constraint ensuring that only one value can be in each square is created\n", - "for r in ROWS:\n", - " for c in COLS:\n", - " prob += lpSum([choices[v][r][c] for v in VALS]) == 1\n", - "\n", - "# The row, column and box constraints are added for each value\n", - "for v in VALS:\n", - " for r in ROWS:\n", - " prob += lpSum([choices[v][r][c] for c in COLS]) == 1\n", - "\n", - " for c in COLS:\n", - " prob += lpSum([choices[v][r][c] for r in ROWS]) == 1\n", - "\n", - " for b in Boxes:\n", - " prob += lpSum([choices[v][r][c] for (r, c) in b]) == 1\n", - "\n", - "# The starting numbers are entered as constraints. \n", - "# For example `(5, 1, 1)` means that there's a 5 in row=1,column=1. \n", - "# Each number in our input problem is represented this way. All the indicies are 1-9, since that's the dimension of a Sudoku problem.\n", - "input_data = [\n", - " (5, 1, 1),\n", - " (6, 2, 1),\n", - " (8, 4, 1),\n", - " (4, 5, 1),\n", - " (7, 6, 1),\n", - " (3, 1, 2),\n", - " (9, 3, 2),\n", - " (6, 7, 2),\n", - " (8, 3, 3),\n", - " (1, 2, 4),\n", - " (8, 5, 4),\n", - " (4, 8, 4),\n", - " (7, 1, 5),\n", - " (9, 2, 5),\n", - " (6, 4, 5),\n", - " (2, 6, 5),\n", - " (1, 8, 5),\n", - " (8, 9, 5),\n", - " (5, 2, 6),\n", - " (3, 5, 6),\n", - " (9, 8, 6),\n", - " (2, 7, 7),\n", - " (6, 3, 8),\n", - " (8, 7, 8),\n", - " (7, 9, 8),\n", - " (3, 4, 9),\n", - " (1, 5, 9),\n", - " (6, 6, 9),\n", - " (5, 8, 9),\n", - "]\n", - "\n", - "for v, r, c in input_data:\n", - " prob += choices[v][r][c] == 1\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "8XGkqiWjNCuM" - }, - "source": [ - "## 3. Problem Solution\n", - "\n", - "PuLP calls on the cuOpt solver, which finds the missing values. Let's take a look at the solution." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "nTkHJsnklmW5" - }, - "outputs": [], - "source": [ - "# The problem is solved using cuOpt\n", - "prob.solve(CUOPT(msg=0))\n", - "\n", - "# The status of the solution is printed to the screen\n", - "print(\"Status:\", LpStatus[prob.status])\n", - "\n", - "# Print the solution\n", - "for r in ROWS:\n", - " if r in [1, 4, 7]:\n", - " print(\"+-------+-------+-------+\")\n", - " row_output = \"\"\n", - " for c in COLS:\n", - " for v in VALS:\n", - " if value(choices[v][r][c]) == 1:\n", - " if c in [1, 4, 7]:\n", - " row_output += \"| \"\n", - " row_output += str(v) + \" \"\n", - " if c == 9:\n", - " row_output += \"|\"\n", - " print(row_output)\n", - "print(\"+-------+-------+-------+\")\n" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "name": "python" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "Aa5AQ8pLJsqF" + }, + "source": [ + "# Sudoku Example with PuLP\n", + "\n", + "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", + "\n", + "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with PuLP and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", + "\n", + "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", + "\n", + "If you are running this in the cuOpt container, you are good to go!\n", + "\n", + "This example is borrowed from PuLP. You can find it on their website __[here](https://coin-or.github.io/pulp/CaseStudies/a_sudoku_problem.html)__\n", + "\n", + "\n", + "## 1. Install Dependencies\n", + "\n", + "To make sure we are good to go, let's install PuLP and cuOpt.\n", + "\n", + "__[PuLP](https://coin-or.github.io/pulp/)__ is a popular linear and mixed integer programming modeler written in Python.\n", + "\n", + "\n", + "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LcKWoNcAmHK9" + }, + "outputs": [], + "source": [ + " !pip install pulp==3.3.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CGnaZHCZk4hj" + }, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mIX0AKStL4It" + }, + "source": [ + "## 2. Problem Setup\n", + "\n", + "In this problem, we will use solve the following Sudoku problem\n", + "\n", + "![Screenshot 2025-06-11 at 11.12.39 AM.png](data:image/png;base64,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JBcvXpTWrVtL8+bNk61fKl8nPnz99dfyzjvvJHtBHkACJEACkUhgxYoV0qJFi5dW3WeXwsKFC196IX5JAiRAApFMAG//yYnPBrdgwYLJXYvfkwAJkEDEEihevHiydffZh5shQwaPi40bN07Kly/vsc/ujT179siQIUPk448/lho1athdfILybt68KZ07d5bevXv75M9JcIEg7njw4IGHC6hevXoyaNCgIJYQ2KV69OghUVFRMnz48MAuEOSzBg8eLLdu3ZKpU6cG+cr+X+6TTz6RLVu2GCfiFTVz5szGthMr6Lf5/PPPZcGCBZInTx4nVPAo002/+ePHj0v//v0N/Xzp1/LZ4BpX/f+VqlWryptvvum929btJ0+eqPJef/11adCgga1lJ1bYTz/9pHa/9tprjuvz22+/eaiYL18+x3WCQjAguXPndoUu0GfixIny8OFDV+gzd+5cqGRInTp15C9/+Yux7cTKuXPnVLH4rb/66qtOqOBRppt+896NUA9Fk9jw2aWQxPncTQIkQAIk4CMBGlwfQfEwEiABEkgpARrclBLk+SRAAiTgIwEaXB9B8TASIAESSCkBGtyUEuT5JEACJOAjARpcH0HxMBIgARJIKQEa3JQS5PkkQAIk4COBgMfh+nh9j8NOnjwpjx8/9tinNzA+E+NX7ZQjR47Izp07BQEoihQpoiZPFC5c2E4VWJaPBDA54cqVK8kejTG+bhgvmqyiPCAiCdhqcBs2bCg3btxIFDQGVm/bti3R74K9848//pB3331X1q5d63HpV155Rc3GGjZsmKRKlcrjO244S2D58uXSq1evZJXATLZJkyYlexwPIAEnCNjmUrh7926Sxtbuivfr188wtlmzZpUKFSpImjRp5Pnz5zJy5EjxnvFjt34sjwRIIDwJ2NbCPXXqlCKYP3/+RA1a9uzZbSF8//59mT9/vioLrer169cLpuh988038vbbb6v9sbGx0rVrV1v0YSG+EUCs0VKlSiV68GeffabuY7p06eS9995L9BjuJAE3ELDd4JYtW9bReeuIdxAfA1ixb9KkiTK22MA6frBPnz6VX3/91Q33hjqYCBQqVEjw8ZbNmzfLd999p3YjiFG1atW8D+E2CbiGgG0G98yZM6rSpUuXVq3JHTt2yIsXLyQ6OloaN25sm88UYSbRqoWUKVNGLfEHUdthbCEVK1ZU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)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lQdRtS9Bjdym" + }, + "outputs": [], + "source": [ + "\"\"\"\n", + "The Sudoku Problem Formulation for the PuLP Modeller\n", + "\n", + "Authors: Antony Phillips, Dr Stuart Mitchell\n", + "edited by Nathan Sudermann-Merx\n", + "\"\"\"\n", + "\n", + "# Import PuLP modeler functions\n", + "from pulp import *\n", + "\n", + "# All rows, columns and values within a Sudoku take values from 1 to 9\n", + "VALS = ROWS = COLS = range(1, 10)\n", + "\n", + "# The boxes list is created, with the row and column index of each square in each box\n", + "Boxes = [\n", + " [(3 * i + k + 1, 3 * j + l + 1) for k in range(3) for l in range(3)]\n", + " for i in range(3)\n", + " for j in range(3)\n", + "]\n", + "\n", + "# The prob variable is created to contain the problem data\n", + "prob = LpProblem(\"Sudoku Problem\")\n", + "\n", + "# The decision variables are created\n", + "choices = LpVariable.dicts(\"Choice\", (VALS, ROWS, COLS), cat=\"Binary\")\n", + "\n", + "# We do not define an objective function since none is needed\n", + "\n", + "# A constraint ensuring that only one value can be in each square is created\n", + "for r in ROWS:\n", + " for c in COLS:\n", + " prob += lpSum([choices[v][r][c] for v in VALS]) == 1\n", + "\n", + "# The row, column and box constraints are added for each value\n", + "for v in VALS:\n", + " for r in ROWS:\n", + " prob += lpSum([choices[v][r][c] for c in COLS]) == 1\n", + "\n", + " for c in COLS:\n", + " prob += lpSum([choices[v][r][c] for r in ROWS]) == 1\n", + "\n", + " for b in Boxes:\n", + " prob += lpSum([choices[v][r][c] for (r, c) in b]) == 1\n", + "\n", + "# The starting numbers are entered as constraints. \n", + "# For example `(5, 1, 1)` means that there's a 5 in row=1,column=1. \n", + "# Each number in our input problem is represented this way. All the indicies are 1-9, since that's the dimension of a Sudoku problem.\n", + "input_data = [\n", + " (5, 1, 1),\n", + " (6, 2, 1),\n", + " (8, 4, 1),\n", + " (4, 5, 1),\n", + " (7, 6, 1),\n", + " (3, 1, 2),\n", + " (9, 3, 2),\n", + " (6, 7, 2),\n", + " (8, 3, 3),\n", + " (1, 2, 4),\n", + " (8, 5, 4),\n", + " (4, 8, 4),\n", + " (7, 1, 5),\n", + " (9, 2, 5),\n", + " (6, 4, 5),\n", + " (2, 6, 5),\n", + " (1, 8, 5),\n", + " (8, 9, 5),\n", + " (5, 2, 6),\n", + " (3, 5, 6),\n", + " (9, 8, 6),\n", + " (2, 7, 7),\n", + " (6, 3, 8),\n", + " (8, 7, 8),\n", + " (7, 9, 8),\n", + " (3, 4, 9),\n", + " (1, 5, 9),\n", + " (6, 6, 9),\n", + " (5, 8, 9),\n", + "]\n", + "\n", + "for v, r, c in input_data:\n", + " prob += choices[v][r][c] == 1\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8XGkqiWjNCuM" + }, + "source": [ + "## 3. Problem Solution\n", + "\n", + "PuLP calls on the cuOpt solver, which finds the missing values. Let's take a look at the solution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nTkHJsnklmW5" + }, + "outputs": [], + "source": [ + "# The problem is solved using cuOpt\n", + "prob.solve(CUOPT(msg=0))\n", + "\n", + "# The status of the solution is printed to the screen\n", + "print(\"Status:\", LpStatus[prob.status])\n", + "\n", + "# Print the solution\n", + "for r in ROWS:\n", + " if r in [1, 4, 7]:\n", + " print(\"+-------+-------+-------+\")\n", + " row_output = \"\"\n", + " for c in COLS:\n", + " for v in VALS:\n", + " if value(choices[v][r][c]) == 1:\n", + " if c in [1, 4, 7]:\n", + " row_output += \"| \"\n", + " row_output += str(v) + \" \"\n", + " if c == 9:\n", + " row_output += \"|\"\n", + " print(row_output)\n", + "print(\"+-------+-------+-------+\")\n" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index dc26d98..7b9a732 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -1,459 +1,505 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Diet Optimization with cuOpt Python API\n", - "\n", - "This notebook demonstrates how to solve the classic diet optimization problem using the cuOpt Python API. The problem involves selecting foods to meet nutritional requirements while minimizing cost.\n", - "\n", - "## Problem Description\n", - "\n", - "We need to select quantities of different foods to:\n", - "- Meet minimum and maximum nutritional requirements\n", - "- Minimize total cost\n", - "- Satisfy additional constraints (like limiting dairy servings)\n", - "\n", - "The nutrition guidelines are based on USDA Dietary Guidelines for Americans, 2005.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Environment Setup\n", - "\n", - "First, let's check if we have a GPU available and install necessary dependencies.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check for GPU availability\n", - "!nvidia-smi\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Install cuOpt if not already installed\n", - "# Uncomment the following line if running in Google Colab or similar environment\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12 # For cuda 12\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 # For cuda 13\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Import Required Libraries\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", - "from cuopt.linear_programming.solver_settings import SolverSettings\n", - "import time\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem Data Setup\n", - "\n", - "Define the nutrition guidelines, food costs, and nutritional values for each food item.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Nutrition guidelines based on USDA Dietary Guidelines for Americans, 2005\n", - "# http://www.health.gov/DietaryGuidelines/dga2005/\n", - "\n", - "# minimum and maximum values for each category\n", - "categories = {\n", - " \"calories\": {\n", - " \"min\": 1800,\n", - " \"max\": 2200\n", - " },\n", - " \"protein\": {\n", - " \"min\": 91,\n", - " \"max\": float('inf')\n", - " },\n", - " \"fat\": {\n", - " \"min\": 0,\n", - " \"max\": 65\n", - " },\n", - " \"sodium\": {\n", - " \"min\": 0,\n", - " \"max\": 1779\n", - " }\n", - "}\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Food costs per serving\n", - "food_costs = {\n", - " \"hamburger\": 2.49,\n", - " \"chicken\": 2.89,\n", - " \"hot dog\": 1.50,\n", - " \"fries\": 1.89,\n", - " \"macaroni\": 2.09,\n", - " \"pizza\": 1.99,\n", - " \"salad\": 2.49,\n", - " \"milk\": 0.89,\n", - " \"ice cream\": 1.59\n", - "}\n", - "\n", - "# Nutrition values for each food (per serving)\n", - "nutrition_data = {\n", - " \"hamburger\": [410, 24, 26, 730],\n", - " \"chicken\": [420, 32, 10, 1190],\n", - " \"hot dog\": [560, 20, 32, 1800],\n", - " \"fries\": [380, 4, 19, 270],\n", - " \"macaroni\": [320, 12, 10, 930],\n", - " \"pizza\": [320, 15, 12, 820],\n", - " \"salad\": [320, 31, 12, 1230],\n", - " \"milk\": [100, 8, 2.5, 125],\n", - " \"ice cream\": [330, 8, 10, 180]\n", - "}\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a DataFrame for better visualization\n", - "nutrition_df = pd.DataFrame(nutrition_data, index=categories.keys()).T\n", - "nutrition_df.columns = [f\"{cat} (per serving)\" for cat in categories.keys()]\n", - "print(\"Nutritional Values per Serving:\")\n", - "print(nutrition_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem Formulation\n", - "\n", - "Now we'll create the optimization problem using the cuOpt Python API as LP. The problem has:\n", - "- **Variables**: Amount of each food to buy (continuous, non-negative)\n", - "- **Objective**: Minimize total cost\n", - "- **Constraints**: Meet nutritional requirements (minimum and maximum bounds)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the optimization problem\n", - "problem = Problem(\"diet_optimization\")\n", - "\n", - "# Add decision variables for each food (amount to buy)\n", - "buy_vars = {}\n", - "for food_name in food_costs:\n", - " var = problem.addVariable(name=f\"{food_name}\", vtype=VType.CONTINUOUS, lb=0.0, ub=float('inf'))\n", - " buy_vars[food_name] = var\n", - "\n", - "print(f\"Created {len(buy_vars)} decision variables for foods\")\n", - "print(f\"Variables: {[var.getVariableName() for var in buy_vars.values()]}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set objective function: minimize total cost\n", - "objective_expr = LinearExpression([], [], 0.0)\n", - "\n", - "for var in buy_vars.values():\n", - " if food_costs[var.getVariableName()] != 0: # Only include non-zero coefficients\n", - " objective_expr += var * food_costs[var.getVariableName()]\n", - "\n", - "# Set objective function: minimize total cost\n", - "problem.setObjective(objective_expr, sense.MINIMIZE)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Add nutrition constraints\n", - "constraint_names = []\n", - "\n", - "for i, category in enumerate(categories):\n", - " # Calculate total nutrition from all foods for this category\n", - " nutrition_expr = LinearExpression([], [], 0.0)\n", - " \n", - " for food_name in food_costs: \n", - " nutrition_value = nutrition_data[food_name][i]\n", - " if nutrition_value != 0: # Only include non-zero coefficients\n", - " nutrition_expr += buy_vars[food_name] * nutrition_value\n", - " \n", - " # Add constraint: min_nutrition[i] <= nutrition_expr <= max_nutrition[i]\n", - " min_val = categories[category][\"min\"]\n", - " max_val = categories[category][\"max\"]\n", - " \n", - " if max_val == float('inf'):\n", - " # Only lower bound constraint\n", - " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", - " constraint_names.append(f\"min_{category}\")\n", - " else:\n", - " # Range constraint (both lower and upper bounds)\n", - " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", - " constraint_names.append(f\"min_{category}\")\n", - " constraint = problem.addConstraint(nutrition_expr <= max_val, name=f\"max_{category}\")\n", - " constraint_names.append(f\"max_{category}\")\n", - "\n", - "print(f\"Added {len(constraint_names)} nutrition constraints\")\n", - "print(f\"Constraints: {constraint_names}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solver Configuration and Solution\n", - "\n", - "Configure the solver settings and solve the optimization problem.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Configure solver settings\n", - "settings = SolverSettings()\n", - "settings.set_parameter(\"time_limit\", 60.0) # 60 second time limit\n", - "settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", - "settings.set_parameter(\"method\", 0) # Use default method\n", - "\n", - "print(\"Solver configured with 60-second time limit\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Solve the problem\n", - "print(\"Solving diet optimization problem...\")\n", - "print(f\"Problem type: {'MIP' if problem.IsMIP else 'LP'}\")\n", - "\n", - "start_time = time.time()\n", - "problem.solve(settings)\n", - "solve_time = time.time() - start_time\n", - "\n", - "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def print_solution():\n", - " \"\"\"Print the optimal solution in a readable format\"\"\"\n", - " if problem.Status.name == \"Optimal\":\n", - " print(f\"\\nOptimal Solution Found!\")\n", - " print(f\"Total Cost: ${problem.ObjValue:.2f}\")\n", - " print(\"\\nFood Purchases:\")\n", - " \n", - " total_cost = 0\n", - " for var in buy_vars.values():\n", - " amount = var.getValue()\n", - " if amount > 0.0001: # Only show foods with significant amounts\n", - " food_cost = amount * food_costs[var.getVariableName()]\n", - " total_cost += food_cost\n", - " print(f\" {var.getVariableName()}: {amount:.3f} servings (${food_cost:.2f})\")\n", - " \n", - " print(f\"\\nTotal Cost: ${total_cost:.2f}\")\n", - " \n", - " # Check nutritional intake\n", - " print(\"\\nNutritional Intake:\")\n", - " for i, category in enumerate(categories):\n", - " total_nutrition = 0\n", - " for var in buy_vars.values():\n", - " amount = var.getValue()\n", - " nutrition_value = nutrition_data[var.getVariableName()][i]\n", - " total_nutrition += amount * nutrition_value\n", - " \n", - " min_req = categories[category][\"min\"]\n", - " max_req = categories[category][\"max\"]\n", - " \n", - " # Check constraints with tolerance for floating point precision\n", - " tolerance = 1e-6\n", - " min_satisfied = total_nutrition >= (min_req - tolerance)\n", - " max_satisfied = (max_req == float('inf')) or (total_nutrition <= (max_req + tolerance))\n", - " status = \"✓\" if (min_satisfied and max_satisfied) else \"✗\"\n", - " \n", - " if max_req == float('inf'):\n", - " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}) {status}\")\n", - " else:\n", - " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}, max: {max_req}) {status}\")\n", - " else:\n", - " print(f\"No optimal solution found. Status: {problem.Status.name}\")\n", - "\n", - "print_solution()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Additional Constraints\n", - "\n", - "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit dairy servings to at most 6.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create LinearExpression for dairy constraint\n", - "dairy_expr = buy_vars[\"milk\"] + buy_vars[\"ice cream\"]\n", - "dairy_constraint = problem.addConstraint(dairy_expr <= 6, name=\"limit_dairy\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Solve the problem again with the new constraint\n", - "print(\"\\nSolving with dairy constraint...\")\n", - "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", - "\n", - "start_time = time.time()\n", - "problem.solve(settings)\n", - "solve_time = time.time() - start_time\n", - "\n", - "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solution Comparison\n", - "\n", - "Let's compare the solutions before and after adding the dairy constraint to see the impact.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Display the new solution\n", - "print_solution()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "This notebook demonstrated how to:\n", - "\n", - "1. **Formulate a diet optimization problem** using the cuOpt Python API\n", - "2. **Set up decision variables** for food quantities\n", - "3. **Define an objective function** to minimize total cost\n", - "4. **Add nutritional constraints** with both lower and upper bounds\n", - "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", - "6. **Add additional constraints** to the existing model\n", - "7. **Analyze and compare solutions** before and after constraint modifications\n", - "\n", - "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like diet optimization.\n", - "\n", - "### Key Benefits of cuOpt:\n", - "- **High Performance**: GPU-accelerated solving for large-scale problems\n", - "- **Easy to Use**: Intuitive Python API similar to other optimization libraries\n", - "- **Flexible**: Support for both LP and MIP problems\n", - "- **Scalable**: Handles problems with thousands of variables and constraints efficiently\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", - "SPDX-License-Identifier: MIT\n", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", - "\n", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cuopt", - "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.12.11" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Diet Optimization with cuOpt Python API\n", + "\n", + "This notebook demonstrates how to solve the classic diet optimization problem using the cuOpt Python API. The problem involves selecting foods to meet nutritional requirements while minimizing cost.\n", + "\n", + "## Problem Description\n", + "\n", + "We need to select quantities of different foods to:\n", + "- Meet minimum and maximum nutritional requirements\n", + "- Minimize total cost\n", + "- Satisfy additional constraints (like limiting dairy servings)\n", + "\n", + "The nutrition guidelines are based on USDA Dietary Guidelines for Americans, 2005.\n" + ] }, - "nbformat": 4, - "nbformat_minor": 2 + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Environment Setup\n", + "\n", + "First, let's check if we have a GPU available and install necessary dependencies.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import Required Libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", + "from cuopt.linear_programming.solver_settings import SolverSettings\n", + "import time\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Data Setup\n", + "\n", + "Define the nutrition guidelines, food costs, and nutritional values for each food item.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Nutrition guidelines based on USDA Dietary Guidelines for Americans, 2005\n", + "# http://www.health.gov/DietaryGuidelines/dga2005/\n", + "\n", + "# minimum and maximum values for each category\n", + "categories = {\n", + " \"calories\": {\n", + " \"min\": 1800,\n", + " \"max\": 2200\n", + " },\n", + " \"protein\": {\n", + " \"min\": 91,\n", + " \"max\": float('inf')\n", + " },\n", + " \"fat\": {\n", + " \"min\": 0,\n", + " \"max\": 65\n", + " },\n", + " \"sodium\": {\n", + " \"min\": 0,\n", + " \"max\": 1779\n", + " }\n", + "}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Food costs per serving\n", + "food_costs = {\n", + " \"hamburger\": 2.49,\n", + " \"chicken\": 2.89,\n", + " \"hot dog\": 1.50,\n", + " \"fries\": 1.89,\n", + " \"macaroni\": 2.09,\n", + " \"pizza\": 1.99,\n", + " \"salad\": 2.49,\n", + " \"milk\": 0.89,\n", + " \"ice cream\": 1.59\n", + "}\n", + "\n", + "# Nutrition values for each food (per serving)\n", + "nutrition_data = {\n", + " \"hamburger\": [410, 24, 26, 730],\n", + " \"chicken\": [420, 32, 10, 1190],\n", + " \"hot dog\": [560, 20, 32, 1800],\n", + " \"fries\": [380, 4, 19, 270],\n", + " \"macaroni\": [320, 12, 10, 930],\n", + " \"pizza\": [320, 15, 12, 820],\n", + " \"salad\": [320, 31, 12, 1230],\n", + " \"milk\": [100, 8, 2.5, 125],\n", + " \"ice cream\": [330, 8, 10, 180]\n", + "}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a DataFrame for better visualization\n", + "nutrition_df = pd.DataFrame(nutrition_data, index=categories.keys()).T\n", + "nutrition_df.columns = [f\"{cat} (per serving)\" for cat in categories.keys()]\n", + "print(\"Nutritional Values per Serving:\")\n", + "print(nutrition_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Formulation\n", + "\n", + "Now we'll create the optimization problem using the cuOpt Python API as LP. The problem has:\n", + "- **Variables**: Amount of each food to buy (continuous, non-negative)\n", + "- **Objective**: Minimize total cost\n", + "- **Constraints**: Meet nutritional requirements (minimum and maximum bounds)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the optimization problem\n", + "problem = Problem(\"diet_optimization\")\n", + "\n", + "# Add decision variables for each food (amount to buy)\n", + "buy_vars = {}\n", + "for food_name in food_costs:\n", + " var = problem.addVariable(name=f\"{food_name}\", vtype=VType.CONTINUOUS, lb=0.0, ub=float('inf'))\n", + " buy_vars[food_name] = var\n", + "\n", + "print(f\"Created {len(buy_vars)} decision variables for foods\")\n", + "print(f\"Variables: {[var.getVariableName() for var in buy_vars.values()]}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set objective function: minimize total cost\n", + "objective_expr = LinearExpression([], [], 0.0)\n", + "\n", + "for var in buy_vars.values():\n", + " if food_costs[var.getVariableName()] != 0: # Only include non-zero coefficients\n", + " objective_expr += var * food_costs[var.getVariableName()]\n", + "\n", + "# Set objective function: minimize total cost\n", + "problem.setObjective(objective_expr, sense.MINIMIZE)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add nutrition constraints\n", + "constraint_names = []\n", + "\n", + "for i, category in enumerate(categories):\n", + " # Calculate total nutrition from all foods for this category\n", + " nutrition_expr = LinearExpression([], [], 0.0)\n", + " \n", + " for food_name in food_costs: \n", + " nutrition_value = nutrition_data[food_name][i]\n", + " if nutrition_value != 0: # Only include non-zero coefficients\n", + " nutrition_expr += buy_vars[food_name] * nutrition_value\n", + " \n", + " # Add constraint: min_nutrition[i] <= nutrition_expr <= max_nutrition[i]\n", + " min_val = categories[category][\"min\"]\n", + " max_val = categories[category][\"max\"]\n", + " \n", + " if max_val == float('inf'):\n", + " # Only lower bound constraint\n", + " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", + " constraint_names.append(f\"min_{category}\")\n", + " else:\n", + " # Range constraint (both lower and upper bounds)\n", + " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", + " constraint_names.append(f\"min_{category}\")\n", + " constraint = problem.addConstraint(nutrition_expr <= max_val, name=f\"max_{category}\")\n", + " constraint_names.append(f\"max_{category}\")\n", + "\n", + "print(f\"Added {len(constraint_names)} nutrition constraints\")\n", + "print(f\"Constraints: {constraint_names}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solver Configuration and Solution\n", + "\n", + "Configure the solver settings and solve the optimization problem.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configure solver settings\n", + "settings = SolverSettings()\n", + "settings.set_parameter(\"time_limit\", 60.0) # 60 second time limit\n", + "settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", + "settings.set_parameter(\"method\", 0) # Use default method\n", + "\n", + "print(\"Solver configured with 60-second time limit\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the problem\n", + "print(\"Solving diet optimization problem...\")\n", + "print(f\"Problem type: {'MIP' if problem.IsMIP else 'LP'}\")\n", + "\n", + "start_time = time.time()\n", + "problem.solve(settings)\n", + "solve_time = time.time() - start_time\n", + "\n", + "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", + "print(f\"Solver status: {problem.Status.name}\")\n", + "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def print_solution():\n", + " \"\"\"Print the optimal solution in a readable format\"\"\"\n", + " if problem.Status.name == \"Optimal\":\n", + " print(f\"\\nOptimal Solution Found!\")\n", + " print(f\"Total Cost: ${problem.ObjValue:.2f}\")\n", + " print(\"\\nFood Purchases:\")\n", + " \n", + " total_cost = 0\n", + " for var in buy_vars.values():\n", + " amount = var.getValue()\n", + " if amount > 0.0001: # Only show foods with significant amounts\n", + " food_cost = amount * food_costs[var.getVariableName()]\n", + " total_cost += food_cost\n", + " print(f\" {var.getVariableName()}: {amount:.3f} servings (${food_cost:.2f})\")\n", + " \n", + " print(f\"\\nTotal Cost: ${total_cost:.2f}\")\n", + " \n", + " # Check nutritional intake\n", + " print(\"\\nNutritional Intake:\")\n", + " for i, category in enumerate(categories):\n", + " total_nutrition = 0\n", + " for var in buy_vars.values():\n", + " amount = var.getValue()\n", + " nutrition_value = nutrition_data[var.getVariableName()][i]\n", + " total_nutrition += amount * nutrition_value\n", + " \n", + " min_req = categories[category][\"min\"]\n", + " max_req = categories[category][\"max\"]\n", + " \n", + " # Check constraints with tolerance for floating point precision\n", + " tolerance = 1e-6\n", + " min_satisfied = total_nutrition >= (min_req - tolerance)\n", + " max_satisfied = (max_req == float('inf')) or (total_nutrition <= (max_req + tolerance))\n", + " status = \"✓\" if (min_satisfied and max_satisfied) else \"✗\"\n", + " \n", + " if max_req == float('inf'):\n", + " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}) {status}\")\n", + " else:\n", + " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}, max: {max_req}) {status}\")\n", + " else:\n", + " print(f\"No optimal solution found. Status: {problem.Status.name}\")\n", + "\n", + "print_solution()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Additional Constraints\n", + "\n", + "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit dairy servings to at most 6.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create LinearExpression for dairy constraint\n", + "dairy_expr = buy_vars[\"milk\"] + buy_vars[\"ice cream\"]\n", + "dairy_constraint = problem.addConstraint(dairy_expr <= 6, name=\"limit_dairy\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the problem again with the new constraint\n", + "print(\"\\nSolving with dairy constraint...\")\n", + "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", + "\n", + "start_time = time.time()\n", + "problem.solve(settings)\n", + "solve_time = time.time() - start_time\n", + "\n", + "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", + "print(f\"Solver status: {problem.Status.name}\")\n", + "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solution Comparison\n", + "\n", + "Let's compare the solutions before and after adding the dairy constraint to see the impact.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display the new solution\n", + "print_solution()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This notebook demonstrated how to:\n", + "\n", + "1. **Formulate a diet optimization problem** using the cuOpt Python API\n", + "2. **Set up decision variables** for food quantities\n", + "3. **Define an objective function** to minimize total cost\n", + "4. **Add nutritional constraints** with both lower and upper bounds\n", + "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", + "6. **Add additional constraints** to the existing model\n", + "7. **Analyze and compare solutions** before and after constraint modifications\n", + "\n", + "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like diet optimization.\n", + "\n", + "### Key Benefits of cuOpt:\n", + "- **High Performance**: GPU-accelerated solving for large-scale problems\n", + "- **Easy to Use**: Intuitive Python API similar to other optimization libraries\n", + "- **Flexible**: Support for both LP and MIP problems\n", + "- **Scalable**: Handles problems with thousands of variables and constraints efficiently\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", + "SPDX-License-Identifier: MIT\n", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", + "\n", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cuopt", + "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.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index 8574efb..1905fbe 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -1,458 +1,504 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Diet Optimization with cuOpt Python API\n", - "\n", - "This notebook demonstrates how to solve the classic diet optimization problem using the cuOpt Python API. The problem involves selecting foods to meet nutritional requirements while minimizing cost.\n", - "\n", - "## Problem Description\n", - "\n", - "We need to select quantities of different foods to:\n", - "- Meet minimum and maximum nutritional requirements\n", - "- Minimize total cost\n", - "- Satisfy additional constraints (like limiting dairy servings)\n", - "\n", - "The nutrition guidelines are based on USDA Dietary Guidelines for Americans, 2005.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Environment Setup\n", - "\n", - "First, let's check if we have a GPU available and install necessary dependencies.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check for GPU availability\n", - "!nvidia-smi\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Install cuOpt if not already installed\n", - "# Uncomment the following line if running in Google Colab or similar environment\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12 # For cuda 12\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 # For cuda 13\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Import Required Libraries\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", - "from cuopt.linear_programming.solver_settings import SolverSettings\n", - "import time\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem Data Setup\n", - "\n", - "Define the nutrition guidelines, food costs, and nutritional values for each food item.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Nutrition guidelines based on USDA Dietary Guidelines for Americans, 2005\n", - "# http://www.health.gov/DietaryGuidelines/dga2005/\n", - "\n", - "# minimum and maximum values for each category\n", - "categories = {\n", - " \"calories\": {\n", - " \"min\": 1800,\n", - " \"max\": 2200\n", - " },\n", - " \"protein\": {\n", - " \"min\": 91,\n", - " \"max\": float('inf')\n", - " },\n", - " \"fat\": {\n", - " \"min\": 0,\n", - " \"max\": 65\n", - " },\n", - " \"sodium\": {\n", - " \"min\": 0,\n", - " \"max\": 1779\n", - " }\n", - "}\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Food costs per serving\n", - "food_costs = {\n", - " \"hamburger\": 2.49,\n", - " \"chicken\": 2.89,\n", - " \"hot dog\": 1.50,\n", - " \"fries\": 1.89,\n", - " \"macaroni\": 2.09,\n", - " \"pizza\": 1.99,\n", - " \"salad\": 2.49,\n", - " \"milk\": 0.89,\n", - " \"ice cream\": 1.59\n", - "}\n", - "\n", - "# Nutrition values for each food (per serving)\n", - "nutrition_data = {\n", - " \"hamburger\": [410, 24, 26, 730],\n", - " \"chicken\": [420, 32, 10, 1190],\n", - " \"hot dog\": [560, 20, 32, 1800],\n", - " \"fries\": [380, 4, 19, 270],\n", - " \"macaroni\": [320, 12, 10, 930],\n", - " \"pizza\": [320, 15, 12, 820],\n", - " \"salad\": [320, 31, 12, 1230],\n", - " \"milk\": [100, 8, 2.5, 125],\n", - " \"ice cream\": [330, 8, 10, 180]\n", - "}\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a DataFrame for better visualization\n", - "nutrition_df = pd.DataFrame(nutrition_data, index=categories.keys()).T\n", - "nutrition_df.columns = [f\"{cat} (per serving)\" for cat in categories.keys()]\n", - "print(\"Nutritional Values per Serving:\")\n", - "print(nutrition_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem Formulation\n", - "\n", - "Now we'll create the optimization problem using the cuOpt Python API as MILP. The problem has:\n", - "- **Variables**: Amount of each food to buy (continuous, non-negative)\n", - "- **Objective**: Minimize total cost\n", - "- **Constraints**: Meet nutritional requirements (minimum and maximum bounds)\n", - "\n", - "Since these are price per serving, you need to have a whole number for number of product that will be used.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the optimization problem\n", - "problem = Problem(\"diet_optimization\")\n", - "\n", - "# Add decision variables for each food (amount to buy)\n", - "buy_vars = {}\n", - "for food_name in food_costs:\n", - " # Using integer type for amount of food to buy since serving needs to be whole number\n", - " # And this converts the problem to MILP\n", - " var = problem.addVariable(name=f\"{food_name}\", vtype=VType.INTEGER, lb=0.0, ub=float('inf'))\n", - " buy_vars[food_name] = var\n", - "\n", - "print(f\"Created {len(buy_vars)} decision variables for foods\")\n", - "print(f\"Variables: {[var.getVariableName() for var in buy_vars.values()]}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "objective_expr = LinearExpression([], [], 0.0)\n", - "\n", - "for var in buy_vars.values():\n", - " if food_costs[var.getVariableName()] != 0: # Only include non-zero coefficients\n", - " objective_expr += var * food_costs[var.getVariableName()]\n", - "\n", - "# Set objective function: minimize total cost\n", - "problem.setObjective(objective_expr, sense.MINIMIZE)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Add nutrition constraints\n", - "constraint_names = []\n", - "\n", - "for i, category in enumerate(categories):\n", - " # Calculate total nutrition from all foods for this category\n", - " nutrition_expr = LinearExpression([], [], 0.0)\n", - " \n", - " for food_name in food_costs: \n", - " nutrition_value = nutrition_data[food_name][i]\n", - " if nutrition_value != 0: # Only include non-zero coefficients\n", - " nutrition_expr += buy_vars[food_name] * nutrition_value\n", - " \n", - " # Add constraint: min_nutrition[i] <= nutrition_expr <= max_nutrition[i]\n", - " min_val = categories[category][\"min\"]\n", - " max_val = categories[category][\"max\"]\n", - " \n", - " if max_val == float('inf'):\n", - " # Only lower bound constraint\n", - " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", - " constraint_names.append(f\"min_{category}\")\n", - " else:\n", - " # Range constraint (both lower and upper bounds)\n", - " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", - " constraint_names.append(f\"min_{category}\")\n", - " constraint = problem.addConstraint(nutrition_expr <= max_val, name=f\"max_{category}\")\n", - " constraint_names.append(f\"max_{category}\")\n", - "\n", - "print(f\"Added {len(constraint_names)} nutrition constraints\")\n", - "print(f\"Constraints: {constraint_names}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solver Configuration and Solution\n", - "\n", - "Configure the solver settings and solve the optimization problem.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Configure solver settings\n", - "settings = SolverSettings()\n", - "settings.set_parameter(\"time_limit\", 60.0) # 60 second time limit\n", - "settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", - "settings.set_parameter(\"method\", 0) # Use default method\n", - "\n", - "print(\"Solver configured with 60-second time limit\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Solve the problem\n", - "print(\"Solving diet optimization problem...\")\n", - "print(f\"Problem type: {'MIP' if problem.IsMIP else 'LP'}\")\n", - "\n", - "start_time = time.time()\n", - "problem.solve(settings)\n", - "solve_time = time.time() - start_time\n", - "\n", - "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def print_solution():\n", - " \"\"\"Print the optimal solution in a readable format\"\"\"\n", - " if problem.Status.name == \"Optimal\" or problem.Status.name == \"FeasibleFound\":\n", - " print(f\"\\nOptimal Solution Found!\")\n", - " print(f\"Total Cost: ${problem.ObjValue:.2f}\")\n", - " print(\"\\nFood Purchases:\")\n", - " \n", - " total_cost = 0\n", - " for var in buy_vars.values():\n", - " amount = var.getValue()\n", - " if amount > 0.0001: # Only show foods with significant amounts\n", - " food_cost = amount * food_costs[var.getVariableName()]\n", - " total_cost += food_cost\n", - " print(f\" {var.getVariableName()}: {amount:.3f} servings (${food_cost:.2f})\")\n", - " \n", - " print(f\"\\nTotal Cost: ${total_cost:.2f}\")\n", - " \n", - " # Check nutritional intake\n", - " print(\"\\nNutritional Intake:\")\n", - " for i, category in enumerate(categories):\n", - " total_nutrition = 0\n", - " for var in buy_vars.values():\n", - " amount = var.getValue()\n", - " nutrition_value = nutrition_data[var.getVariableName()][i]\n", - " total_nutrition += amount * nutrition_value\n", - " \n", - " min_req = categories[category][\"min\"]\n", - " max_req = categories[category][\"max\"]\n", - " \n", - " # Check constraints with tolerance for floating point precision\n", - " tolerance = 1e-6\n", - " min_satisfied = total_nutrition >= (min_req - tolerance)\n", - " max_satisfied = (max_req == float('inf')) or (total_nutrition <= (max_req + tolerance))\n", - " status = \"✓\" if (min_satisfied and max_satisfied) else \"✗\"\n", - " \n", - " if max_req == float('inf'):\n", - " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}) {status}\")\n", - " else:\n", - " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}, max: {max_req}) {status}\")\n", - " else:\n", - " print(f\"No optimal solution found. Status: {problem.Status.name}\")\n", - "\n", - "print_solution()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Additional Constraints\n", - "\n", - "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit dairy servings to at most 6.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create LinearExpression for dairy constraint\n", - "dairy_expr = buy_vars[\"milk\"] + buy_vars[\"ice cream\"]\n", - "\n", - "dairy_constraint = problem.addConstraint(dairy_expr <= 6, name=\"limit_dairy\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Solve the problem again with the new constraint\n", - "print(\"\\nSolving with dairy constraint...\")\n", - "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", - "\n", - "start_time = time.time()\n", - "problem.solve(settings)\n", - "solve_time = time.time() - start_time\n", - "\n", - "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solution Comparison\n", - "\n", - "Let's compare the solutions before and after adding the dairy constraint to see the impact.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Display the new solution\n", - "print_solution()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "This notebook demonstrated how to:\n", - "\n", - "1. **Formulate a diet optimization problem** using the cuOpt Python API\n", - "2. **Set up decision variables** for food quantities\n", - "3. **Define an objective function** to minimize total cost\n", - "4. **Add nutritional constraints** with both lower and upper bounds\n", - "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", - "6. **Add additional constraints** to the existing model\n", - "7. **Analyze and compare solutions** before and after constraint modifications\n", - "\n", - "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like diet optimization.\n", - "\n", - "### Key Benefits of cuOpt:\n", - "- **High Performance**: GPU-accelerated solving for large-scale problems\n", - "- **Easy to Use**: Intuitive Python API similar to other optimization libraries\n", - "- **Flexible**: Support for both LP and MIP problems\n", - "- **Scalable**: Handles problems with thousands of variables and constraints efficiently\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", - "SPDX-License-Identifier: MIT\n", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", - "\n", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cuopt", - "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.12.11" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Diet Optimization with cuOpt Python API\n", + "\n", + "This notebook demonstrates how to solve the classic diet optimization problem using the cuOpt Python API. The problem involves selecting foods to meet nutritional requirements while minimizing cost.\n", + "\n", + "## Problem Description\n", + "\n", + "We need to select quantities of different foods to:\n", + "- Meet minimum and maximum nutritional requirements\n", + "- Minimize total cost\n", + "- Satisfy additional constraints (like limiting dairy servings)\n", + "\n", + "The nutrition guidelines are based on USDA Dietary Guidelines for Americans, 2005.\n" + ] }, - "nbformat": 4, - "nbformat_minor": 2 + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Environment Setup\n", + "\n", + "First, let's check if we have a GPU available and install necessary dependencies.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import Required Libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", + "from cuopt.linear_programming.solver_settings import SolverSettings\n", + "import time\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Data Setup\n", + "\n", + "Define the nutrition guidelines, food costs, and nutritional values for each food item.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Nutrition guidelines based on USDA Dietary Guidelines for Americans, 2005\n", + "# http://www.health.gov/DietaryGuidelines/dga2005/\n", + "\n", + "# minimum and maximum values for each category\n", + "categories = {\n", + " \"calories\": {\n", + " \"min\": 1800,\n", + " \"max\": 2200\n", + " },\n", + " \"protein\": {\n", + " \"min\": 91,\n", + " \"max\": float('inf')\n", + " },\n", + " \"fat\": {\n", + " \"min\": 0,\n", + " \"max\": 65\n", + " },\n", + " \"sodium\": {\n", + " \"min\": 0,\n", + " \"max\": 1779\n", + " }\n", + "}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Food costs per serving\n", + "food_costs = {\n", + " \"hamburger\": 2.49,\n", + " \"chicken\": 2.89,\n", + " \"hot dog\": 1.50,\n", + " \"fries\": 1.89,\n", + " \"macaroni\": 2.09,\n", + " \"pizza\": 1.99,\n", + " \"salad\": 2.49,\n", + " \"milk\": 0.89,\n", + " \"ice cream\": 1.59\n", + "}\n", + "\n", + "# Nutrition values for each food (per serving)\n", + "nutrition_data = {\n", + " \"hamburger\": [410, 24, 26, 730],\n", + " \"chicken\": [420, 32, 10, 1190],\n", + " \"hot dog\": [560, 20, 32, 1800],\n", + " \"fries\": [380, 4, 19, 270],\n", + " \"macaroni\": [320, 12, 10, 930],\n", + " \"pizza\": [320, 15, 12, 820],\n", + " \"salad\": [320, 31, 12, 1230],\n", + " \"milk\": [100, 8, 2.5, 125],\n", + " \"ice cream\": [330, 8, 10, 180]\n", + "}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a DataFrame for better visualization\n", + "nutrition_df = pd.DataFrame(nutrition_data, index=categories.keys()).T\n", + "nutrition_df.columns = [f\"{cat} (per serving)\" for cat in categories.keys()]\n", + "print(\"Nutritional Values per Serving:\")\n", + "print(nutrition_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Formulation\n", + "\n", + "Now we'll create the optimization problem using the cuOpt Python API as MILP. The problem has:\n", + "- **Variables**: Amount of each food to buy (continuous, non-negative)\n", + "- **Objective**: Minimize total cost\n", + "- **Constraints**: Meet nutritional requirements (minimum and maximum bounds)\n", + "\n", + "Since these are price per serving, you need to have a whole number for number of product that will be used.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the optimization problem\n", + "problem = Problem(\"diet_optimization\")\n", + "\n", + "# Add decision variables for each food (amount to buy)\n", + "buy_vars = {}\n", + "for food_name in food_costs:\n", + " # Using integer type for amount of food to buy since serving needs to be whole number\n", + " # And this converts the problem to MILP\n", + " var = problem.addVariable(name=f\"{food_name}\", vtype=VType.INTEGER, lb=0.0, ub=float('inf'))\n", + " buy_vars[food_name] = var\n", + "\n", + "print(f\"Created {len(buy_vars)} decision variables for foods\")\n", + "print(f\"Variables: {[var.getVariableName() for var in buy_vars.values()]}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "objective_expr = LinearExpression([], [], 0.0)\n", + "\n", + "for var in buy_vars.values():\n", + " if food_costs[var.getVariableName()] != 0: # Only include non-zero coefficients\n", + " objective_expr += var * food_costs[var.getVariableName()]\n", + "\n", + "# Set objective function: minimize total cost\n", + "problem.setObjective(objective_expr, sense.MINIMIZE)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add nutrition constraints\n", + "constraint_names = []\n", + "\n", + "for i, category in enumerate(categories):\n", + " # Calculate total nutrition from all foods for this category\n", + " nutrition_expr = LinearExpression([], [], 0.0)\n", + " \n", + " for food_name in food_costs: \n", + " nutrition_value = nutrition_data[food_name][i]\n", + " if nutrition_value != 0: # Only include non-zero coefficients\n", + " nutrition_expr += buy_vars[food_name] * nutrition_value\n", + " \n", + " # Add constraint: min_nutrition[i] <= nutrition_expr <= max_nutrition[i]\n", + " min_val = categories[category][\"min\"]\n", + " max_val = categories[category][\"max\"]\n", + " \n", + " if max_val == float('inf'):\n", + " # Only lower bound constraint\n", + " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", + " constraint_names.append(f\"min_{category}\")\n", + " else:\n", + " # Range constraint (both lower and upper bounds)\n", + " constraint = problem.addConstraint(nutrition_expr >= min_val, name=f\"min_{category}\")\n", + " constraint_names.append(f\"min_{category}\")\n", + " constraint = problem.addConstraint(nutrition_expr <= max_val, name=f\"max_{category}\")\n", + " constraint_names.append(f\"max_{category}\")\n", + "\n", + "print(f\"Added {len(constraint_names)} nutrition constraints\")\n", + "print(f\"Constraints: {constraint_names}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solver Configuration and Solution\n", + "\n", + "Configure the solver settings and solve the optimization problem.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configure solver settings\n", + "settings = SolverSettings()\n", + "settings.set_parameter(\"time_limit\", 60.0) # 60 second time limit\n", + "settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", + "settings.set_parameter(\"method\", 0) # Use default method\n", + "\n", + "print(\"Solver configured with 60-second time limit\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the problem\n", + "print(\"Solving diet optimization problem...\")\n", + "print(f\"Problem type: {'MIP' if problem.IsMIP else 'LP'}\")\n", + "\n", + "start_time = time.time()\n", + "problem.solve(settings)\n", + "solve_time = time.time() - start_time\n", + "\n", + "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", + "print(f\"Solver status: {problem.Status.name}\")\n", + "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def print_solution():\n", + " \"\"\"Print the optimal solution in a readable format\"\"\"\n", + " if problem.Status.name == \"Optimal\" or problem.Status.name == \"FeasibleFound\":\n", + " print(f\"\\nOptimal Solution Found!\")\n", + " print(f\"Total Cost: ${problem.ObjValue:.2f}\")\n", + " print(\"\\nFood Purchases:\")\n", + " \n", + " total_cost = 0\n", + " for var in buy_vars.values():\n", + " amount = var.getValue()\n", + " if amount > 0.0001: # Only show foods with significant amounts\n", + " food_cost = amount * food_costs[var.getVariableName()]\n", + " total_cost += food_cost\n", + " print(f\" {var.getVariableName()}: {amount:.3f} servings (${food_cost:.2f})\")\n", + " \n", + " print(f\"\\nTotal Cost: ${total_cost:.2f}\")\n", + " \n", + " # Check nutritional intake\n", + " print(\"\\nNutritional Intake:\")\n", + " for i, category in enumerate(categories):\n", + " total_nutrition = 0\n", + " for var in buy_vars.values():\n", + " amount = var.getValue()\n", + " nutrition_value = nutrition_data[var.getVariableName()][i]\n", + " total_nutrition += amount * nutrition_value\n", + " \n", + " min_req = categories[category][\"min\"]\n", + " max_req = categories[category][\"max\"]\n", + " \n", + " # Check constraints with tolerance for floating point precision\n", + " tolerance = 1e-6\n", + " min_satisfied = total_nutrition >= (min_req - tolerance)\n", + " max_satisfied = (max_req == float('inf')) or (total_nutrition <= (max_req + tolerance))\n", + " status = \"✓\" if (min_satisfied and max_satisfied) else \"✗\"\n", + " \n", + " if max_req == float('inf'):\n", + " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}) {status}\")\n", + " else:\n", + " print(f\" {category}: {total_nutrition:.1f} (min: {min_req}, max: {max_req}) {status}\")\n", + " else:\n", + " print(f\"No optimal solution found. Status: {problem.Status.name}\")\n", + "\n", + "print_solution()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Additional Constraints\n", + "\n", + "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit dairy servings to at most 6.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create LinearExpression for dairy constraint\n", + "dairy_expr = buy_vars[\"milk\"] + buy_vars[\"ice cream\"]\n", + "\n", + "dairy_constraint = problem.addConstraint(dairy_expr <= 6, name=\"limit_dairy\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the problem again with the new constraint\n", + "print(\"\\nSolving with dairy constraint...\")\n", + "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", + "\n", + "start_time = time.time()\n", + "problem.solve(settings)\n", + "solve_time = time.time() - start_time\n", + "\n", + "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", + "print(f\"Solver status: {problem.Status.name}\")\n", + "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solution Comparison\n", + "\n", + "Let's compare the solutions before and after adding the dairy constraint to see the impact.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display the new solution\n", + "print_solution()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This notebook demonstrated how to:\n", + "\n", + "1. **Formulate a diet optimization problem** using the cuOpt Python API\n", + "2. **Set up decision variables** for food quantities\n", + "3. **Define an objective function** to minimize total cost\n", + "4. **Add nutritional constraints** with both lower and upper bounds\n", + "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", + "6. **Add additional constraints** to the existing model\n", + "7. **Analyze and compare solutions** before and after constraint modifications\n", + "\n", + "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like diet optimization.\n", + "\n", + "### Key Benefits of cuOpt:\n", + "- **High Performance**: GPU-accelerated solving for large-scale problems\n", + "- **Easy to Use**: Intuitive Python API similar to other optimization libraries\n", + "- **Flexible**: Support for both LP and MIP problems\n", + "- **Scalable**: Handles problems with thousands of variables and constraints efficiently\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", + "SPDX-License-Identifier: MIT\n", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", + "\n", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cuopt", + "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.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb index 04cad60..523c536 100644 --- a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb +++ b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb @@ -24,6 +24,63 @@ "- **Waypoint Graph :** Choose a waypoint graph when your data is portrayed as a network where a subset of nodes represent target locations and others are only being traversed (not acted upon by vehicles or agents, and lack associated orders or jobs). This approach is common in custom environments and indoor locations like warehouses and factories where the cost between target locations is dynamic or not easily calculated." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c77efe4", + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -32,7 +89,7 @@ "outputs": [], "source": [ "# Install notebook dependencies\n", - "!pip install --user -q scipy matplotlib pandas requests polyline folium" + "!pip install -q scipy matplotlib pandas requests polyline folium" ] }, { diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index 5a76813..89c699e 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -52,8 +52,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -66,17 +112,15 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "c5735277", "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { diff --git a/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index f225178..acf3141 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -64,8 +64,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -78,17 +124,15 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "01d94bc0", "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { @@ -99,7 +143,7 @@ "outputs": [], "source": [ "# Install notebook dependencies\n", - "!pip install --user -q matplotlib" + "!pip install -q matplotlib" ] }, { diff --git a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index d40ef4a..848c480 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -39,8 +39,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -53,17 +99,15 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "e0ce3f89", "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { @@ -74,7 +118,7 @@ "outputs": [], "source": [ "# Install notebook dependencyn\n", - "!pip install --user -q matplotlib scipy " + "!pip install -q matplotlib scipy " ] }, { diff --git a/last_mile_delivery/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index 6af2a4a..ef0b628 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -68,8 +68,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -87,12 +133,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { @@ -102,7 +146,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install --user -q matplotlib scipy " + "!pip install -q matplotlib scipy " ] }, { diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/01_optimization_with_cufolio.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/01_optimization_with_cufolio.ipynb index 2b9fc4c..648c774 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/01_optimization_with_cufolio.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/01_optimization_with_cufolio.ipynb @@ -31,19 +31,71 @@ { "cell_type": "code", "execution_count": null, - "id": "3d0f49d0-640d-45bd-8682-b073fc45071d", + "id": "d4b0528a", "metadata": {}, "outputs": [], "source": [ - "# Install dependencies\n", - "# This cell only needs to be run once, typically after setting up the environment.\n", - "# If dependencies are already installed, you can comment out or skip this cell.\n", - "\n", - "# Install cuOpt (if not already installed)\n", - "#!pip install --upgrade --user --extra-index-url https://pypi.nvidia.com -q cuopt-cu12 \n", + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", "\n", - "# Install other dependencies (if not already installed)\n", - "!pip install --user --pre --extra-index-url https://pypi.nvidia.com -q \"numpy>=1.24.4\" \"pandas>=2.2.1\" \"cvxpy>=1.6.5\" \"scipy==1.15.2\" \"scikit-learn==1.6.1\" \"msgpack>=1.1.0\" \"cuml-cu12\" \"seaborn>=0.13.2\" bin/cufolio-25.8-py3-none-any.whl" + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d0f49d0-640d-45bd-8682-b073fc45071d", + "metadata": {}, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index 65ff234..c67d637 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -29,6 +29,63 @@ "Before diving into portfolio optimization and backtesting, we need to import the necessary libraries." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebb4627c", + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -36,15 +93,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install dependencies\n", - "# This cell only needs to be run once, typically after setting up the environment.\n", - "# If dependencies are already installed, you can comment out or skip this cell.\n", - "\n", - "# Install cuOpt (if not already installed)\n", - "#!pip install --upgrade --user --extra-index-url https://pypi.nvidia.com -q cuopt-cu12 \n", - "\n", - "# Install other dependencies (if not already installed)\n", - "!pip install --user --pre --extra-index-url https://pypi.nvidia.com -q \"numpy>=1.24.4\" \"pandas>=2.2.1\" \"cvxpy>=1.6.5\" \"scipy==1.15.2\" \"scikit-learn==1.6.1\" \"msgpack>=1.1.0\" \"cuml-cu12\" \"seaborn>=0.13.2\" bin/cufolio-25.8-py3-none-any.whl" + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb index 544f565..a069016 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb @@ -33,6 +33,63 @@ "Before diving into portfolio optimization, we need to import the necessary libraries and perform initial setup if required." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bdde410", + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -40,15 +97,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install dependencies\n", - "# This cell only needs to be run once, typically after setting up the environment.\n", - "# If dependencies are already installed, you can comment out or skip this cell.\n", - "\n", - "# Install cuOpt (if not already installed)\n", - "#!pip install --upgrade --user --extra-index-url https://pypi.nvidia.com -q cuopt-cu12 \n", - "\n", - "# Install other dependencies (if not already installed)\n", - "!pip install --pre --user --extra-index-url https://pypi.nvidia.com -q \"numpy>=1.24.4\" \"pandas>=2.2.1\" \"cvxpy>=1.6.5\" \"scipy==1.15.2\" \"scikit-learn==1.6.1\" \"msgpack>=1.1.0\" \"cuml-cu12\" \"seaborn>=0.13.2\" bin/cufolio-25.8-py3-none-any.whl\n" + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, { diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index cf5c407..bda7d89 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -1,1094 +1,1133 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# CVaR Portfolio Optimization with cuOpt Python API\n", - "\n", - "This notebook demonstrates Conditional Value at Risk (CVaR) portfolio optimization using NVIDIA's cuOpt Python API with S&P 500 stock data.\n", - "\n", - "## Overview\n", - "\n", - "**Conditional Value at Risk (CVaR)** is a risk measure that quantifies the expected loss in the worst-case scenarios beyond a certain confidence level. It's particularly useful for portfolio optimization as it provides a coherent risk measure that captures tail risk.\n", - "\n", - "### CVaR Formulation\n", - "\n", - "The CVaR portfolio optimization problem can be formulated as:\n", - "\n", - "$$\n", - "\\begin{align}\n", - "\\text{maximize: } & \\mu^T w - \\lambda \\text{CVaR}_\\alpha(w) \\\\\n", - "\\text{subject to: } & \\mathbf{1}^T w = 1 \\\\\n", - "& w_i^{\\min} \\leq w_i \\leq w_i^{\\max}, \\quad i = 1, \\ldots, n\n", - "\\end{align}\n", - "$$\n", - "\n", - "Where:\n", - "- $w$ is the portfolio weight vector\n", - "- $\\mu$ is the expected return vector\n", - "- $\\lambda$ is the risk aversion parameter\n", - "- $\\text{CVaR}_\\alpha(w)$ is the Conditional Value at Risk at confidence level $\\alpha$\n", - "\n", - "### Data Source\n", - "We use S&P 500 stock data from `./cuFOLIO_portfolio_optimization/data/stock_data/sp500.csv` which contains historical price data for S&P 500 constituents.\n", - "\n", - "### Requirements\n", - "- **GPU**: NVIDIA GPU with CUDA support (recommended for optimal performance)\n", - "- **CUDA**: Version 12.x or 13.x\n", - "- **Python**: 3.10 or higher\n", - "- **Memory**: Sufficient RAM for large-scale optimization (8GB+ recommended)\n", - "\n", - "### Installation Notes\n", - "- cuOpt requires an NVIDIA GPU and CUDA toolkit\n", - "- The package is available through NVIDIA's PyPI index\n", - "- Different versions are available for different CUDA versions (cu11, cu12)\n", - "- For CPU-only environments, consider using alternative optimization libraries\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Environment Setup and Installation\n", - "\n", - "### 1.1 Install Required Dependencies\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mon Oct 6 12:49:10 2025 \n", - "+-----------------------------------------------------------------------------------------+\n", - "| NVIDIA-SMI 580.82.07 Driver Version: 580.82.07 CUDA Version: 13.0 |\n", - "+-----------------------------------------+------------------------+----------------------+\n", - "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n", - "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n", - "| | | MIG M. |\n", - "|=========================================+========================+======================|\n", - "| 0 Quadro P620 On | 00000000:42:00.0 Off | N/A |\n", - "| 34% 41C P8 N/A / N/A | 10MiB / 2048MiB | 0% Default |\n", - "| | | N/A |\n", - "+-----------------------------------------+------------------------+----------------------+\n", - "| 1 Quadro RTX 8000 On | 00000000:61:00.0 On | Off |\n", - "| 33% 41C P2 67W / 260W | 2366MiB / 49152MiB | 7% Default |\n", - "| | | N/A |\n", - "+-----------------------------------------+------------------------+----------------------+\n", - "\n", - "+-----------------------------------------------------------------------------------------+\n", - "| Processes: |\n", - "| GPU GI CI PID Type Process name GPU Memory |\n", - "| ID ID Usage |\n", - "|=========================================================================================|\n", - "| 0 N/A N/A 4408 G /usr/lib/xorg/Xorg 4MiB |\n", - "| 1 N/A N/A 4408 G /usr/lib/xorg/Xorg 527MiB |\n", - "| 1 N/A N/A 4664 G /usr/bin/gnome-shell 293MiB |\n", - "| 1 N/A N/A 7558 G ...ersion=20250926-130007.640000 227MiB |\n", - "| 1 N/A N/A 771862 G ...slack/215/usr/lib/slack/slack 127MiB |\n", - "| 1 N/A N/A 1836477 G ...ess --variations-seed-version 408MiB |\n", - "| 1 N/A N/A 1981088 C ...iforge3/envs/cuopt/bin/python 662MiB |\n", - "+-----------------------------------------------------------------------------------------+\n" - ] - } - ], - "source": [ - "# Check GPU availability\n", - "!nvidia-smi\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: numpy in /home/luffy/.local/lib/python3.12/site-packages (2.0.2)\n", - "Requirement already satisfied: pandas in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (2.3.3)\n", - "Requirement already satisfied: matplotlib in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (3.10.6)\n", - "Requirement already satisfied: seaborn in /home/luffy/.local/lib/python3.12/site-packages (0.13.2)\n", - "Requirement already satisfied: scipy in /home/luffy/.local/lib/python3.12/site-packages (1.15.2)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /home/luffy/.local/lib/python3.12/site-packages (from pandas) (2.9.0.post0)\n", - "Requirement already satisfied: pytz>=2020.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from pandas) (2025.2)\n", - "Requirement already satisfied: tzdata>=2022.7 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from pandas) (2025.2)\n", - "Requirement already satisfied: contourpy>=1.0.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (1.3.3)\n", - "Requirement already satisfied: cycler>=0.10 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (0.12.1)\n", - "Requirement already satisfied: fonttools>=4.22.0 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (4.60.1)\n", - "Requirement already satisfied: kiwisolver>=1.3.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (1.4.9)\n", - "Requirement already satisfied: packaging>=20.0 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (25.0)\n", - "Requirement already satisfied: pillow>=8 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (11.3.0)\n", - "Requirement already satisfied: pyparsing>=2.3.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (3.2.5)\n", - "Requirement already satisfied: six>=1.5 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n" - ] - } - ], - "source": [ - "# Install cuOpt and other required packages\n", - "# Uncomment the following lines if running in a new environment\n", - "\n", - "# For CUDA 12.x systems:\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12\n", - "\n", - "# For CUDA 13.x systems:\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13\n", - "\n", - "# Install other dependencies\n", - "!pip install numpy pandas matplotlib seaborn scipy" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1.2 Import Required Libraries\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# Import required libraries\n", - "import numpy as np\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "from scipy import stats\n", - "import warnings\n", - "warnings.filterwarnings('ignore')\n", - "\n", - "# cuOpt imports\n", - "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", - "from cuopt.linear_programming.solver_settings import SolverSettings, PDLPSolverMode\n", - "from cuopt.linear_programming.solver.solver_parameters import *\n", - "\n", - "# Set random seed for reproducibility\n", - "np.random.seed(42)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1.3 Configure Solver Settings\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Configure solver settings for larger problem\n", - "solver_settings = SolverSettings()\n", - "solver_settings.set_parameter(\"time_limit\", 300.0) # 5 minute time limit for larger problem\n", - "solver_settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", - "solver_settings.set_parameter(\"method\", 0) # Use default method\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1.4 Load S&P 500 Data\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Date range: 2005-01-03 00:00:00 to 2024-04-30 00:00:00\n", - "Number of assets: 397\n", - "\n", - "First few columns: ['A', 'AAPL', 'ABT', 'ACGL', 'ACN', 'ADBE', 'ADI', 'ADM', 'ADP', 'ADSK']\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", - " \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", - " \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", - "
AAAPLABTACGLACNADBEADIADMADPADSK...WRBWSTWTWWYWYNNXELXOMYUMZBHZBRA
Date
2005-01-0314.4649840.95680914.2261194.23333318.85339730.83894922.99989513.99072222.00261737.410706...6.72845010.49192270.31845112.67667735.7139828.78788826.21030611.74792969.54950055.509998
2005-01-0414.0833690.96663614.0828494.17777818.41012030.02411122.37418213.83781521.65137534.981960...6.70974310.57486370.81138612.48179935.6377148.65621626.03239411.59236569.52318654.470001
2005-01-0514.0773080.97510113.9212894.15333318.33863329.85914222.47531113.60209421.56106435.251820...6.69390910.57486369.68957512.53477636.0408908.55868125.89634311.56475868.97995852.570000
2005-01-0613.7683860.97585714.2352634.14777818.17419129.36423922.43739713.88241521.41554535.081905...6.73276710.56242269.40062712.58585237.5010388.54405226.22601111.69523469.77728352.650002
2005-01-0713.7562691.04691114.4791194.19111119.02498429.38423322.46899613.91427121.37541434.282318...6.69102910.53338968.51675412.78263436.2533688.49528226.05332811.63000369.65460253.099998
\n", - "

5 rows × 397 columns

\n", - "
" - ], - "text/plain": [ - " A AAPL ABT ACGL ACN ADBE \\\n", - "Date \n", - "2005-01-03 14.464984 0.956809 14.226119 4.233333 18.853397 30.838949 \n", - "2005-01-04 14.083369 0.966636 14.082849 4.177778 18.410120 30.024111 \n", - "2005-01-05 14.077308 0.975101 13.921289 4.153333 18.338633 29.859142 \n", - "2005-01-06 13.768386 0.975857 14.235263 4.147778 18.174191 29.364239 \n", - "2005-01-07 13.756269 1.046911 14.479119 4.191111 19.024984 29.384233 \n", - "\n", - " ADI ADM ADP ADSK ... WRB \\\n", - "Date ... \n", - "2005-01-03 22.999895 13.990722 22.002617 37.410706 ... 6.728450 \n", - "2005-01-04 22.374182 13.837815 21.651375 34.981960 ... 6.709743 \n", - "2005-01-05 22.475311 13.602094 21.561064 35.251820 ... 6.693909 \n", - "2005-01-06 22.437397 13.882415 21.415545 35.081905 ... 6.732767 \n", - "2005-01-07 22.468996 13.914271 21.375414 34.282318 ... 6.691029 \n", - "\n", - " WST WTW WY WYNN XEL XOM \\\n", - "Date \n", - "2005-01-03 10.491922 70.318451 12.676677 35.713982 8.787888 26.210306 \n", - "2005-01-04 10.574863 70.811386 12.481799 35.637714 8.656216 26.032394 \n", - "2005-01-05 10.574863 69.689575 12.534776 36.040890 8.558681 25.896343 \n", - "2005-01-06 10.562422 69.400627 12.585852 37.501038 8.544052 26.226011 \n", - "2005-01-07 10.533389 68.516754 12.782634 36.253368 8.495282 26.053328 \n", - "\n", - " YUM ZBH ZBRA \n", - "Date \n", - "2005-01-03 11.747929 69.549500 55.509998 \n", - "2005-01-04 11.592365 69.523186 54.470001 \n", - "2005-01-05 11.564758 68.979958 52.570000 \n", - "2005-01-06 11.695234 69.777283 52.650002 \n", - "2005-01-07 11.630003 69.654602 53.099998 \n", - "\n", - "[5 rows x 397 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Load S&P 500 data\n", - "data_path = './cuFOLIO_portfolio_optimization/data/stock_data/sp500.csv'\n", - "df = pd.read_csv(data_path, index_col='Date', parse_dates=True)\n", - "\n", - "print(f\"Date range: {df.index.min()} to {df.index.max()}\")\n", - "print(f\"Number of assets: {len(df.columns)}\")\n", - "print(f\"\\nFirst few columns: {list(df.columns[:10])}\")\n", - "\n", - "# Display basic statistics\n", - "df.head()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Data Preprocessing and Return Calculation\n" - ] - }, + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CVaR Portfolio Optimization with cuOpt Python API\n", + "\n", + "This notebook demonstrates Conditional Value at Risk (CVaR) portfolio optimization using NVIDIA's cuOpt Python API with S&P 500 stock data.\n", + "\n", + "## Overview\n", + "\n", + "**Conditional Value at Risk (CVaR)** is a risk measure that quantifies the expected loss in the worst-case scenarios beyond a certain confidence level. It's particularly useful for portfolio optimization as it provides a coherent risk measure that captures tail risk.\n", + "\n", + "### CVaR Formulation\n", + "\n", + "The CVaR portfolio optimization problem can be formulated as:\n", + "\n", + "$$\n", + "\\begin{align}\n", + "\\text{maximize: } & \\mu^T w - \\lambda \\text{CVaR}_\\alpha(w) \\\\\n", + "\\text{subject to: } & \\mathbf{1}^T w = 1 \\\\\n", + "& w_i^{\\min} \\leq w_i \\leq w_i^{\\max}, \\quad i = 1, \\ldots, n\n", + "\\end{align}\n", + "$$\n", + "\n", + "Where:\n", + "- $w$ is the portfolio weight vector\n", + "- $\\mu$ is the expected return vector\n", + "- $\\lambda$ is the risk aversion parameter\n", + "- $\\text{CVaR}_\\alpha(w)$ is the Conditional Value at Risk at confidence level $\\alpha$\n", + "\n", + "### Data Source\n", + "We use S&P 500 stock data from `./cuFOLIO_portfolio_optimization/data/stock_data/sp500.csv` which contains historical price data for S&P 500 constituents.\n", + "\n", + "### Requirements\n", + "- **GPU**: NVIDIA GPU with CUDA support (recommended for optimal performance)\n", + "- **CUDA**: Version 12.x or 13.x\n", + "- **Python**: 3.10 or higher\n", + "- **Memory**: Sufficient RAM for large-scale optimization (8GB+ recommended)\n", + "\n", + "### Installation Notes\n", + "- cuOpt requires an NVIDIA GPU and CUDA toolkit\n", + "- The package is available through NVIDIA's PyPI index\n", + "- Different versions are available for different CUDA versions (cu11, cu12)\n", + "- For CPU-only environments, consider using alternative optimization libraries\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Environment Setup and Installation\n", + "\n", + "### 1.1 Install Required Dependencies\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total assets in dataset: 397\n", - "Assets with complete data: 397\n", - "Price data shape: (4864, 397)\n", - "Selected assets (first 10): ['A', 'AAPL', 'ABT', 'ACGL', 'ACN', 'ADBE', 'ADI', 'ADM', 'ADP', 'ADSK']\n", - "Returns data shape: (4863, 397)\n", - "Returns date range: 2005-01-04 00:00:00 to 2024-04-30 00:00:00\n", - "\n", - "Return Statistics (first 5 assets):\n", - " A AAPL ABT ACGL ACN\n", - "count 4863.000000 4863.000000 4863.000000 4863.000000 4863.000000\n", - "mean 0.000462 0.001066 0.000413 0.000637 0.000570\n", - "std 0.019274 0.020429 0.013795 0.015633 0.016319\n", - "min -0.116690 -0.197470 -0.102982 -0.184827 -0.144498\n", - "25% -0.008411 -0.008457 -0.006327 -0.006095 -0.007144\n", - "50% 0.000895 0.000990 0.000430 0.000918 0.000954\n", - "75% 0.010221 0.011700 0.007655 0.007772 0.008602\n", - "max 0.138395 0.130194 0.103783 0.142868 0.151577\n" - ] - } - ], - "source": [ - "# Use all S&P 500 assets with complete data\n", - "# Remove any assets with missing data\n", - "price_data = df.dropna(axis=1, how='any') # Drop columns with any NaN values\n", - "selected_assets = price_data.columns\n", - "\n", - "print(f\"Total assets in dataset: {len(df.columns)}\")\n", - "print(f\"Assets with complete data: {len(selected_assets)}\")\n", - "print(f\"Price data shape: {price_data.shape}\")\n", - "print(f\"Selected assets (first 10): {list(selected_assets[:10])}\")\n", - "\n", - "# Calculate log returns\n", - "returns = np.log(price_data / price_data.shift(1)).dropna()\n", - "\n", - "print(f\"Returns data shape: {returns.shape}\")\n", - "print(f\"Returns date range: {returns.index.min()} to {returns.index.max()}\")\n", - "\n", - "# Display return statistics\n", - "print(\"\\nReturn Statistics (first 5 assets):\")\n", - "print(returns.iloc[:, :5].describe())\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Mon Oct 6 12:49:10 2025 \n", + "+-----------------------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 580.82.07 Driver Version: 580.82.07 CUDA Version: 13.0 |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|=========================================+========================+======================|\n", + "| 0 Quadro P620 On | 00000000:42:00.0 Off | N/A |\n", + "| 34% 41C P8 N/A / N/A | 10MiB / 2048MiB | 0% Default |\n", + "| | | N/A |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + "| 1 Quadro RTX 8000 On | 00000000:61:00.0 On | Off |\n", + "| 33% 41C P2 67W / 260W | 2366MiB / 49152MiB | 7% Default |\n", + "| | | N/A |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + "\n", + "+-----------------------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=========================================================================================|\n", + "| 0 N/A N/A 4408 G /usr/lib/xorg/Xorg 4MiB |\n", + "| 1 N/A N/A 4408 G /usr/lib/xorg/Xorg 527MiB |\n", + "| 1 N/A N/A 4664 G /usr/bin/gnome-shell 293MiB |\n", + "| 1 N/A N/A 7558 G ...ersion=20250926-130007.640000 227MiB |\n", + "| 1 N/A N/A 771862 G ...slack/215/usr/lib/slack/slack 127MiB |\n", + "| 1 N/A N/A 1836477 G ...ess --variations-seed-version 408MiB |\n", + "| 1 N/A N/A 1981088 C ...iforge3/envs/cuopt/bin/python 662MiB |\n", + "+-----------------------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Annualized expected returns (top 5):\n", - "A: 0.1165\n", - "AAPL: 0.2685\n", - "ABT: 0.1041\n", - "ACGL: 0.1604\n", - "ACN: 0.1435\n" - ] - } - ], - "source": [ - "# Calculate expected returns and covariance matrix\n", - "mu = returns.mean().values # Expected returns\n", - "Sigma = returns.cov().values # Covariance matrix\n", - "n_assets = len(selected_assets)\n", - "\n", - "# Annualize returns (assuming 252 trading days)\n", - "mu_annual = mu * 252\n", - "Sigma_annual = Sigma * 252\n", - "\n", - "print(f\"\\nAnnualized expected returns (top 5):\")\n", - "for i in range(5):\n", - " print(f\"{selected_assets[i]}: {mu_annual[i]:.4f}\")\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: numpy in /home/luffy/.local/lib/python3.12/site-packages (2.0.2)\n", + "Requirement already satisfied: pandas in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (2.3.3)\n", + "Requirement already satisfied: matplotlib in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (3.10.6)\n", + "Requirement already satisfied: seaborn in /home/luffy/.local/lib/python3.12/site-packages (0.13.2)\n", + "Requirement already satisfied: scipy in /home/luffy/.local/lib/python3.12/site-packages (1.15.2)\n", + "Requirement already satisfied: python-dateutil>=2.8.2 in /home/luffy/.local/lib/python3.12/site-packages (from pandas) (2.9.0.post0)\n", + "Requirement already satisfied: pytz>=2020.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from pandas) (2025.2)\n", + "Requirement already satisfied: tzdata>=2022.7 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from pandas) (2025.2)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (1.3.3)\n", + "Requirement already satisfied: cycler>=0.10 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (4.60.1)\n", + "Requirement already satisfied: kiwisolver>=1.3.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (1.4.9)\n", + "Requirement already satisfied: packaging>=20.0 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (25.0)\n", + "Requirement already satisfied: pillow>=8 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (11.3.0)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from matplotlib) (3.2.5)\n", + "Requirement already satisfied: six>=1.5 in /home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n" + ] + } + ], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.2 Import Required Libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Import required libraries\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "from scipy import stats\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# cuOpt imports\n", + "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", + "from cuopt.linear_programming.solver_settings import SolverSettings, PDLPSolverMode\n", + "from cuopt.linear_programming.solver.solver_parameters import *\n", + "\n", + "# Set random seed for reproducibility\n", + "np.random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.3 Configure Solver Settings\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Configure solver settings for larger problem\n", + "solver_settings = SolverSettings()\n", + "solver_settings.set_parameter(\"time_limit\", 300.0) # 5 minute time limit for larger problem\n", + "solver_settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", + "solver_settings.set_parameter(\"method\", 0) # Use default method\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.4 Load S&P 500 Data\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. CVaR Scenario Generation\n", - "\n", - "For CVaR optimization, we need to generate scenarios of portfolio returns. We'll use historical simulation and Monte Carlo methods.\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Date range: 2005-01-03 00:00:00 to 2024-04-30 00:00:00\n", + "Number of assets: 397\n", + "\n", + "First few columns: ['A', 'AAPL', 'ABT', 'ACGL', 'ACN', 'ADBE', 'ADI', 'ADM', 'ADP', 'ADSK']\n" + ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Historical scenarios: 4863\n", - "Number of assets: 397\n", - "Monte Carlo scenarios: 2000\n", - "Total scenarios: 6863\n", - "Scenario matrix shape: (6863, 397)\n", - "Problem size: 397 assets × 6863 scenarios = 2724611 scenario-asset combinations\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", + " \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", + " \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", + "
AAAPLABTACGLACNADBEADIADMADPADSK...WRBWSTWTWWYWYNNXELXOMYUMZBHZBRA
Date
2005-01-0314.4649840.95680914.2261194.23333318.85339730.83894922.99989513.99072222.00261737.410706...6.72845010.49192270.31845112.67667735.7139828.78788826.21030611.74792969.54950055.509998
2005-01-0414.0833690.96663614.0828494.17777818.41012030.02411122.37418213.83781521.65137534.981960...6.70974310.57486370.81138612.48179935.6377148.65621626.03239411.59236569.52318654.470001
2005-01-0514.0773080.97510113.9212894.15333318.33863329.85914222.47531113.60209421.56106435.251820...6.69390910.57486369.68957512.53477636.0408908.55868125.89634311.56475868.97995852.570000
2005-01-0613.7683860.97585714.2352634.14777818.17419129.36423922.43739713.88241521.41554535.081905...6.73276710.56242269.40062712.58585237.5010388.54405226.22601111.69523469.77728352.650002
2005-01-0713.7562691.04691114.4791194.19111119.02498429.38423322.46899613.91427121.37541434.282318...6.69102910.53338968.51675412.78263436.2533688.49528226.05332811.63000369.65460253.099998
\n", + "

5 rows × 397 columns

\n", + "
" ], - "source": [ - "# Historical simulation scenarios\n", - "historical_scenarios = returns.values\n", - "n_scenarios_hist = historical_scenarios.shape[0]\n", - "\n", - "print(f\"Historical scenarios: {n_scenarios_hist}\")\n", - "print(f\"Number of assets: {len(selected_assets)}\")\n", - "\n", - "# For computational efficiency with many assets, use fewer Monte Carlo scenarios\n", - "# Adjust based on problem size\n", - "n_scenarios_mc = min(2000, n_scenarios_hist) # Use at most 2000 MC scenarios\n", - "mc_scenarios = np.random.multivariate_normal(mu, Sigma, n_scenarios_mc)\n", - "\n", - "print(f\"Monte Carlo scenarios: {n_scenarios_mc}\")\n", - "\n", - "# Combine scenarios\n", - "all_scenarios = np.vstack([historical_scenarios, mc_scenarios])\n", - "n_scenarios_total = all_scenarios.shape[0]\n", - "scenario_probs = np.ones(n_scenarios_total) / n_scenarios_total\n", - "\n", - "print(f\"Total scenarios: {n_scenarios_total}\")\n", - "print(f\"Scenario matrix shape: {all_scenarios.shape}\")\n", - "print(f\"Problem size: {len(selected_assets)} assets × {n_scenarios_total} scenarios = {len(selected_assets) * n_scenarios_total} scenario-asset combinations\")\n" + "text/plain": [ + " A AAPL ABT ACGL ACN ADBE \\\n", + "Date \n", + "2005-01-03 14.464984 0.956809 14.226119 4.233333 18.853397 30.838949 \n", + "2005-01-04 14.083369 0.966636 14.082849 4.177778 18.410120 30.024111 \n", + "2005-01-05 14.077308 0.975101 13.921289 4.153333 18.338633 29.859142 \n", + "2005-01-06 13.768386 0.975857 14.235263 4.147778 18.174191 29.364239 \n", + "2005-01-07 13.756269 1.046911 14.479119 4.191111 19.024984 29.384233 \n", + "\n", + " ADI ADM ADP ADSK ... WRB \\\n", + "Date ... \n", + "2005-01-03 22.999895 13.990722 22.002617 37.410706 ... 6.728450 \n", + "2005-01-04 22.374182 13.837815 21.651375 34.981960 ... 6.709743 \n", + "2005-01-05 22.475311 13.602094 21.561064 35.251820 ... 6.693909 \n", + "2005-01-06 22.437397 13.882415 21.415545 35.081905 ... 6.732767 \n", + "2005-01-07 22.468996 13.914271 21.375414 34.282318 ... 6.691029 \n", + "\n", + " WST WTW WY WYNN XEL XOM \\\n", + "Date \n", + "2005-01-03 10.491922 70.318451 12.676677 35.713982 8.787888 26.210306 \n", + "2005-01-04 10.574863 70.811386 12.481799 35.637714 8.656216 26.032394 \n", + "2005-01-05 10.574863 69.689575 12.534776 36.040890 8.558681 25.896343 \n", + "2005-01-06 10.562422 69.400627 12.585852 37.501038 8.544052 26.226011 \n", + "2005-01-07 10.533389 68.516754 12.782634 36.253368 8.495282 26.053328 \n", + "\n", + " YUM ZBH ZBRA \n", + "Date \n", + "2005-01-03 11.747929 69.549500 55.509998 \n", + "2005-01-04 11.592365 69.523186 54.470001 \n", + "2005-01-05 11.564758 68.979958 52.570000 \n", + "2005-01-06 11.695234 69.777283 52.650002 \n", + "2005-01-07 11.630003 69.654602 53.099998 \n", + "\n", + "[5 rows x 397 columns]" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. CVaR Portfolio Optimization with cuOpt\n", - "\n", - "Now we'll implement the CVaR optimization using cuOpt's linear programming interface. The CVaR optimization problem can be reformulated as a linear program.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "def solve_cvar_portfolio(scenarios, scenario_probs, mu, alpha=0.95, lambda_risk=1.0, \n", - " w_min=None, w_max=None, solver_settings=None):\n", - " \"\"\"\n", - " Solve CVaR portfolio optimization using cuOpt linear programming.\n", - " \n", - " Parameters:\n", - " - scenarios: numpy array of return scenarios (n_scenarios x n_assets)\n", - " - scenario_probs: probability weights for scenarios\n", - " - mu: expected returns vector\n", - " - alpha: confidence level for CVaR (default 0.95)\n", - " - lambda_risk: risk aversion parameter (default 1.0)\n", - " - w_min, w_max: bounds on portfolio weights\n", - " - solver_settings: cuOpt solver settings\n", - " \n", - " Returns:\n", - " - optimal_weights: optimal portfolio weights\n", - " - cvar_value: CVaR value at optimum\n", - " - expected_return: expected portfolio return\n", - " \"\"\"\n", - " \n", - " n_scenarios, n_assets = scenarios.shape\n", - " \n", - " if w_min is None:\n", - " w_min = np.zeros(n_assets)\n", - " if w_max is None:\n", - " w_max = np.ones(n_assets)\n", - " \n", - " # Create the linear programming problem\n", - " problem = Problem(\"cvar_portfolio_optimization\")\n", - " \n", - " # Decision variables\n", - " # Portfolio weights\n", - " w = {}\n", - " for i in range(n_assets):\n", - " w[i] = problem.addVariable(name=f\"w_{i}\", vtype=VType.CONTINUOUS, \n", - " lb=w_min[i], ub=w_max[i])\n", - " \n", - " # CVaR auxiliary variables\n", - " t = problem.addVariable(name=\"t\", vtype=VType.CONTINUOUS, \n", - " lb=-float('inf'), ub=float('inf')) # VaR variable\n", - " u = {}\n", - " for s in range(n_scenarios):\n", - " u[s] = problem.addVariable(name=f\"u_{s}\", vtype=VType.CONTINUOUS, \n", - " lb=0.0, ub=float('inf')) # CVaR auxiliary\n", - " \n", - " # Objective: maximize expected return - lambda * CVaR\n", - " # CVaR = t + (1/(1-alpha)) * sum(p_s * u_s)\n", - " objective_expr = LinearExpression([], [], 0.0)\n", - " \n", - " # Add expected return terms\n", - " for i in range(n_assets):\n", - " if mu[i] != 0:\n", - " objective_expr += w[i] * mu[i]\n", - " \n", - " # Subtract CVaR terms to penalize higher risk (lower CVaR increases objective value)\n", - " if lambda_risk != 0:\n", - " objective_expr -= t * lambda_risk\n", - " cvar_coeff = lambda_risk / (1.0 - alpha)\n", - " for s in range(n_scenarios):\n", - " if scenario_probs[s] != 0:\n", - " objective_expr -= u[s] * (cvar_coeff * scenario_probs[s])\n", - " \n", - " problem.setObjective(objective_expr, sense.MAXIMIZE)\n", - " \n", - " # Constraints\n", - " # Budget constraint: sum of weights = 1\n", - " budget_expr = LinearExpression([], [], 0.0)\n", - " for i in range(n_assets):\n", - " budget_expr += w[i]\n", - " problem.addConstraint(budget_expr == 1.0, name=\"budget\")\n", - " \n", - " # CVaR constraints: u_s >= -R_s^T * w - t for all scenarios s\n", - " for s in range(n_scenarios):\n", - " cvar_constraint_expr = LinearExpression([], [], 0.0)\n", - " cvar_constraint_expr += u[s] # u_s\n", - " cvar_constraint_expr += t # + t\n", - " \n", - " # Add portfolio return terms: + R_s^T * w\n", - " for i in range(n_assets):\n", - " if scenarios[s, i] != 0:\n", - " cvar_constraint_expr += w[i] * scenarios[s, i]\n", - " \n", - " problem.addConstraint(cvar_constraint_expr >= 0.0, name=f\"cvar_{s}\")\n", - " \n", - " # Solve the optimization problem\n", - " if solver_settings is not None:\n", - " problem.solve(solver_settings)\n", - " else:\n", - " problem.solve()\n", - " \n", - " if problem.Status.name == \"Optimal\":\n", - " # Extract optimal solution\n", - " optimal_weights = np.array([w[i].getValue() for i in range(n_assets)])\n", - " t_value = t.getValue()\n", - " u_values = np.array([u[s].getValue() for s in range(n_scenarios)])\n", - " \n", - " # Calculate CVaR and expected return\n", - " cvar_value = t_value + (1.0 / (1.0 - alpha)) * np.sum(scenario_probs * u_values)\n", - " expected_return = np.dot(mu, optimal_weights)\n", - " \n", - " return optimal_weights, cvar_value, expected_return, problem\n", - " else:\n", - " raise RuntimeError(f\"Optimization failed with status: {problem.Status.name}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. Solve the CVaR Optimization Problem\n" - ] - }, + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load S&P 500 data\n", + "data_path = './cuFOLIO_portfolio_optimization/data/stock_data/sp500.csv'\n", + "df = pd.read_csv(data_path, index_col='Date', parse_dates=True)\n", + "\n", + "print(f\"Date range: {df.index.min()} to {df.index.max()}\")\n", + "print(f\"Number of assets: {len(df.columns)}\")\n", + "print(f\"\\nFirst few columns: {list(df.columns[:10])}\")\n", + "\n", + "# Display basic statistics\n", + "df.head()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Data Preprocessing and Return Calculation\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Diversification constraints:\n", - "- Maximum weight per asset: 100.0%\n", - "- This forces allocation across at least 1 assets\n", - "- Confidence level (alpha): 0.95\n", - "- Risk aversion (lambda): 2.0\n", - "- Number of scenarios: 6863\n", - "- Number of assets: 397\n", - "Setting parameter time_limit to 3.000000e+02\n", - "Setting parameter log_to_console to true\n", - "Setting parameter method to 0\n", - "cuOpt version: 25.10.0, git hash: f4082fe3, host arch: x86_64, device archs: 75\n", - "CPU: AMD Ryzen Threadripper PRO 3975WX 32-Cores, threads (physical/logical): 32/64, RAM: 1.65 GiB\n", - "CUDA 13.0, device: Quadro RTX 8000 (ID 0), VRAM: 47.25 GiB\n", - "CUDA device UUID: ffffffb7fffffff2ffffffb679-057e-ffff\n", - "\n", - "Third-party presolve is disabled, skipping\n", - "Solving a problem with 6864 constraints 7261 variables (0 integers) and 2725089 nonzeros\n", - "Objective offset -0.000000 scaling_factor -1.000000\n", - "Running concurrent\n", - "\n", - " Iter Primal Obj. Dual Obj. Gap Primal Res. Dual Res. Time\n", - " 0 -0.00000000e+00 -0.00000000e+00 0.00e+00 1.00e+00 3.08e+00 0.129s\n", - " 1000 +2.01815200e-01 +2.00428379e-01 1.39e-03 1.76e-03 5.72e-03 0.379s\n", - "Handling free variables 1\n", - "Dual simplex finished in 0.46 seconds, total time 0.55\n", - "FAILED: CUDSS call ended unsuccessfully with status = 5, details: \"cudssExecute for reordering\"\n", - "PDLP finished\n", - "Barrier finished in 0.59 seconds\n", - "Barrier Solve status A numerical error was encountered.\n", - "Concurrent time: 0.548s, total time 0.595s\n", - "Solved with dual simplex\n", - "Status: Optimal Objective: 2.01903713e-01 Iterations: 1032 Time: 0.595s\n", - "\n", - "Optimization successfuli!\n", - "Status: Optimal\n", - "Objective value: 0.201904\n", - "Expected annual return: 0.2920 (29.20%)\n", - "CVaR (95%): 0.0450\n" - ] - } - ], - "source": [ - "# Set optimization parameters\n", - "alpha = 0.95 # 95% confidence level\n", - "lambda_risk = 2.0 # Risk aversion parameter\n", - "\n", - "# Portfolio weight bounds for DIVERSIFIED portfolio\n", - "w_min = np.zeros(n_assets) # No short selling\n", - "w_max = np.ones(n_assets) # Maximum can be 100% in any single asset\n", - "\n", - "print(f\"Diversification constraints:\")\n", - "print(f\"- Maximum weight per asset: {w_max[0]:.1%}\")\n", - "print(f\"- This forces allocation across at least {1/w_max[0]:.0f} assets\")\n", - "\n", - "# Alternative diversification strategies (uncomment to try):\n", - "\n", - "# Strategy 1: Even more diversified (max 10% per asset)\n", - "# w_max = np.ones(n_assets) * 0.10\n", - "\n", - "# Strategy 2: Minimum holdings requirement (forces broader diversification)\n", - "# min_holdings = 30 # Require at least 30 assets\n", - "# w_min = np.zeros(n_assets)\n", - "# w_min[:min_holdings] = 0.005 # Minimum 0.5% in top assets\n", - "\n", - "# Strategy 3: Lower risk aversion (allows more return-seeking behavior)\n", - "# lambda_risk = 0.5 # Less conservative approach\n", - "\n", - "print(f\"- Confidence level (alpha): {alpha}\")\n", - "print(f\"- Risk aversion (lambda): {lambda_risk}\")\n", - "print(f\"- Number of scenarios: {n_scenarios_total}\")\n", - "print(f\"- Number of assets: {n_assets}\")\n", - "\n", - "# Solve the optimization problem\n", - "try:\n", - " optimal_weights, cvar_value, expected_return, solve_result = solve_cvar_portfolio(\n", - " scenarios=all_scenarios,\n", - " scenario_probs=scenario_probs,\n", - " mu=mu_annual, # Use annualized returns\n", - " alpha=alpha,\n", - " lambda_risk=lambda_risk,\n", - " w_min=w_min,\n", - " w_max=w_max,\n", - " solver_settings=solver_settings\n", - " )\n", - " \n", - " print(f\"\\nOptimization successfuli!\")\n", - " print(f\"Status: {solve_result.Status.name}\")\n", - " print(f\"Objective value: {solve_result.ObjValue:.6f}\")\n", - " print(f\"Expected annual return: {expected_return:.4f} ({expected_return*100:.2f}%)\")\n", - " print(f\"CVaR (95%): {cvar_value:.4f}\")\n", - " \n", - "except Exception as e:\n", - " print(f\"Optimization failed: {e}\")\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Total assets in dataset: 397\n", + "Assets with complete data: 397\n", + "Price data shape: (4864, 397)\n", + "Selected assets (first 10): ['A', 'AAPL', 'ABT', 'ACGL', 'ACN', 'ADBE', 'ADI', 'ADM', 'ADP', 'ADSK']\n", + "Returns data shape: (4863, 397)\n", + "Returns date range: 2005-01-04 00:00:00 to 2024-04-30 00:00:00\n", + "\n", + "Return Statistics (first 5 assets):\n", + " A AAPL ABT ACGL ACN\n", + "count 4863.000000 4863.000000 4863.000000 4863.000000 4863.000000\n", + "mean 0.000462 0.001066 0.000413 0.000637 0.000570\n", + "std 0.019274 0.020429 0.013795 0.015633 0.016319\n", + "min -0.116690 -0.197470 -0.102982 -0.184827 -0.144498\n", + "25% -0.008411 -0.008457 -0.006327 -0.006095 -0.007144\n", + "50% 0.000895 0.000990 0.000430 0.000918 0.000954\n", + "75% 0.010221 0.011700 0.007655 0.007772 0.008602\n", + "max 0.138395 0.130194 0.103783 0.142868 0.151577\n" + ] + } + ], + "source": [ + "# Use all S&P 500 assets with complete data\n", + "# Remove any assets with missing data\n", + "price_data = df.dropna(axis=1, how='any') # Drop columns with any NaN values\n", + "selected_assets = price_data.columns\n", + "\n", + "print(f\"Total assets in dataset: {len(df.columns)}\")\n", + "print(f\"Assets with complete data: {len(selected_assets)}\")\n", + "print(f\"Price data shape: {price_data.shape}\")\n", + "print(f\"Selected assets (first 10): {list(selected_assets[:10])}\")\n", + "\n", + "# Calculate log returns\n", + "returns = np.log(price_data / price_data.shift(1)).dropna()\n", + "\n", + "print(f\"Returns data shape: {returns.shape}\")\n", + "print(f\"Returns date range: {returns.index.min()} to {returns.index.max()}\")\n", + "\n", + "# Display return statistics\n", + "print(\"\\nReturn Statistics (first 5 assets):\")\n", + "print(returns.iloc[:, :5].describe())\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 6. Analyze the Optimal Portfolio\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Annualized expected returns (top 5):\n", + "A: 0.1165\n", + "AAPL: 0.2685\n", + "ABT: 0.1041\n", + "ACGL: 0.1604\n", + "ACN: 0.1435\n" + ] + } + ], + "source": [ + "# Calculate expected returns and covariance matrix\n", + "mu = returns.mean().values # Expected returns\n", + "Sigma = returns.cov().values # Covariance matrix\n", + "n_assets = len(selected_assets)\n", + "\n", + "# Annualize returns (assuming 252 trading days)\n", + "mu_annual = mu * 252\n", + "Sigma_annual = Sigma * 252\n", + "\n", + "print(f\"\\nAnnualized expected returns (top 5):\")\n", + "for i in range(5):\n", + " print(f\"{selected_assets[i]}: {mu_annual[i]:.4f}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. CVaR Scenario Generation\n", + "\n", + "For CVaR optimization, we need to generate scenarios of portfolio returns. We'll use historical simulation and Monte Carlo methods.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimal Portfolio Composition (Top 20 Holdings):\n", - "======================================================================\n", - " NVDA: 0.3300 ( 33.00%) | Expected Return: 0.3199\n", - " AAPL: 0.3208 ( 32.08%) | Expected Return: 0.2685\n", - " NFLX: 0.2485 ( 24.85%) | Expected Return: 0.2995\n", - " MNST: 0.0689 ( 6.89%) | Expected Return: 0.2560\n", - " BKNG: 0.0320 ( 3.20%) | Expected Return: 0.2582\n" - ] - } - ], - "source": [ - "# Create portfolio results DataFrame\n", - "portfolio_df = pd.DataFrame({\n", - " 'Asset': selected_assets,\n", - " 'Weight': optimal_weights,\n", - " 'Expected_Return': mu_annual\n", - "})\n", - "\n", - "# Sort by weight (descending)\n", - "portfolio_df = portfolio_df.sort_values('Weight', ascending=False)\n", - "\n", - "# Display portfolio composition (top holdings only)\n", - "significant_holdings = portfolio_df[portfolio_df['Weight'] > 0.001] # Only assets with weight > 0.1%\n", - "top_holdings = significant_holdings.head(20) # Show top 20 holdings\n", - "\n", - "print(\"Optimal Portfolio Composition (Top 20 Holdings):\")\n", - "print(\"=\" * 70)\n", - "for _, row in top_holdings.iterrows():\n", - " print(f\"{row['Asset']:>6}: {row['Weight']:>8.4f} ({row['Weight']*100:>6.2f}%) | Expected Return: {row['Expected_Return']:>8.4f}\")" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Historical scenarios: 4863\n", + "Number of assets: 397\n", + "Monte Carlo scenarios: 2000\n", + "Total scenarios: 6863\n", + "Scenario matrix shape: (6863, 397)\n", + "Problem size: 397 assets × 6863 scenarios = 2724611 scenario-asset combinations\n" + ] + } + ], + "source": [ + "# Historical simulation scenarios\n", + "historical_scenarios = returns.values\n", + "n_scenarios_hist = historical_scenarios.shape[0]\n", + "\n", + "print(f\"Historical scenarios: {n_scenarios_hist}\")\n", + "print(f\"Number of assets: {len(selected_assets)}\")\n", + "\n", + "# For computational efficiency with many assets, use fewer Monte Carlo scenarios\n", + "# Adjust based on problem size\n", + "n_scenarios_mc = min(2000, n_scenarios_hist) # Use at most 2000 MC scenarios\n", + "mc_scenarios = np.random.multivariate_normal(mu, Sigma, n_scenarios_mc)\n", + "\n", + "print(f\"Monte Carlo scenarios: {n_scenarios_mc}\")\n", + "\n", + "# Combine scenarios\n", + "all_scenarios = np.vstack([historical_scenarios, mc_scenarios])\n", + "n_scenarios_total = all_scenarios.shape[0]\n", + "scenario_probs = np.ones(n_scenarios_total) / n_scenarios_total\n", + "\n", + "print(f\"Total scenarios: {n_scenarios_total}\")\n", + "print(f\"Scenario matrix shape: {all_scenarios.shape}\")\n", + "print(f\"Problem size: {len(selected_assets)} assets × {n_scenarios_total} scenarios = {len(selected_assets) * n_scenarios_total} scenario-asset combinations\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. CVaR Portfolio Optimization with cuOpt\n", + "\n", + "Now we'll implement the CVaR optimization using cuOpt's linear programming interface. The CVaR optimization problem can be reformulated as a linear program.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def solve_cvar_portfolio(scenarios, scenario_probs, mu, alpha=0.95, lambda_risk=1.0, \n", + " w_min=None, w_max=None, solver_settings=None):\n", + " \"\"\"\n", + " Solve CVaR portfolio optimization using cuOpt linear programming.\n", + " \n", + " Parameters:\n", + " - scenarios: numpy array of return scenarios (n_scenarios x n_assets)\n", + " - scenario_probs: probability weights for scenarios\n", + " - mu: expected returns vector\n", + " - alpha: confidence level for CVaR (default 0.95)\n", + " - lambda_risk: risk aversion parameter (default 1.0)\n", + " - w_min, w_max: bounds on portfolio weights\n", + " - solver_settings: cuOpt solver settings\n", + " \n", + " Returns:\n", + " - optimal_weights: optimal portfolio weights\n", + " - cvar_value: CVaR value at optimum\n", + " - expected_return: expected portfolio return\n", + " \"\"\"\n", + " \n", + " n_scenarios, n_assets = scenarios.shape\n", + " \n", + " if w_min is None:\n", + " w_min = np.zeros(n_assets)\n", + " if w_max is None:\n", + " w_max = np.ones(n_assets)\n", + " \n", + " # Create the linear programming problem\n", + " problem = Problem(\"cvar_portfolio_optimization\")\n", + " \n", + " # Decision variables\n", + " # Portfolio weights\n", + " w = {}\n", + " for i in range(n_assets):\n", + " w[i] = problem.addVariable(name=f\"w_{i}\", vtype=VType.CONTINUOUS, \n", + " lb=w_min[i], ub=w_max[i])\n", + " \n", + " # CVaR auxiliary variables\n", + " t = problem.addVariable(name=\"t\", vtype=VType.CONTINUOUS, \n", + " lb=-float('inf'), ub=float('inf')) # VaR variable\n", + " u = {}\n", + " for s in range(n_scenarios):\n", + " u[s] = problem.addVariable(name=f\"u_{s}\", vtype=VType.CONTINUOUS, \n", + " lb=0.0, ub=float('inf')) # CVaR auxiliary\n", + " \n", + " # Objective: maximize expected return - lambda * CVaR\n", + " # CVaR = t + (1/(1-alpha)) * sum(p_s * u_s)\n", + " objective_expr = LinearExpression([], [], 0.0)\n", + " \n", + " # Add expected return terms\n", + " for i in range(n_assets):\n", + " if mu[i] != 0:\n", + " objective_expr += w[i] * mu[i]\n", + " \n", + " # Subtract CVaR terms to penalize higher risk (lower CVaR increases objective value)\n", + " if lambda_risk != 0:\n", + " objective_expr -= t * lambda_risk\n", + " cvar_coeff = lambda_risk / (1.0 - alpha)\n", + " for s in range(n_scenarios):\n", + " if scenario_probs[s] != 0:\n", + " objective_expr -= u[s] * (cvar_coeff * scenario_probs[s])\n", + " \n", + " problem.setObjective(objective_expr, sense.MAXIMIZE)\n", + " \n", + " # Constraints\n", + " # Budget constraint: sum of weights = 1\n", + " budget_expr = LinearExpression([], [], 0.0)\n", + " for i in range(n_assets):\n", + " budget_expr += w[i]\n", + " problem.addConstraint(budget_expr == 1.0, name=\"budget\")\n", + " \n", + " # CVaR constraints: u_s >= -R_s^T * w - t for all scenarios s\n", + " for s in range(n_scenarios):\n", + " cvar_constraint_expr = LinearExpression([], [], 0.0)\n", + " cvar_constraint_expr += u[s] # u_s\n", + " cvar_constraint_expr += t # + t\n", + " \n", + " # Add portfolio return terms: + R_s^T * w\n", + " for i in range(n_assets):\n", + " if scenarios[s, i] != 0:\n", + " cvar_constraint_expr += w[i] * scenarios[s, i]\n", + " \n", + " problem.addConstraint(cvar_constraint_expr >= 0.0, name=f\"cvar_{s}\")\n", + " \n", + " # Solve the optimization problem\n", + " if solver_settings is not None:\n", + " problem.solve(solver_settings)\n", + " else:\n", + " problem.solve()\n", + " \n", + " if problem.Status.name == \"Optimal\":\n", + " # Extract optimal solution\n", + " optimal_weights = np.array([w[i].getValue() for i in range(n_assets)])\n", + " t_value = t.getValue()\n", + " u_values = np.array([u[s].getValue() for s in range(n_scenarios)])\n", + " \n", + " # Calculate CVaR and expected return\n", + " cvar_value = t_value + (1.0 / (1.0 - alpha)) * np.sum(scenario_probs * u_values)\n", + " expected_return = np.dot(mu, optimal_weights)\n", + " \n", + " return optimal_weights, cvar_value, expected_return, problem\n", + " else:\n", + " raise RuntimeError(f\"Optimization failed with status: {problem.Status.name}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Solve the CVaR Optimization Problem\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABgoAAAMWCAYAAAAge92DAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XVYFdn/B/D3pbskVQTBRFHsblaxc41dA9fuzrXXdtfVde1du1tXsTDWXrsLFAwkpTvu+f3hj/v1egEBgSHer+fhWTlz5sxn5p7Lzsxn5hyZEEKAiIiIiIiIiIiIiIiKJDWpAyAiIiIiIiIiIiIiIukwUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBE+dKWLVsgk8ng6+tbpLadHadOnYKLiwt0dHQgk8kQHh6e6XXnzJkDmUymVGZvbw93d/ecDTKX+fr6QiaTYcuWLdle99dff835wChfcnd3h729/VfrpdWv0vrOEBERERERERV0TBQQUaY8efIEvXv3RokSJaCtrY3ixYvjxx9/xJMnT76p3YULF+LIkSM5E2QeS71hmPqjp6cHJycnzJgxA5GRkTm2ndjYWMyZMwcXL15UWfbx40d0794durq6WL16NbZv3w59ff0c2/a3cnJyQtWqVVXKDx8+DJlMhiZNmqgs27RpE2QyGc6cOZMXIWaJh4cH5syZk+fb/bKvpffTtGnTXI/l0KFD6NGjBxwcHKCnp4fy5ctjwoQJ6Saojh07hurVq0NHRwelSpXC7NmzkZyc/NXtXLx4ETKZDAcOHEhzubu7OwwMDL5lV4iIiIiIiIjo/2lIHQAR5X+HDh1Cr169YGZmhgEDBqB06dLw9fXF33//jQMHDmDPnj3o3LlzttpeuHAhunXrhk6dOimV9+nTBz179oS2tnYO7EHuWrt2LQwMDBAdHY0zZ85gwYIFOH/+PK5evZojTx7HxsZi7ty5AKByI/jWrVuIiorCL7/8AldX12/eFgC8ePECamo5k0du2LAh/v77b0RERMDY2FhRfvXqVWhoaODWrVtISkqCpqam0jJ1dXXUq1cv09ux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rKysxLBhw0RYWJjKunZ2dkplHz9+FH369BFGRkbC2NhY9OnTR9y7d0+lX6X1nUnvu5vWd+ncuXOiWrVqQktLSzg6Ooq//vpLTJgwQejo6CjV6dixoyhevLjQ0tISxYsXF7169RIvX75M81gQERERERERfSuZENmYZY+IKANz5szB3LlzERwcnGOT/BLlFBcXF1hYWODs2bNShyIJfj/zn06dOuHJkycq80YQERERERER5RXOUUBERIVSUlISkpOTlcouXryIBw8eoGnTptIERUVeXFyc0u9eXl7w8PBgnyQiIiIiIiJJcY4CIiIqlPz8/ODq6orevXujePHieP78OdatWwdra2sMHTpU6vCoiHJwcIC7uzscHBzw5s0brF27FlpaWpg8ebLUoREREREREVERxkQBEREVSqampqhRowb++usvBAcHQ19fH23btsXixYtRrFgxqcOjIsrNzQ27d+9GQEAAtLW1Ua9ePSxcuBBly5aVOjQiIiIiIiIqwjhHARERERERERERERFREcY5CoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCojomyxduhQVKlSAXC6XOpR8z97eHu7u7lKHQWnIymdjb2+Pdu3a5W5AOczd3R329vaZqjtnzhzIZLLcDUgCWTkGaa1rYGCQswGloW7dupzUmIiICrxbt26hfv360NfXh0wmw/379zO97pYtWyCTyeDr66soa9q0KZo2bZrjcRbEeDLj4sWLkMlkuHjxotShUB5Iq4+m58trHvaV7PH19YVMJsOvv/4qdShEOY6JAiLKtsjISCxZsgRTpkyBmtr//pyMGzcO1atXh5mZGfT09FCxYkXMmTMH0dHRKm3cuXMHbm5uMDIygqGhIVq2bKlyMZH6P+L0fgYNGpRhnB8+fMCcOXOydJHyJQ8PD8yZMyfb6xd0OXEMMyOjz3rPnj25uu3PPX36FHPmzMnUCXdBFBsbizlz5vCiIId963GdMmUKVq9ejYCAgJwNjIiIioTUG4apPzo6OihXrhxGjhyJwMDAHN3WwoULceTIEZXypKQkfP/99wgNDcXvv/+O7du3w87OLke3nZNq164NmUyGtWvXSh1Klq1ZswZbtmyROowsS705nZmf3BYdHY3Zs2fDzc0NZmZmkMlkGR7TZ8+ewc3NDQYGBjAzM0OfPn0QHBycqW3JZDKM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GBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFlGO8vLxQv359lCtXDrVq1cKTJ09U6ly/fh0uLi5wcXFBpUqVMHToUCQkJKS5bMiQIYplAPD333+jbNmycHR0xKBBg5CUlJRn+0ZERERERERERERUWDFRQDlmyJAhGDx4MF6+fIkpU6bA3d1dpU7VqlVx69Yt3L9/H48ePUJQUBC2bNmS7rI1a9YAAHx8fDBz5kxcvnwZ3t7eCAwMxIYNG/Jw74iIiIiIiIiIiIgKJyYKKEcEBQXh9u3b6N27NwCga9euePfuHby9vZXq6enpQVNTEwCQmJiIuLg4yGSyry47cOAAOnToAGtra8hkMgwdOhS7d+/Oq90jIiIiIiIiIiIiKrSYKKAc8e7dO9jY2EBDQwMAIJPJUKpUKbx9+1alrq+vL6pWrQpzc3MYGxsrvXnw5bLhw4cDAN6+fQs7OztFPXt7+zTbJiIiIiIiIiIiIqKsYaKA8py9vT0ePHiAgIAAJCQkwMPDI91lhw4dkjBSIiIiIiIiIiIiosKPiQLKEba2tvD390dycjIAQAiBt2/folSpUumuY2BggB49eqSZDDAwMEDPnj2xc+dOAECpUqXw5s0bxXJfX98M2yYiIiIiIiIiIiKizGGigHKEpaUlqlevjh07dgAADh48iJIlS6JMmTJK9by9vZGUlATg0zwER44cQcWKFdNcdvjwYVSpUgXApzkPjh07hoCAAAghsG7dOvTs2TOvdo+IiIiIiIiIiIio0GKigHLM+vXrsX79epQrVw6LFy/G5s2bAQADBw7EsWPHAADnz59HtWrVULVqVVSrVg1WVlYYN25custmzpwJAHBwcMDcuXPRoEEDlClTBhYWFhgyZIg0O0pERERERERERERUiMiEEELqIPKb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iIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiChfadq0KWQyGTw9PZXKly1bBplMhrFjxwIAZDIZbG1tER8fr6hz5MgR2NvbK3739/fHDz/8AGtraxgaGsLBwQHjxo0DAFSqVAkGBgYwMDCApqYmtLS0FL9XqlQp1/eTKL9gooCIiIiIiIiIiIjynfLly2Pz5s1KZZs3b0aFChWUyuLi4rBq1ap02+nTpw90dHTw/PlzRERE4OzZs3BxcQEAPHnyBNHR0YiOjsaPP/6I4cOHK35/8uRJju8TUX7FRAERERERERERERHlOz179sTJkycREREBAPjvv/8AAHXq1FGqN336dCxatAjh4eFptnPjxg30798fJiYmUFNTg6OjI/r165ersRMVNEwUEBERERERERERUb5jYmICNzc37N69GwCwadMm9O/fX6Ve8+bNUatWLSxZsiTNdho0aICxY8di27ZtePnyZa7GTFRQMVFARERERERERERE+VL//v2xefNmxMXF4eDBg+jTp0+a9RYvXoxVq1bhw4cPKsv279+P9u3bY8WKFahUqRLs7Oywa9eu3A6dqEBhooCIiIiIiIiIiIjypRYtWsDf3x+//PIL6tWrB2tr6zTrVatWDR06dMDcuXNVlhkZGWHOnDm4e/cuwsLCMHr0aPTt2xfPnj3L7fCJCgwmCoiIiIiIiIiIiChfUlNTQ79+/bB48eI0hx363Pz587Fjx44MhxcyMDDAhAkTYGxsjKdPn+Z0uEQFlobUARARERERERERUeEXn5SCyLgkRMQlITL+038j4pIQGZf82b//Vx4Vn4ykFDlShIBcLv7/v4BcCKTIBeRCYLyeGeL9YyGTySCTATI12ad/qwHqmurQ0lGHpvanHy0dDWjqpJal8W9tdWjra0LPSAs6+ppSHy76zLhx49CkSRM0adIkw3oODg746aefsHTpUhgYGCjKJ02ahB9//BFOTk4AgG3btiEmJgY1atTI1biJChImCoiIiIiIiIiIKNtiEpLxNjQW70Jj8TY0Fu/D4vA+LA5hsYlKCYCEZHmObztJloy4qKQcb1dNQwY9Qy3oGX32Y6INAxNtGJjqwMBUG/om2kwo5BEzMzO4urpmqu7MmTOxdetWpbKEhAT07NkTfn5+0NTURMWKFXH06FHY29vnQrREBRMTBURERERE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j9iwYQMuXbqEhg0bIi4uTmndv//+G2ZmZvj7779zZ2e+0bRDDxEakyh1GESUw3SeXZM6hALtXveq+Ff3jdRhUDr2vdiH6x+uSx0G5aF8kShYvXo17O3toaOjgzp16uDmzZvp1t24cSMaNWoEU1NTmJqawtXVVaV+6pjTn/+4ubnl9m4QEVE+ExkZifbt22PSpElwcXEBAPz1118oVaoUmjdvrlI/IiICK1euxM2bN/HmzRv8/fff6Ny5MxITeWFPRESUVcOHD8fVq1dx6dIlpXItLS307t0bmzdvVirfvXs3SpYsicaNG6u0pa6ujpo1a+LgwYMICAhQWtfLywuXLl3Cpk2bcPfuXTx48CB3diib9t9+B89nHMaQqLAxMtGAehi/29kVV78KFtrekzoMyoCAwKxrsxCdqPomHxVOkicK9u7di/Hjx2P27Nm4e/cuqlatilatWqU7HvTFixfRq1cvXLhwAdevX4etrS1atmwJPz8/pXpubm7w9/dX/OzevTsvdoeIiHKRra0t/P39kZycDAAQQuDt27coVaqUSt2oqCi4ubmhY8eOGDdunKL8woULOHr0KOzt7WFvbw8AqFKlCu7du4ezZ8/CxMQEFStWBAC0b98ekZGRePOGT7kQERFllZmZGaZMmYKpU6eqLBswYADOnj2rdB23adMm/PTTTxm2aWJiAldXV/z7779K61WrVg0dO3ZEo0aN8tVbBX7hcZj3z1OpwyCiXGCql7PD5ejVrYtS27eh/J3bKH/nNkofOQy9evXSrqypCfPhw+F46hTKP7gPx7NnYDZggGKxTFMTNosXodytmyhz7hyM2rT53zJtbTiePoViQwbnaPxZIbO2xJSG6b8VTvlHQEwAltxaInUYlEckTxQsX74cgwYNQv/+/eHk5IR169ZBT08PmzZtSrP+zp07MXz4cLi4uKBChQr466+/IJfLce7cOaV62trasLa2VvyYmprmxe4QEVEusrS0RPXq1bFjxw4AwMGDB1GyZEmUKVNGqV50dDTc3Nzg5uaGGTNmKC3buXMn3r17B19fX/j6+gIAHj58qJjX4P79+wgICAAAXL9+HcnJybC1tc39nSMiIiqExo4dizdv3uDIkSNK5c7OzqhevbpiiL8nT57g3r176Nev31fbLFGiBEJDQwEAKSkp2Lp1q2K9vn37YufOnWnOf5DXhBCY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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Concentration Analysis:\n", - "Herfindahl-Hirschman Index (HHI): 0.279259\n", - "Effective number of assets: 3.58\n", - "Diversification ratio: 5/397 = 1.26%\n" - ] - } - ], - "source": [ - "# Visualize portfolio composition\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))\n", - "\n", - "# Portfolio weights bar chart (top 20 holdings)\n", - "top_20_holdings = significant_holdings.head(20)\n", - "bars = ax1.bar(range(len(top_20_holdings)), top_20_holdings['Weight'])\n", - "ax1.set_xlabel('Assets (Top 20 Holdings)')\n", - "ax1.set_ylabel('Portfolio Weight')\n", - "ax1.set_title(f'Optimal Portfolio Weights - Top 20 Holdings\\n({len(selected_assets)} total assets, {len(significant_holdings)} with positive weights)')\n", - "ax1.set_xticks(range(len(top_20_holdings)))\n", - "ax1.set_xticklabels(top_20_holdings['Asset'], rotation=45, ha='right')\n", - "ax1.grid(True, alpha=0.3)\n", - "\n", - "# Add value labels on bars for top holdings\n", - "for i, bar in enumerate(bars):\n", - " height = bar.get_height()\n", - " if height > 0.01: # Only label if weight > 1%\n", - " ax1.text(bar.get_x() + bar.get_width()/2., height + 0.001,\n", - " f'{height:.3f}', ha='center', va='bottom', fontsize=8)\n", - "\n", - "# Portfolio weights pie chart (top 10 holdings)\n", - "top_10_holdings = significant_holdings.head(10)\n", - "other_weight = significant_holdings.iloc[10:]['Weight'].sum() if len(significant_holdings) > 10 else 0\n", - "\n", - "if other_weight > 0:\n", - " pie_data = list(top_10_holdings['Weight']) + [other_weight]\n", - " pie_labels = list(top_10_holdings['Asset']) + [f'Others ({len(significant_holdings)-10} assets)']\n", - "else:\n", - " pie_data = top_10_holdings['Weight']\n", - " pie_labels = top_10_holdings['Asset']\n", - "\n", - "wedges, texts, autotexts = ax2.pie(pie_data, labels=pie_labels, autopct='%1.1f%%', \n", - " startangle=90, textprops={'fontsize': 9})\n", - "ax2.set_title('Portfolio Allocation - Top 10 Holdings + Others')\n", - "\n", - "# Improve pie chart readability\n", - "for autotext in autotexts:\n", - " autotext.set_color('white')\n", - " autotext.set_fontweight('bold')\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n", - "\n", - "# Additional statistics\n", - "print(f\"\\nConcentration Analysis:\")\n", - "print(f\"Herfindahl-Hirschman Index (HHI): {np.sum(optimal_weights**2):.6f}\")\n", - "print(f\"Effective number of assets: {1/np.sum(optimal_weights**2):.2f}\")\n", - "print(f\"Diversification ratio: {len(significant_holdings)}/{len(selected_assets)} = {len(significant_holdings)/len(selected_assets):.2%}\")\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Diversification constraints:\n", + "- Maximum weight per asset: 100.0%\n", + "- This forces allocation across at least 1 assets\n", + "- Confidence level (alpha): 0.95\n", + "- Risk aversion (lambda): 2.0\n", + "- Number of scenarios: 6863\n", + "- Number of assets: 397\n", + "Setting parameter time_limit to 3.000000e+02\n", + "Setting parameter log_to_console to true\n", + "Setting parameter method to 0\n", + "cuOpt version: 25.10.0, git hash: f4082fe3, host arch: x86_64, device archs: 75\n", + "CPU: AMD Ryzen Threadripper PRO 3975WX 32-Cores, threads (physical/logical): 32/64, RAM: 1.65 GiB\n", + "CUDA 13.0, device: Quadro RTX 8000 (ID 0), VRAM: 47.25 GiB\n", + "CUDA device UUID: ffffffb7fffffff2ffffffb679-057e-ffff\n", + "\n", + "Third-party presolve is disabled, skipping\n", + "Solving a problem with 6864 constraints 7261 variables (0 integers) and 2725089 nonzeros\n", + "Objective offset -0.000000 scaling_factor -1.000000\n", + "Running concurrent\n", + "\n", + " Iter Primal Obj. Dual Obj. Gap Primal Res. Dual Res. Time\n", + " 0 -0.00000000e+00 -0.00000000e+00 0.00e+00 1.00e+00 3.08e+00 0.129s\n", + " 1000 +2.01815200e-01 +2.00428379e-01 1.39e-03 1.76e-03 5.72e-03 0.379s\n", + "Handling free variables 1\n", + "Dual simplex finished in 0.46 seconds, total time 0.55\n", + "FAILED: CUDSS call ended unsuccessfully with status = 5, details: \"cudssExecute for reordering\"\n", + "PDLP finished\n", + "Barrier finished in 0.59 seconds\n", + "Barrier Solve status A numerical error was encountered.\n", + "Concurrent time: 0.548s, total time 0.595s\n", + "Solved with dual simplex\n", + "Status: Optimal Objective: 2.01903713e-01 Iterations: 1032 Time: 0.595s\n", + "\n", + "Optimization successfuli!\n", + "Status: Optimal\n", + "Objective value: 0.201904\n", + "Expected annual return: 0.2920 (29.20%)\n", + "CVaR (95%): 0.0450\n" + ] + } + ], + "source": [ + "# Set optimization parameters\n", + "alpha = 0.95 # 95% confidence level\n", + "lambda_risk = 2.0 # Risk aversion parameter\n", + "\n", + "# Portfolio weight bounds for DIVERSIFIED portfolio\n", + "w_min = np.zeros(n_assets) # No short selling\n", + "w_max = np.ones(n_assets) # Maximum can be 100% in any single asset\n", + "\n", + "print(f\"Diversification constraints:\")\n", + "print(f\"- Maximum weight per asset: {w_max[0]:.1%}\")\n", + "print(f\"- This forces allocation across at least {1/w_max[0]:.0f} assets\")\n", + "\n", + "# Alternative diversification strategies (uncomment to try):\n", + "\n", + "# Strategy 1: Even more diversified (max 10% per asset)\n", + "# w_max = np.ones(n_assets) * 0.10\n", + "\n", + "# Strategy 2: Minimum holdings requirement (forces broader diversification)\n", + "# min_holdings = 30 # Require at least 30 assets\n", + "# w_min = np.zeros(n_assets)\n", + "# w_min[:min_holdings] = 0.005 # Minimum 0.5% in top assets\n", + "\n", + "# Strategy 3: Lower risk aversion (allows more return-seeking behavior)\n", + "# lambda_risk = 0.5 # Less conservative approach\n", + "\n", + "print(f\"- Confidence level (alpha): {alpha}\")\n", + "print(f\"- Risk aversion (lambda): {lambda_risk}\")\n", + "print(f\"- Number of scenarios: {n_scenarios_total}\")\n", + "print(f\"- Number of assets: {n_assets}\")\n", + "\n", + "# Solve the optimization problem\n", + "try:\n", + " optimal_weights, cvar_value, expected_return, solve_result = solve_cvar_portfolio(\n", + " scenarios=all_scenarios,\n", + " scenario_probs=scenario_probs,\n", + " mu=mu_annual, # Use annualized returns\n", + " alpha=alpha,\n", + " lambda_risk=lambda_risk,\n", + " w_min=w_min,\n", + " w_max=w_max,\n", + " solver_settings=solver_settings\n", + " )\n", + " \n", + " print(f\"\\nOptimization successfuli!\")\n", + " print(f\"Status: {solve_result.Status.name}\")\n", + " print(f\"Objective value: {solve_result.ObjValue:.6f}\")\n", + " print(f\"Expected annual return: {expected_return:.4f} ({expected_return*100:.2f}%)\")\n", + " print(f\"CVaR (95%): {cvar_value:.4f}\")\n", + " \n", + "except Exception as e:\n", + " print(f\"Optimization failed: {e}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Analyze the Optimal Portfolio\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CVaR Portfolio Optimization Summary\n", - "==================================================\n", - "Dataset: S&P 500 stocks (397 assets)\n", - "Optimization method: CVaR with cuOpt GPU acceleration\n", - "Confidence level: 95.0%\n", - "Risk aversion parameter: 2.0\n", - "Number of scenarios: 6,863\n", - "\n", - "Optimal Portfolio Performance:\n", - "- Expected annual return: 29.20%\n", - "- Annual volatility: 31.52%\n", - "- Sharpe ratio: 0.926\n", - "- CVaR (95%): 4.50%\n", - "- Number of assets with positive weights: 5\n", - "\n", - "Top 5 Holdings:\n", - "- NVDA: 33.00%\n", - "- AAPL: 32.08%\n", - "- NFLX: 24.85%\n", - "- MNST: 6.89%\n", - "- BKNG: 3.20%\n", - "\n", - "Computational Performance:\n", - "- Solver status: Optimal\n", - "- Objective value: 0.201904\n" - ] - } - ], - "source": [ - "# Final summary statistics\n", - "print(\"CVaR Portfolio Optimization Summary\")\n", - "print(\"=\" * 50)\n", - "print(f\"Dataset: S&P 500 stocks ({n_assets} assets)\")\n", - "print(f\"Optimization method: CVaR with cuOpt GPU acceleration\")\n", - "print(f\"Confidence level: {alpha*100}%\")\n", - "print(f\"Risk aversion parameter: {lambda_risk}\")\n", - "print(f\"Number of scenarios: {n_scenarios_total:,}\")\n", - "\n", - "if 'optimal_weights' in locals():\n", - " portfolio_std = np.std(all_scenarios @ optimal_weights) * np.sqrt(252)\n", - " print(f\"\\nOptimal Portfolio Performance:\")\n", - " print(f\"- Expected annual return: {expected_return:.2%}\")\n", - " print(f\"- Annual volatility: {portfolio_std:.2%}\")\n", - " print(f\"- Sharpe ratio: {expected_return/portfolio_std:.3f}\")\n", - " print(f\"- CVaR (95%): {cvar_value:.2%}\")\n", - " print(f\"- Number of assets with positive weights: {np.sum(optimal_weights > 0.001)}\")\n", - " \n", - " # Top 5 holdings\n", - " top_5 = portfolio_df.head(5)\n", - " print(f\"\\nTop 5 Holdings:\")\n", - " for _, row in top_5.iterrows():\n", - " if row['Weight'] > 0.001:\n", - " print(f\"- {row['Asset']}: {row['Weight']:.2%}\")\n", - " \n", - " print(f\"\\nComputational Performance:\")\n", - " print(f\"- Solver status: {solve_result.Status.name}\")\n", - " print(f\"- Objective value: {solve_result.ObjValue:.6f}\")\n", - "else:\n", - " print(\"\\nOptimization was not successful - please check the previous cells.\")\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Portfolio Composition (Top 20 Holdings):\n", + "======================================================================\n", + " NVDA: 0.3300 ( 33.00%) | Expected Return: 0.3199\n", + " AAPL: 0.3208 ( 32.08%) | Expected Return: 0.2685\n", + " NFLX: 0.2485 ( 24.85%) | Expected Return: 0.2995\n", + " MNST: 0.0689 ( 6.89%) | Expected Return: 0.2560\n", + " BKNG: 0.0320 ( 3.20%) | Expected Return: 0.2582\n" + ] + } + ], + "source": [ + "# Create portfolio results DataFrame\n", + "portfolio_df = pd.DataFrame({\n", + " 'Asset': selected_assets,\n", + " 'Weight': optimal_weights,\n", + " 'Expected_Return': mu_annual\n", + "})\n", + "\n", + "# Sort by weight (descending)\n", + "portfolio_df = portfolio_df.sort_values('Weight', ascending=False)\n", + "\n", + "# Display portfolio composition (top holdings only)\n", + "significant_holdings = portfolio_df[portfolio_df['Weight'] > 0.001] # Only assets with weight > 0.1%\n", + "top_holdings = significant_holdings.head(20) # Show top 20 holdings\n", + "\n", + "print(\"Optimal Portfolio Composition (Top 20 Holdings):\")\n", + "print(\"=\" * 70)\n", + "for _, row in top_holdings.iterrows():\n", + " print(f\"{row['Asset']:>6}: {row['Weight']:>8.4f} ({row['Weight']*100:>6.2f}%) | Expected Return: {row['Expected_Return']:>8.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 8. Summary and Key Takeaways\n", - "\n", - "This notebook demonstrated how to implement CVaR portfolio optimization using NVIDIA's cuOpt Python API with S&P 500 data. \n", - "\n", - "### Key Features Implemented:\n", - "1. **GPU-Accelerated Optimization**: Used cuOpt for fast linear programming solution\n", - "2. **CVaR Risk Management**: Implemented conditional value-at-risk as the risk measure\n", - "3. **Scenario-Based Approach**: Combined historical and Monte Carlo simulation scenarios\n", - "4. **Diversification Constraints**: Added maximum weight limits to improve portfolio diversification\n", - "5. **Comprehensive Analysis**: Portfolio composition, risk metrics, and visualization\n", - "\n", - "### Diversification Strategies Available:\n", - "- **Maximum Weight Constraints**: Limit concentration in any single asset\n", - "- **Minimum Weight Requirements**: Force broader asset allocation across more assets\n", - "- **Risk Aversion Adjustment**: Lower lambda_risk for more return-seeking behavior" + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABgoAAAMWCAYAAAAge92DAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XVYFdn/B/D3pbskVQTBRFHsblaxc41dA9fuzrXXdtfVde1du1tXsTDWXrsLFAwkpTvu+f3hj/v1egEBgSHer+fhWTlz5sxn5p7Lzsxn5hyZEEKAiIiIiIiIiIiIiIiKJDWpAyAiIiIiIiIiIiIiIukwUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBEREREREREREREVIQxUUBE+dKWLVsgk8ng6+tbpLadHadOnYKLiwt0dHQgk8kQHh6e6XXnzJkDmUymVGZvbw93d/ecDTKX+fr6QiaTYcuWLdle99dff835wChfcnd3h729/VfrpdWv0vrOEBERERERERV0TBQQUaY8efIEvXv3RokSJaCtrY3ixYvjxx9/xJMnT76p3YULF+LIkSM5E2QeS71hmPqjp6cHJycnzJgxA5GRkTm2ndjYWMyZMwcXL15UWfbx40d0794durq6WL16NbZv3w59ff0c2/a3cnJyQtWqVVXKDx8+DJlMhiZNmqgs27RpE2QyGc6cOZMXIWaJh4cH5syZk+fb/bKvpffTtGnTXI/l0KFD6NGjBxwcHKCnp4fy5ctjwoQJ6Saojh07hurVq0NHRwelSpXC7NmzkZyc/NXtXLx4ETKZDAcOHEhzubu7OwwMDL5lV4iIiIiIiIjo/2lIHQAR5X+HDh1Cr169YGZmhgEDBqB06dLw9fXF33//jQMHDmDPnj3o3LlzttpeuHAhunXrhk6dOimV9+nTBz179oS2tnYO7EHuWrt2LQwMDBAdHY0zZ85gwYIFOH/+PK5evZojTx7HxsZi7ty5AKByI/jWrVuIiorCL7/8AldX12/eFgC8ePECamo5k0du2LAh/v77b0RERMDY2FhRfvXqVWhoaODWrVtISkqCpqam0jJ1dXXUq1cv09ux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rKysxLBhw0RYWJjKunZ2dkplHz9+FH369BFGRkbC2NhY9OnTR9y7d0+lX6X1nUnvu5vWd+ncuXOiWrVqQktLSzg6Ooq//vpLTJgwQejo6CjV6dixoyhevLjQ0tISxYsXF7169RIvX75M81gQERERERERfSuZENmYZY+IKANz5szB3LlzERwcnGOT/BLlFBcXF1hYWODs2bNShyIJfj/zn06dOuHJkycq80YQERERERER5RXOUUBERIVSUlISkpOTlcouXryIBw8eoGnTptIERUVeXFyc0u9eXl7w8PBgnyQiIiIiIiJJcY4CIiIqlPz8/ODq6orevXujePHieP78OdatWwdra2sMHTpU6vCoiHJwcIC7uzscHBzw5s0brF27FlpaWpg8ebLUoREREREREVERxkQBEREVSqampqhRowb++usvBAcHQ19fH23btsXixYtRrFgxqcOjIsrNzQ27d+9GQEAAtLW1Ua9ePSxcuBBly5aVOjQiIiIiIiIqwjhHARERERERERERERFREcY5CoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCojomyxduhQVKlSAXC6XOpR8z97eHu7u7lKHQWnIymdjb2+Pdu3a5W5AOczd3R329vaZqjtnzhzIZLLcDUgCWTkGaa1rYGCQswGloW7dupzUmIiICrxbt26hfv360NfXh0wmw/379zO97pYtWyCTyeDr66soa9q0KZo2bZrjcRbEeDLj4sWLkMlkuHjxotShUB5Iq4+m58trHvaV7PH19YVMJsOvv/4qdShEOY6JAiLKtsjISCxZsgRTpkyBmtr//pyMGzcO1atXh5mZGfT09FCxYkXMmTMH0dHRKm3cuXMHbm5uMDIygqGhIVq2bKlyMZH6P+L0fgYNGpRhnB8+fMCcOXOydJHyJQ8PD8yZMyfb6xd0OXEMMyOjz3rPnj25uu3PPX36FHPmzMnUCXdBFBsbizlz5vCiIId963GdMmUKVq9ejYCAgJwNjIiIioTUG4apPzo6OihXrhxGjhyJwMDAHN3WwoULceTIEZXypKQkfP/99wgNDcXvv/+O7du3w87OLke3nZNq164NmUyGtWvXSh1Klq1ZswZbtmyROowsS705nZmf3BYdHY3Zs2fDzc0NZmZmkMlkGR7TZ8+ewc3NDQYGBjAzM0OfPn0QHBycqW3JZDKM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GBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFlGO8vLxQv359lCtXDrVq1cKTJ09U6ly/fh0uLi5wcXFBpUqVMHToUCQkJKS5bMiQIYplAPD333+jbNmycHR0xKBBg5CUlJRn+0ZERERERERERERUWDFRQDlmyJAhGDx4MF6+fIkpU6bA3d1dpU7VqlVx69Yt3L9/H48ePUJQUBC2bNmS7rI1a9YAAHx8fDBz5kxcvnwZ3t7eCAwMxIYNG/Jw74iIiIiIiIiIiIgKJyYKKEcEBQXh9u3b6N27NwCga9euePfuHby9vZXq6enpQVNTEwCQmJiIuLg4yGSyry47cOAAOnToAGtra8hkMgwdOhS7d+/Oq90jIiIiIiIiIiIiKrSYKKAc8e7dO9jY2EBDQwMAIJPJUKpUKbx9+1alrq+vL6pWrQpzc3MYGxsrvXnw5bLhw4cDAN6+fQs7OztFPXt7+zTbJiIiIiIiIiIiIqKsYaKA8py9vT0ePHiAgIAAJCQkwMPDI91lhw4dkjBSIiIiIiIiIiIiosKPiQLKEba2tvD390dycjIAQAiBt2/folSpUumuY2BggB49eqSZDDAwMEDPnj2xc+dOAECpUqXw5s0bxXJfX98M2yYiIiIiIiIiIiKizGGigHKEpaUlqlevjh07dgAADh48iJIlS6JMmTJK9by9vZGUlATg0zwER44cQcWKFdNcdvjwYVSpUgXApzkPjh07hoCAAAghsG7dOvTs2TOvdo+IiIiIiIiIiIio0GKigHLM+vXrsX79epQrVw6LFy/G5s2bAQADBw7EsWPHAADnz59HtWrVULVqVVSrVg1WVlYYN25custmzpwJAHBwcMDcuXPRoEEDlClTBhYWFhgyZIg0O0pERERERERERERUiMiEEELqIPKb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iIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiChfadq0KWQyGTw9PZXKly1bBplMhrFjxwIAZDIZbG1tER8fr6hz5MgR2NvbK3739/fHDz/8AGtraxgaGsLBwQHjxo0DAFSqVAkGBgYwMDCApqYmtLS0FL9XqlQp1/eTKL9gooCIiIiIiIiIiIjynfLly2Pz5s1KZZs3b0aFChWUyuLi4rBq1ap02+nTpw90dHTw/PlzRERE4OzZs3BxcQEAPHnyBNHR0YiOjsaPP/6I4cOHK35/8uRJju8TUX7FRAERERERERERERHlOz179sTJkycREREBAPjvv/8AAHXq1FGqN336dCxatAjh4eFptnPjxg30798fJiYmUFNTg6OjI/r165ersRMVNEwUEBERERERERERUb5jYmICNzc37N69GwCwadMm9O/fX6Ve8+bNUatWLSxZsiTNdho0aICxY8di27ZtePnyZa7GTFRQMVFARERERERERERE+VL//v2xefNmxMXF4eDBg+jTp0+a9RYvXoxVq1bhw4cPKsv279+P9u3bY8WKFahUqRLs7Oywa9eu3A6dqEBhooCIiIiIiIiIiIjypRYtWsDf3x+//PIL6tWrB2tr6zTrVatWDR06dMDcuXNVlhkZGWHOnDm4e/cuwsLCMHr0aPTt2xfPnj3L7fCJCgwmCoiIiIiIiIiIiChfUlNTQ79+/bB48eI0hx363Pz587Fjx44MhxcyMDDAhAkTYGxsjKdPn+Z0uEQFlobUARARERERERERUeEXn5SCyLgkRMQlITL+038j4pIQGZf82b//Vx4Vn4ykFDlShIBcLv7/v4BcCKTIBeRCYLyeGeL9YyGTySCTATI12ad/qwHqmurQ0lGHpvanHy0dDWjqpJal8W9tdWjra0LPSAs6+ppSHy76zLhx49CkSRM0adIkw3oODg746aefsHTpUhgYGCjKJ02ahB9//BFOTk4AgG3btiEmJgY1atTI1biJChImCoiIiIiIiIiIKNtiEpLxNjQW70Jj8TY0Fu/D4vA+LA5hsYlKCYCEZHmObztJloy4qKQcb1dNQwY9Qy3oGX32Y6INAxNtGJjqwMBUG/om2kwo5BEzMzO4urpmqu7MmTOxdetWpbKEhAT07NkTfn5+0NTURMWKFXH06FHY29vnQrREBRMTBURERERE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j9iwYQMuXbqEhg0bIi4uTmndv//+G2ZmZvj7779zZ2e+0bRDDxEakyh1GESUw3SeXZM6hALtXveq+Ff3jdRhUDr2vdiH6x+uSx0G5aF8kShYvXo17O3toaOjgzp16uDmzZvp1t24cSMaNWoEU1NTmJqawtXVVaV+6pjTn/+4ubnl9m4QEVE+ExkZifbt22PSpElwcXEBAPz1118oVaoUmjdvrlI/IiICK1euxM2bN/HmzRv8/fff6Ny5MxITeWFPRESUVcOHD8fVq1dx6dIlpXItLS307t0bmzdvVirfvXs3SpYsicaNG6u0pa6ujpo1a+LgwYMICAhQWtfLywuXLl3Cpk2bcPfuXTx48CB3diib9t9+B89nHMaQqLAxMtGAehi/29kVV78KFtrekzoMyoCAwKxrsxCdqPomHxVOkicK9u7di/Hjx2P27Nm4e/cuqlatilatWqU7HvTFixfRq1cvXLhwAdevX4etrS1atmwJPz8/pXpubm7w9/dX/OzevTsvdoeIiHKRra0t/P39kZycDAAQQuDt27coVaqUSt2oqCi4ubmhY8eOGDdunKL8woULOHr0KOzt7WFvbw8AqFKlCu7du4ezZ8/CxMQEFStWBAC0b98ekZGRePOGT7kQERFllZmZGaZMmYKpU6eqLBswYADOnj2rdB23adMm/PTTTxm2aWJiAldXV/z7779K61WrVg0dO3ZEo0aN8tVbBX7hcZj3z1OpwyCiXGCql7PD5ejVrYtS27eh/J3bKH/nNkofOQy9evXSrqypCfPhw+F46hTKP7gPx7NnYDZggGKxTFMTNosXodytmyhz7hyM2rT53zJtbTiePoViQwbnaPxZIbO2xJSG6b8VTvlHQEwAltxaInUYlEckTxQsX74cgwYNQv/+/eHk5IR169ZBT08PmzZtSrP+zp07MXz4cLi4uKBChQr466+/IJfLce7cOaV62trasLa2VvyYmprmxe4QEVEusrS0RPXq1bFjxw4AwMGDB1GyZEmUKVNGqV50dDTc3Nzg5uaGGTNmKC3buXMn3r17B19fX/j6+gIAHj58qJjX4P79+wgICAAAXL9+HcnJybC1tc39nSMiIiqExo4dizdv3uDIkSNK5c7OzqhevbpiiL8nT57g3r176Nev31fbLFGiBEJDQwEAKSkp2Lp1q2K9vn37YufOnWnOf5DXhBCY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", + "text/plain": [ + "
" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", - "\n", - "SPDX-License-Identifier: MIT\n", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", - "\n", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Concentration Analysis:\n", + "Herfindahl-Hirschman Index (HHI): 0.279259\n", + "Effective number of assets: 3.58\n", + "Diversification ratio: 5/397 = 1.26%\n" + ] } - ], - "metadata": { - "kernelspec": { - "display_name": "cuopt", - "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.12.11" + ], + "source": [ + "# Visualize portfolio composition\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))\n", + "\n", + "# Portfolio weights bar chart (top 20 holdings)\n", + "top_20_holdings = significant_holdings.head(20)\n", + "bars = ax1.bar(range(len(top_20_holdings)), top_20_holdings['Weight'])\n", + "ax1.set_xlabel('Assets (Top 20 Holdings)')\n", + "ax1.set_ylabel('Portfolio Weight')\n", + "ax1.set_title(f'Optimal Portfolio Weights - Top 20 Holdings\\n({len(selected_assets)} total assets, {len(significant_holdings)} with positive weights)')\n", + "ax1.set_xticks(range(len(top_20_holdings)))\n", + "ax1.set_xticklabels(top_20_holdings['Asset'], rotation=45, ha='right')\n", + "ax1.grid(True, alpha=0.3)\n", + "\n", + "# Add value labels on bars for top holdings\n", + "for i, bar in enumerate(bars):\n", + " height = bar.get_height()\n", + " if height > 0.01: # Only label if weight > 1%\n", + " ax1.text(bar.get_x() + bar.get_width()/2., height + 0.001,\n", + " f'{height:.3f}', ha='center', va='bottom', fontsize=8)\n", + "\n", + "# Portfolio weights pie chart (top 10 holdings)\n", + "top_10_holdings = significant_holdings.head(10)\n", + "other_weight = significant_holdings.iloc[10:]['Weight'].sum() if len(significant_holdings) > 10 else 0\n", + "\n", + "if other_weight > 0:\n", + " pie_data = list(top_10_holdings['Weight']) + [other_weight]\n", + " pie_labels = list(top_10_holdings['Asset']) + [f'Others ({len(significant_holdings)-10} assets)']\n", + "else:\n", + " pie_data = top_10_holdings['Weight']\n", + " pie_labels = top_10_holdings['Asset']\n", + "\n", + "wedges, texts, autotexts = ax2.pie(pie_data, labels=pie_labels, autopct='%1.1f%%', \n", + " startangle=90, textprops={'fontsize': 9})\n", + "ax2.set_title('Portfolio Allocation - Top 10 Holdings + Others')\n", + "\n", + "# Improve pie chart readability\n", + "for autotext in autotexts:\n", + " autotext.set_color('white')\n", + " autotext.set_fontweight('bold')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Additional statistics\n", + "print(f\"\\nConcentration Analysis:\")\n", + "print(f\"Herfindahl-Hirschman Index (HHI): {np.sum(optimal_weights**2):.6f}\")\n", + "print(f\"Effective number of assets: {1/np.sum(optimal_weights**2):.2f}\")\n", + "print(f\"Diversification ratio: {len(significant_holdings)}/{len(selected_assets)} = {len(significant_holdings)/len(selected_assets):.2%}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CVaR Portfolio Optimization Summary\n", + "==================================================\n", + "Dataset: S&P 500 stocks (397 assets)\n", + "Optimization method: CVaR with cuOpt GPU acceleration\n", + "Confidence level: 95.0%\n", + "Risk aversion parameter: 2.0\n", + "Number of scenarios: 6,863\n", + "\n", + "Optimal Portfolio Performance:\n", + "- Expected annual return: 29.20%\n", + "- Annual volatility: 31.52%\n", + "- Sharpe ratio: 0.926\n", + "- CVaR (95%): 4.50%\n", + "- Number of assets with positive weights: 5\n", + "\n", + "Top 5 Holdings:\n", + "- NVDA: 33.00%\n", + "- AAPL: 32.08%\n", + "- NFLX: 24.85%\n", + "- MNST: 6.89%\n", + "- BKNG: 3.20%\n", + "\n", + "Computational Performance:\n", + "- Solver status: Optimal\n", + "- Objective value: 0.201904\n" + ] } + ], + "source": [ + "# Final summary statistics\n", + "print(\"CVaR Portfolio Optimization Summary\")\n", + "print(\"=\" * 50)\n", + "print(f\"Dataset: S&P 500 stocks ({n_assets} assets)\")\n", + "print(f\"Optimization method: CVaR with cuOpt GPU acceleration\")\n", + "print(f\"Confidence level: {alpha*100}%\")\n", + "print(f\"Risk aversion parameter: {lambda_risk}\")\n", + "print(f\"Number of scenarios: {n_scenarios_total:,}\")\n", + "\n", + "if 'optimal_weights' in locals():\n", + " portfolio_std = np.std(all_scenarios @ optimal_weights) * np.sqrt(252)\n", + " print(f\"\\nOptimal Portfolio Performance:\")\n", + " print(f\"- Expected annual return: {expected_return:.2%}\")\n", + " print(f\"- Annual volatility: {portfolio_std:.2%}\")\n", + " print(f\"- Sharpe ratio: {expected_return/portfolio_std:.3f}\")\n", + " print(f\"- CVaR (95%): {cvar_value:.2%}\")\n", + " print(f\"- Number of assets with positive weights: {np.sum(optimal_weights > 0.001)}\")\n", + " \n", + " # Top 5 holdings\n", + " top_5 = portfolio_df.head(5)\n", + " print(f\"\\nTop 5 Holdings:\")\n", + " for _, row in top_5.iterrows():\n", + " if row['Weight'] > 0.001:\n", + " print(f\"- {row['Asset']}: {row['Weight']:.2%}\")\n", + " \n", + " print(f\"\\nComputational Performance:\")\n", + " print(f\"- Solver status: {solve_result.Status.name}\")\n", + " print(f\"- Objective value: {solve_result.ObjValue:.6f}\")\n", + "else:\n", + " print(\"\\nOptimization was not successful - please check the previous cells.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary and Key Takeaways\n", + "\n", + "This notebook demonstrated how to implement CVaR portfolio optimization using NVIDIA's cuOpt Python API with S&P 500 data. \n", + "\n", + "### Key Features Implemented:\n", + "1. **GPU-Accelerated Optimization**: Used cuOpt for fast linear programming solution\n", + "2. **CVaR Risk Management**: Implemented conditional value-at-risk as the risk measure\n", + "3. **Scenario-Based Approach**: Combined historical and Monte Carlo simulation scenarios\n", + "4. **Diversification Constraints**: Added maximum weight limits to improve portfolio diversification\n", + "5. **Comprehensive Analysis**: Portfolio composition, risk metrics, and visualization\n", + "\n", + "### Diversification Strategies Available:\n", + "- **Maximum Weight Constraints**: Limit concentration in any single asset\n", + "- **Minimum Weight Requirements**: Force broader asset allocation across more assets\n", + "- **Risk Aversion Adjustment**: Lower lambda_risk for more return-seeking behavior" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", + "\n", + "SPDX-License-Identifier: MIT\n", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", + "\n", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cuopt", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 2 + "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.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb index e013784..6c6c5cd 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -39,8 +39,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -58,11 +104,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-server-cu12 cuopt-sh-client" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 cuopt-sh-client" ] }, { @@ -73,7 +118,7 @@ "outputs": [], "source": [ "#Install notebook dependencies\n", - "!pip install --user -q matplotlib scipy pandas numpy" + "!pip install -q matplotlib scipy pandas numpy" ] }, { diff --git a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index dba3b79..ce50377 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -60,8 +60,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -79,11 +125,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-server-cu12 cuopt-sh-client" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 cuopt-sh-client" ] }, { @@ -94,7 +139,7 @@ "outputs": [], "source": [ "#Install notebook dependencies\n", - "!pip install --user -q matplotlib scipy pandas numpy" + "!pip install -q matplotlib scipy pandas numpy" ] }, { diff --git a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index 64ba4ca..4b74155 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -46,10 +46,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi\n", - "\n", - "\n" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -67,11 +111,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-server-cu12 cuopt-sh-client" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 cuopt-sh-client" ] }, { diff --git a/sample_lp_sever_notebooks/linear-programming.ipynb b/sample_lp_sever_notebooks/linear-programming.ipynb index c345539..d3042f6 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -46,8 +46,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -65,11 +111,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-server-cu12 cuopt-sh-client" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 cuopt-sh-client" ] }, { diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming-with-datamodel.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming-with-datamodel.ipynb index 72a3047..718f7ce 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming-with-datamodel.ipynb @@ -47,8 +47,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -66,11 +112,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-server-cu12 cuopt-sh-client" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 cuopt-sh-client" ] }, { diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb index 41e42a1..d0d103d 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -47,8 +47,54 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi\n" + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" ] }, { @@ -67,11 +113,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Install cuOpt\n", - "\n", "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "#!pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-server-cu12 cuopt-sh-client" + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-server-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 cuopt-sh-client" ] }, { diff --git a/workforce_optimization/workforce_optimization_milp.ipynb b/workforce_optimization/workforce_optimization_milp.ipynb index 2132daf..2d4b161 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -1,806 +1,535 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Workforce Optimization with cuOpt Python API\n", - "\n", - "This notebook demonstrates how to solve a workforce optimization problem using the cuOpt Python API. The problem involves assigning workers to shifts while minimizing total labor costs.\n", - "\n", - "## Problem Description\n", - "\n", - "We need to assign workers to shifts such that:\n", - "- Each shift has the required number of workers.\n", - "- Workers can only be assigned to shifts they are available for.\n", - "- Total labor cost is minimized.\n", - "\n", - "This is a classic assignment problem that can be formulated as a Mixed Integer Linear Program (MILP)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Environment Setup\n", - "\n", - "First, let's check if we have a GPU available and install necessary dependencies.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tue Sep 30 13:38:25 2025 \n", - "+-----------------------------------------------------------------------------------------+\n", - "| NVIDIA-SMI 580.82.07 Driver Version: 580.82.07 CUDA Version: 13.0 |\n", - "+-----------------------------------------+------------------------+----------------------+\n", - "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n", - "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n", - "| | | MIG M. |\n", - "|=========================================+========================+======================|\n", - "| 0 Quadro P620 On | 00000000:42:00.0 Off | N/A |\n", - "| 34% 40C P8 N/A / N/A | 8MiB / 2048MiB | 0% Default |\n", - "| | | N/A |\n", - "+-----------------------------------------+------------------------+----------------------+\n", - "| 1 Quadro RTX 8000 On | 00000000:61:00.0 On | Off |\n", - "| 33% 42C P0 70W / 260W | 1895MiB / 49152MiB | 10% Default |\n", - "| | | N/A |\n", - "+-----------------------------------------+------------------------+----------------------+\n", - "\n", - "+-----------------------------------------------------------------------------------------+\n", - "| Processes: |\n", - "| GPU GI CI PID Type Process name GPU Memory |\n", - "| ID ID Usage |\n", - "|=========================================================================================|\n", - "| 0 N/A N/A 4408 G /usr/lib/xorg/Xorg 4MiB |\n", - "| 1 N/A N/A 4408 G /usr/lib/xorg/Xorg 702MiB |\n", - "| 1 N/A N/A 4664 G /usr/bin/gnome-shell 249MiB |\n", - "| 1 N/A N/A 7558 G ...ersion=20250926-130007.640000 223MiB |\n", - "| 1 N/A N/A 589564 G ...ess --variations-seed-version 502MiB |\n", - "| 1 N/A N/A 771862 G ...slack/215/usr/lib/slack/slack 98MiB |\n", - "+-----------------------------------------------------------------------------------------+\n" - ] - } - ], - "source": [ - "# Check for GPU availability\n", - "!nvidia-smi\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# Install cuOpt if not already installed\n", - "# Uncomment the following line if running in Google Colab or similar environment\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12 # For cuda 12\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 # For cuda 13\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Import Required Libraries\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/luffy/.local/lib/python3.12/site-packages/cudf/utils/_ptxcompiler.py:64: UserWarning: Error getting driver and runtime versions:\n", - "\n", - "stdout:\n", - "\n", - "\n", - "\n", - "stderr:\n", - "\n", - "Traceback (most recent call last):\n", - " File \"\", line 4, in \n", - " File \"/home/luffy/miniforge3/envs/cuopt/lib/python3.12/site-packages/numba_cuda/numba/cuda/cudadrv/driver.py\", line 393, in safe_cuda_api_call\n", - " return self._check_cuda_python_error(fname, libfn(*args))\n", - " ^^^^^^^^^^^^\n", - "TypeError: cuDriverGetVersion() takes no arguments (1 given)\n", - "\n", - "\n", - "Not patching Numba\n", - " warnings.warn(msg, UserWarning)\n", - "/home/luffy/.local/lib/python3.12/site-packages/cupy/_environment.py:596: UserWarning: \n", - "--------------------------------------------------------------------------------\n", - "\n", - " CuPy may not function correctly because multiple CuPy packages are installed\n", - " in your environment:\n", - "\n", - " cupy, cupy-cuda12x\n", - "\n", - " Follow these steps to resolve this issue:\n", - "\n", - " 1. For all packages listed above, run the following command to remove all\n", - " existing CuPy installations:\n", - "\n", - " $ pip uninstall \n", - "\n", - " If you previously installed CuPy via conda, also run the following:\n", - "\n", - " $ conda uninstall cupy\n", - "\n", - " 2. Install the appropriate CuPy package.\n", - " Refer to the Installation Guide for detailed instructions.\n", - "\n", - " https://docs.cupy.dev/en/stable/install.html\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\n", - " warnings.warn(f'''\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", - "from cuopt.linear_programming.solver_settings import SolverSettings\n", - "import time\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem Data Setup\n", - "\n", - "Define the shift requirements, worker pay rates, and availability constraints.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of shifts: 14\n", - "Number of workers: 7\n", - "Number of available assignments: 73\n" - ] - } - ], - "source": [ - "# Number of workers required for each shift\n", - "shift_requirements = {\n", - " \"Mon1\": 3,\n", - " \"Tue2\": 2,\n", - " \"Wed3\": 4,\n", - " \"Thu4\": 2,\n", - " \"Fri5\": 5,\n", - " \"Sat6\": 3,\n", - " \"Sun7\": 4,\n", - " \"Mon8\": 2,\n", - " \"Tue9\": 2,\n", - " \"Wed10\": 3,\n", - " \"Thu11\": 4,\n", - " \"Fri12\": 5,\n", - " \"Sat13\": 7,\n", - " \"Sun14\": 5,\n", - "}\n", - "\n", - "# Amount each worker is paid to work one shift\n", - "worker_pay = {\n", - " \"Amy\": 10,\n", - " \"Bob\": 12,\n", - " \"Cathy\": 10,\n", - " \"Dan\": 8,\n", - " \"Ed\": 8,\n", - " \"Fred\": 9,\n", - " \"Gu\": 11,\n", - "}\n", - "\n", - "# Worker availability \n", - "availability = {\n", - " \"Amy\": [\"Tue2\", \"Wed3\", \"Fri5\", \"Sun7\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", - " \"Bob\": [\"Mon1\", \"Tue2\", \"Fri5\", \"Sat6\", \"Mon8\", \"Thu11\", \"Sat13\", \"Sun14\"],\n", - " \"Cathy\": [\"Wed3\", \"Thu4\", \"Fri5\", \"Sun7\", \"Mon8\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", - " \"Dan\": [\"Tue2\", \"Wed3\", \"Fri5\", \"Sat6\", \"Mon8\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", - " \"Ed\": [\"Mon1\", \"Tue2\", \"Wed3\", \"Thu4\", \"Fri5\", \"Sun7\", \"Mon8\", \"Tue9\", \"Thu11\", \"Sat13\", \"Sun14\"],\n", - " \"Fred\": [\"Mon1\", \"Tue2\", \"Wed3\", \"Sat6\", \"Mon8\", \"Tue9\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", - " \"Gu\": [\"Mon1\", \"Tue2\", \"Wed3\", \"Fri5\", \"Sat6\", \"Sun7\", \"Mon8\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"], \n", - "}\n", - "\n", - "print(f\"Number of shifts: {len(shift_requirements)}\")\n", - "print(f\"Number of workers: {len(worker_pay)}\")\n", - "print(f\"Number of available assignments: {sum(len(v) for v in availability.values())}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Shift Requirements:\n", - " Shift Required Workers\n", - "0 Mon1 3\n", - "1 Tue2 2\n", - "2 Wed3 4\n", - "3 Thu4 2\n", - "4 Fri5 5\n", - "5 Sat6 3\n", - "6 Sun7 4\n", - "7 Mon8 2\n", - "8 Tue9 2\n", - "9 Wed10 3\n", - "10 Thu11 4\n", - "11 Fri12 5\n", - "12 Sat13 7\n", - "13 Sun14 5\n", - "\n", - "Worker Pay Rates:\n", - " Worker Pay per Shift\n", - "0 Amy 10\n", - "1 Bob 12\n", - "2 Cathy 10\n", - "3 Dan 8\n", - "4 Ed 8\n", - "5 Fred 9\n", - "6 Gu 11\n" - ] - } - ], - "source": [ - "# Create DataFrames for better visualization\n", - "shifts_df = pd.DataFrame(list(shift_requirements.items()), columns=['Shift', 'Required Workers'])\n", - "workers_df = pd.DataFrame(list(worker_pay.items()), columns=['Worker', 'Pay per Shift'])\n", - "\n", - "print(\"Shift Requirements:\")\n", - "print(shifts_df)\n", - "print(\"\\nWorker Pay Rates:\")\n", - "print(workers_df)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem Formulation\n", - "\n", - "Now we'll create the optimization problem using the cuOpt Python API as a MILP. The problem has:\n", - "- **Variables**: Binary variables for each (worker, shift) assignment\n", - "- **Objective**: Minimize total labor cost\n", - "- **Constraints**: Meet shift requirements and respect worker availability\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Created 73 binary decision variables\n", - "Sample variables: ['Amy_Tue2', 'Amy_Wed3', 'Amy_Fri5', 'Amy_Sun7', 'Amy_Tue9']\n" - ] - } - ], - "source": [ - "# Create the optimization problem\n", - "problem = Problem(\"workforce_optimization\")\n", - "\n", - "# Add binary decision variables for each available (worker, shift) assignment\n", - "assignment_vars = {}\n", - "for worker, shifts in availability.items():\n", - " for shift in shifts:\n", - " var_name = f\"{worker}_{shift}\"\n", - " var = problem.addVariable(name=var_name, vtype=VType.INTEGER, lb=0.0, ub=1.0)\n", - " assignment_vars[(worker, shift)] = var\n", - "\n", - "print(f\"Created {len(assignment_vars)} binary decision variables\")\n", - "print(f\"Sample variables: {[var.getVariableName() for var in assignment_vars.values()][:5]}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Objective function set: minimize total labor cost\n" - ] - } - ], - "source": [ - "# Create objective function: minimize total labor cost\n", - "objective_expr = LinearExpression([], [], 0.0)\n", - "\n", - "for (worker, shift), var in assignment_vars.items():\n", - " cost = worker_pay[worker]\n", - " if cost != 0: # Only include non-zero coefficients\n", - " objective_expr += var * cost\n", - "\n", - "# Set objective function: minimize total cost\n", - "problem.setObjective(objective_expr, sense.MINIMIZE)\n", - "print(\"Objective function set: minimize total labor cost\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Added 14 shift requirement constraints\n", - "Sample constraints: ['shift_Mon1', 'shift_Tue2', 'shift_Wed3', 'shift_Thu4', 'shift_Fri5']\n" - ] - } - ], - "source": [ - "# Add constraints: assign exactly the required number of workers to each shift\n", - "constraint_names = []\n", - "\n", - "for shift, required_count in shift_requirements.items():\n", - " # Find all workers available for this shift\n", - " shift_assignments = []\n", - " for (worker, shift_name), var in assignment_vars.items():\n", - " if shift_name == shift:\n", - " shift_assignments.append(var)\n", - " \n", - " if len(shift_assignments) > 0:\n", - " # Create constraint: sum of assignments for this shift = required_count\n", - " shift_expr = LinearExpression([], [], 0.0)\n", - " for var in shift_assignments:\n", - " shift_expr += var\n", - " \n", - " constraint = problem.addConstraint(shift_expr == required_count, name=f\"shift_{shift}\")\n", - " constraint_names.append(f\"shift_{shift}\")\n", - " else:\n", - " print(f\"Warning: No workers available for shift {shift}\")\n", - "\n", - "print(f\"Added {len(constraint_names)} shift requirement constraints\")\n", - "print(f\"Sample constraints: {constraint_names[:5]}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solver Configuration and Solution\n", - "\n", - "Configure the solver settings and solve the optimization problem.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Solver configured with 60-second time limit\n" - ] - } - ], - "source": [ - "# Configure solver settings\n", - "settings = SolverSettings()\n", - "settings.set_parameter(\"time_limit\", 60.0) # 60 second time limit\n", - "settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", - "settings.set_parameter(\"method\", 0) # Use default method\n", - "\n", - "print(\"Solver configured with 60-second time limit\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Solving workforce optimization problem...\n", - "Problem type: MIP\n", - "Number of variables: 73\n", - "Number of constraints: 14\n", - "Setting parameter time_limit to 6.000000e+01\n", - "Setting parameter log_to_console to true\n", - "Setting parameter method to 0\n", - "cuOpt version: 25.10.0, git hash: c426e3a, host arch: x86_64, device archs: 75\n", - "CPU: AMD Ryzen Threadripper PRO 3975WX 32-Cores, threads (physical/logical): 32/64, RAM: 20.93 GiB\n", - "CUDA 13.0, device: Quadro RTX 8000 (ID 0), VRAM: 47.25 GiB\n", - "CUDA device UUID: ffffffb7fffffff2ffffffb679-057e-ffff\n", - "\n", - "Unpresolved problem:: 14 constraints, 73 variables, 73 nonzeros\n", - "Presolve status:: reduced the problem\n", - "Presolve removed:: 8 constraints, 36 variables, 36 nonzeros\n", - "Presolved problem:: 6 constraints, 37 variables, 37 nonzeros\n", - "Third party presolve time: 0.119085\n", - "Solving a problem with 6 constraints 37 variables (37 integers) and 37 nonzeros\n", - "Objective offset 304.000000 scaling_factor 1.000000\n", - "Running presolve!\n", - "After trivial presolve #constraints 6 #variables 37 objective offset 304.000000.\n", - "Solving LP root relaxation\n", - "Scaling matrix. Maximum column norm 1.000000e+00\n", - "Dual Simplex Phase 1\n", - "Dual feasible solution found.\n", - "Dual Simplex Phase 2\n", - " Iter Objective Num Inf. Sum Inf. Perturb Time\n", - " 1 +3.2400000000000000e+02 6 7.47619048e+00 0.00e+00 0.00\n", - "\n", - "Root relaxation solution found in 11 iterations and 0.00s\n", - "Root relaxation objective +4.68000000e+02\n", - "\n", - "Optimal solution found at root node. Objective 4.6800000000000000e+02. Time 0.00.\n", - "B&B added a solution to population, solution queue size 0 with objective 468\n", - "Consuming B&B solutions, solution queue size 1\n", - "Post-solve status:: succeeded\n", - "Solution objective: 468.000000 , relative_mip_gap 0.000000 solution_bound 468.000000 presolve_time 0.169514 total_solve_time 0.302656 max constraint violation 0.000000 max int violation 0.000000 max var bounds violation 0.000000 nodes 0 simplex_iterations 11\n", - "\n", - "Solve completed in 0.303 seconds\n", - "Solver status: Optimal\n", - "Objective value: $468.00\n" - ] - } - ], - "source": [ - "# Solve the problem\n", - "print(\"Solving workforce optimization problem...\")\n", - "print(f\"Problem type: {'MIP' if problem.IsMIP else 'LP'}\")\n", - "print(f\"Number of variables: {problem.NumVariables}\")\n", - "print(f\"Number of constraints: {problem.NumConstraints}\")\n", - "\n", - "problem.solve(settings)\n", - "\n", - "print(f\"\\nSolve completed in {problem.SolveTime:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solution Analysis\n", - "\n", - "Let's analyze the optimal solution and create visualizations.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Optimal Solution Found!\n", - "Total Labor Cost: $468.00\n", - "\n", - "Shift Assignments:\n", - " Fri12: ['Amy', 'Cathy', 'Dan', 'Fred', 'Gu'] (Required: 5, Assigned: 5, Cost: $48)\n", - " Fri5: ['Amy', 'Cathy', 'Dan', 'Ed', 'Gu'] (Required: 5, Assigned: 5, Cost: $47)\n", - " Mon1: ['Ed', 'Fred', 'Gu'] (Required: 3, Assigned: 3, Cost: $28)\n", - " Mon8: ['Dan', 'Ed'] (Required: 2, Assigned: 2, Cost: $16)\n", - " Sat13: ['Amy', 'Bob', 'Cathy', 'Dan', 'Ed', 'Fred', 'Gu'] (Required: 7, Assigned: 7, Cost: $68)\n", - " Sat6: ['Dan', 'Fred', 'Gu'] (Required: 3, Assigned: 3, Cost: $28)\n", - " Sun14: ['Amy', 'Cathy', 'Dan', 'Ed', 'Fred'] (Required: 5, Assigned: 5, Cost: $45)\n", - " Sun7: ['Amy', 'Cathy', 'Ed', 'Gu'] (Required: 4, Assigned: 4, Cost: $39)\n", - " Thu11: ['Amy', 'Cathy', 'Dan', 'Ed'] (Required: 4, Assigned: 4, Cost: $36)\n", - " Thu4: ['Cathy', 'Ed'] (Required: 2, Assigned: 2, Cost: $18)\n", - " Tue2: ['Dan', 'Ed'] (Required: 2, Assigned: 2, Cost: $16)\n", - " Tue9: ['Dan', 'Ed'] (Required: 2, Assigned: 2, Cost: $16)\n", - " Wed10: ['Amy', 'Cathy', 'Dan'] (Required: 3, Assigned: 3, Cost: $28)\n", - " Wed3: ['Cathy', 'Dan', 'Ed', 'Fred'] (Required: 4, Assigned: 4, Cost: $35)\n", - "\n", - "Worker Assignments:\n", - " Amy: ['Fri5', 'Sun7', 'Wed10', 'Thu11', 'Fri12', 'Sat13', 'Sun14'] (7 shifts, $70)\n", - " Bob: ['Sat13'] (1 shifts, $12)\n", - " Cathy: ['Wed3', 'Thu4', 'Fri5', 'Sun7', 'Wed10', 'Thu11', 'Fri12', 'Sat13', 'Sun14'] (9 shifts, $90)\n", - " Dan: ['Tue2', 'Wed3', 'Fri5', 'Sat6', 'Mon8', 'Tue9', 'Wed10', 'Thu11', 'Fri12', 'Sat13', 'Sun14'] (11 shifts, $88)\n", - " Ed: ['Mon1', 'Tue2', 'Wed3', 'Thu4', 'Fri5', 'Sun7', 'Mon8', 'Tue9', 'Thu11', 'Sat13', 'Sun14'] (11 shifts, $88)\n", - " Fred: ['Mon1', 'Wed3', 'Sat6', 'Fri12', 'Sat13', 'Sun14'] (6 shifts, $54)\n", - " Gu: ['Mon1', 'Fri5', 'Sat6', 'Sun7', 'Fri12', 'Sat13'] (6 shifts, $66)\n" - ] - } - ], - "source": [ - "def print_solution():\n", - " \"\"\"Print the optimal solution in a readable format\"\"\"\n", - " if problem.Status.name == \"Optimal\" or problem.Status.name == \"FeasibleFound\":\n", - " print(f\"\\nOptimal Solution Found!\")\n", - " print(f\"Total Labor Cost: ${problem.ObjValue:.2f}\")\n", - " print(\"\\nShift Assignments:\")\n", - " \n", - " # Group assignments by shift\n", - " shift_assignments = {}\n", - " for (worker, shift), var in assignment_vars.items():\n", - " if var.getValue() > 0.5: # Binary variable is 1\n", - " if shift not in shift_assignments:\n", - " shift_assignments[shift] = []\n", - " shift_assignments[shift].append(worker)\n", - " \n", - " # Display assignments by shift\n", - " for shift in sorted(shift_assignments.keys()):\n", - " workers = shift_assignments[shift]\n", - " required = shift_requirements[shift]\n", - " total_cost = sum(worker_pay[w] for w in workers)\n", - " print(f\" {shift}: {workers} (Required: {required}, Assigned: {len(workers)}, Cost: ${total_cost})\")\n", - " \n", - " # Display assignments by worker\n", - " print(\"\\nWorker Assignments:\")\n", - " worker_assignments = {}\n", - " for (worker, shift), var in assignment_vars.items():\n", - " if var.getValue() > 0.5:\n", - " if worker not in worker_assignments:\n", - " worker_assignments[worker] = []\n", - " worker_assignments[worker].append(shift)\n", - " \n", - " for worker in sorted(worker_assignments.keys()):\n", - " shifts = worker_assignments[worker]\n", - " total_cost = len(shifts) * worker_pay[worker]\n", - " print(f\" {worker}: {shifts} ({len(shifts)} shifts, ${total_cost})\")\n", - " \n", - " return shift_assignments, worker_assignments\n", - " else:\n", - " print(f\"No optimal solution found. Status: {problem.Status.name}\")\n", - " return None, None\n", - "\n", - "shift_assignments, worker_assignments = print_solution()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Solution Summary:\n", - "Shift Required Assigned Workers Cost\n", - "Fri12 5 5 Amy, Cathy, Dan, Fred, Gu $48\n", - " Fri5 5 5 Amy, Cathy, Dan, Ed, Gu $47\n", - " Mon1 3 3 Ed, Fred, Gu $28\n", - " Mon8 2 2 Dan, Ed $16\n", - "Sat13 7 7 Amy, Bob, Cathy, Dan, Ed, Fred, Gu $68\n", - " Sat6 3 3 Dan, Fred, Gu $28\n", - "Sun14 5 5 Amy, Cathy, Dan, Ed, Fred $45\n", - " Sun7 4 4 Amy, Cathy, Ed, Gu $39\n", - "Thu11 4 4 Amy, Cathy, Dan, Ed $36\n", - " Thu4 2 2 Cathy, Ed $18\n", - " Tue2 2 2 Dan, Ed $16\n", - " Tue9 2 2 Dan, Ed $16\n", - "Wed10 3 3 Amy, Cathy, Dan $28\n", - " Wed3 4 4 Cathy, Dan, Ed, Fred $35\n" - ] - } - ], - "source": [ - "# Create a summary table of the solution\n", - "if shift_assignments:\n", - " solution_data = []\n", - " for shift in sorted(shift_assignments.keys()):\n", - " workers = shift_assignments[shift]\n", - " required = shift_requirements[shift]\n", - " assigned = len(workers)\n", - " total_cost = sum(worker_pay[w] for w in workers)\n", - " \n", - " solution_data.append({\n", - " 'Shift': shift,\n", - " 'Required': required,\n", - " 'Assigned': assigned,\n", - " 'Workers': ', '.join(workers),\n", - " 'Cost': f\"${total_cost}\"\n", - " })\n", - " \n", - " solution_df = pd.DataFrame(solution_data)\n", - " print(\"\\nSolution Summary:\")\n", - " print(solution_df.to_string(index=False))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Additional Constraints\n", - "\n", - "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit the maximum number of shifts per worker.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Added maximum shift constraints (max 4 shifts per worker)\n" - ] - } - ], - "source": [ - "# Add constraint: each worker can work at most 4 shifts per week\n", - "max_shifts_per_worker = 4\n", - "\n", - "for worker in worker_pay.keys():\n", - " # Find all shifts this worker is available for\n", - " worker_shifts = []\n", - " for (w, shift), var in assignment_vars.items():\n", - " if w == worker:\n", - " worker_shifts.append(var)\n", - " \n", - " if worker_shifts:\n", - " # Create constraint: sum of shifts for this worker <= max_shifts_per_worker\n", - " worker_expr = LinearExpression([], [], 0.0)\n", - " for var in worker_shifts:\n", - " worker_expr += var\n", - " \n", - " constraint = problem.addConstraint(worker_expr <= max_shifts_per_worker, \n", - " name=f\"max_shifts_{worker}\")\n", - "\n", - "print(f\"Added maximum shift constraints (max {max_shifts_per_worker} shifts per worker)\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Solving with maximum shift constraints...\n", - "Problem now has 73 variables and 21 constraints\n", - "Setting parameter time_limit to 6.000000e+01\n", - "Setting parameter log_to_console to true\n", - "Setting parameter method to 0\n", - "cuOpt version: 25.10.0, git hash: c426e3a, host arch: x86_64, device archs: 75\n", - "CPU: AMD Ryzen Threadripper PRO 3975WX 32-Cores, threads (physical/logical): 32/64, RAM: 21.47 GiB\n", - "CUDA 13.0, device: Quadro RTX 8000 (ID 0), VRAM: 47.25 GiB\n", - "CUDA device UUID: ffffffb7fffffff2ffffffb679-057e-ffff\n", - "\n", - "Unpresolved problem:: 21 constraints, 73 variables, 146 nonzeros\n", - "Presolve status:: found an infeasible problem\n", - "\n", - "Solve completed in 0.000 seconds\n", - "Solver status: Infeasible\n", - "Objective value: $nan\n" - ] - } - ], - "source": [ - "# Solve the problem again with the new constraints\n", - "print(\"\\nSolving with maximum shift constraints...\")\n", - "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", - "\n", - "\n", - "problem.solve(settings)\n", - "\n", - "print(f\"\\nSolve completed in {problem.SolveTime:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "No optimal solution found. Status: Infeasible\n" - ] - } - ], - "source": [ - "# Display the new solution\n", - "shift_assignments_new, worker_assignments_new = print_solution()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "This notebook demonstrated how to:\n", - "\n", - "1. **Formulate a workforce optimization problem** using the cuOpt Python API\n", - "2. **Set up binary decision variables** for worker-shift assignments\n", - "3. **Define an objective function** to minimize total labor cost\n", - "4. **Add shift requirement constraints** to ensure proper staffing\n", - "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", - "6. **Add additional constraints** to limit worker shifts\n", - "7. **Analyze and compare solutions** before and after constraint modifications\n", - "\n", - "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like workforce scheduling.\n", - "\n", - "### Key Benefits of cuOpt:\n", - "- **High Performance**: GPU-accelerated solving for large-scale problems\n", - "- **Easy to Use**: Intuitive Python API similar to other optimization libraries\n", - "- **Flexible**: Support for both LP and MIP problems\n", - "- **Scalable**: Handles problems with thousands of variables and constraints efficiently\n", - "\n", - "### Problem Extensions:\n", - "This basic workforce optimization model can be extended with additional constraints such as:\n", - "- Minimum rest time between shifts\n", - "- Skill requirements for specific shifts\n", - "- Overtime cost considerations\n", - "- Worker preferences and fairness constraints\n", - "- Multi-week scheduling with carryover constraints" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## License\n", - "\n", - "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", - "SPDX-License-Identifier: MIT\n", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", - "\n", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cuopt", - "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.12.11" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Workforce Optimization with cuOpt Python API\n", + "\n", + "This notebook demonstrates how to solve a workforce optimization problem using the cuOpt Python API. The problem involves assigning workers to shifts while minimizing total labor costs.\n", + "\n", + "## Problem Description\n", + "\n", + "We need to assign workers to shifts such that:\n", + "- Each shift has the required number of workers.\n", + "- Workers can only be assigned to shifts they are available for.\n", + "- Total labor cost is minimized.\n", + "\n", + "This is a classic assignment problem that can be formulated as a Mixed Integer Linear Program (MILP)." + ] }, - "nbformat": 4, - "nbformat_minor": 2 + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Environment Setup\n", + "\n", + "First, let's check if we have a GPU available and install necessary dependencies.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output([\"nvidia-smi\"], shell=False, stderr=subprocess.STDOUT).decode()\n", + " lines = output.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info}
\n", + "
\n", + " \"\"\"))\n", + " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install cuOpt if not already installed\n", + "# Uncomment the following line if running in Google Colab or similar environment\n", + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 # For cuda 12\n", + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 # For cuda 13\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import Required Libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from cuopt.linear_programming.problem import Problem, VType, sense, LinearExpression\n", + "from cuopt.linear_programming.solver_settings import SolverSettings\n", + "import time\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Data Setup\n", + "\n", + "Define the shift requirements, worker pay rates, and availability constraints.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Number of workers required for each shift\n", + "shift_requirements = {\n", + " \"Mon1\": 3,\n", + " \"Tue2\": 2,\n", + " \"Wed3\": 4,\n", + " \"Thu4\": 2,\n", + " \"Fri5\": 5,\n", + " \"Sat6\": 3,\n", + " \"Sun7\": 4,\n", + " \"Mon8\": 2,\n", + " \"Tue9\": 2,\n", + " \"Wed10\": 3,\n", + " \"Thu11\": 4,\n", + " \"Fri12\": 5,\n", + " \"Sat13\": 7,\n", + " \"Sun14\": 5,\n", + "}\n", + "\n", + "# Amount each worker is paid to work one shift\n", + "worker_pay = {\n", + " \"Amy\": 10,\n", + " \"Bob\": 12,\n", + " \"Cathy\": 10,\n", + " \"Dan\": 8,\n", + " \"Ed\": 8,\n", + " \"Fred\": 9,\n", + " \"Gu\": 11,\n", + "}\n", + "\n", + "# Worker availability \n", + "availability = {\n", + " \"Amy\": [\"Tue2\", \"Wed3\", \"Fri5\", \"Sun7\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", + " \"Bob\": [\"Mon1\", \"Tue2\", \"Fri5\", \"Sat6\", \"Mon8\", \"Thu11\", \"Sat13\", \"Sun14\"],\n", + " \"Cathy\": [\"Wed3\", \"Thu4\", \"Fri5\", \"Sun7\", \"Mon8\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", + " \"Dan\": [\"Tue2\", \"Wed3\", \"Fri5\", \"Sat6\", \"Mon8\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", + " \"Ed\": [\"Mon1\", \"Tue2\", \"Wed3\", \"Thu4\", \"Fri5\", \"Sun7\", \"Mon8\", \"Tue9\", \"Thu11\", \"Sat13\", \"Sun14\"],\n", + " \"Fred\": [\"Mon1\", \"Tue2\", \"Wed3\", \"Sat6\", \"Mon8\", \"Tue9\", \"Fri12\", \"Sat13\", \"Sun14\"],\n", + " \"Gu\": [\"Mon1\", \"Tue2\", \"Wed3\", \"Fri5\", \"Sat6\", \"Sun7\", \"Mon8\", \"Tue9\", \"Wed10\", \"Thu11\", \"Fri12\", \"Sat13\", \"Sun14\"], \n", + "}\n", + "\n", + "print(f\"Number of shifts: {len(shift_requirements)}\")\n", + "print(f\"Number of workers: {len(worker_pay)}\")\n", + "print(f\"Number of available assignments: {sum(len(v) for v in availability.values())}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create DataFrames for better visualization\n", + "shifts_df = pd.DataFrame(list(shift_requirements.items()), columns=['Shift', 'Required Workers'])\n", + "workers_df = pd.DataFrame(list(worker_pay.items()), columns=['Worker', 'Pay per Shift'])\n", + "\n", + "print(\"Shift Requirements:\")\n", + "print(shifts_df)\n", + "print(\"\\nWorker Pay Rates:\")\n", + "print(workers_df)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Formulation\n", + "\n", + "Now we'll create the optimization problem using the cuOpt Python API as a MILP. The problem has:\n", + "- **Variables**: Binary variables for each (worker, shift) assignment\n", + "- **Objective**: Minimize total labor cost\n", + "- **Constraints**: Meet shift requirements and respect worker availability\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the optimization problem\n", + "problem = Problem(\"workforce_optimization\")\n", + "\n", + "# Add binary decision variables for each available (worker, shift) assignment\n", + "assignment_vars = {}\n", + "for worker, shifts in availability.items():\n", + " for shift in shifts:\n", + " var_name = f\"{worker}_{shift}\"\n", + " var = problem.addVariable(name=var_name, vtype=VType.INTEGER, lb=0.0, ub=1.0)\n", + " assignment_vars[(worker, shift)] = var\n", + "\n", + "print(f\"Created {len(assignment_vars)} binary decision variables\")\n", + "print(f\"Sample variables: {[var.getVariableName() for var in assignment_vars.values()][:5]}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create objective function: minimize total labor cost\n", + "objective_expr = LinearExpression([], [], 0.0)\n", + "\n", + "for (worker, shift), var in assignment_vars.items():\n", + " cost = worker_pay[worker]\n", + " if cost != 0: # Only include non-zero coefficients\n", + " objective_expr += var * cost\n", + "\n", + "# Set objective function: minimize total cost\n", + "problem.setObjective(objective_expr, sense.MINIMIZE)\n", + "print(\"Objective function set: minimize total labor cost\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add constraints: assign exactly the required number of workers to each shift\n", + "constraint_names = []\n", + "\n", + "for shift, required_count in shift_requirements.items():\n", + " # Find all workers available for this shift\n", + " shift_assignments = []\n", + " for (worker, shift_name), var in assignment_vars.items():\n", + " if shift_name == shift:\n", + " shift_assignments.append(var)\n", + " \n", + " if len(shift_assignments) > 0:\n", + " # Create constraint: sum of assignments for this shift = required_count\n", + " shift_expr = LinearExpression([], [], 0.0)\n", + " for var in shift_assignments:\n", + " shift_expr += var\n", + " \n", + " constraint = problem.addConstraint(shift_expr == required_count, name=f\"shift_{shift}\")\n", + " constraint_names.append(f\"shift_{shift}\")\n", + " else:\n", + " print(f\"Warning: No workers available for shift {shift}\")\n", + "\n", + "print(f\"Added {len(constraint_names)} shift requirement constraints\")\n", + "print(f\"Sample constraints: {constraint_names[:5]}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solver Configuration and Solution\n", + "\n", + "Configure the solver settings and solve the optimization problem.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configure solver settings\n", + "settings = SolverSettings()\n", + "settings.set_parameter(\"time_limit\", 60.0) # 60 second time limit\n", + "settings.set_parameter(\"log_to_console\", True) # Enable solver logging\n", + "settings.set_parameter(\"method\", 0) # Use default method\n", + "\n", + "print(\"Solver configured with 60-second time limit\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the problem\n", + "print(\"Solving workforce optimization problem...\")\n", + "print(f\"Problem type: {'MIP' if problem.IsMIP else 'LP'}\")\n", + "print(f\"Number of variables: {problem.NumVariables}\")\n", + "print(f\"Number of constraints: {problem.NumConstraints}\")\n", + "\n", + "problem.solve(settings)\n", + "\n", + "print(f\"\\nSolve completed in {problem.SolveTime:.3f} seconds\")\n", + "print(f\"Solver status: {problem.Status.name}\")\n", + "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solution Analysis\n", + "\n", + "Let's analyze the optimal solution and create visualizations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def print_solution():\n", + " \"\"\"Print the optimal solution in a readable format\"\"\"\n", + " if problem.Status.name == \"Optimal\" or problem.Status.name == \"FeasibleFound\":\n", + " print(f\"\\nOptimal Solution Found!\")\n", + " print(f\"Total Labor Cost: ${problem.ObjValue:.2f}\")\n", + " print(\"\\nShift Assignments:\")\n", + " \n", + " # Group assignments by shift\n", + " shift_assignments = {}\n", + " for (worker, shift), var in assignment_vars.items():\n", + " if var.getValue() > 0.5: # Binary variable is 1\n", + " if shift not in shift_assignments:\n", + " shift_assignments[shift] = []\n", + " shift_assignments[shift].append(worker)\n", + " \n", + " # Display assignments by shift\n", + " for shift in sorted(shift_assignments.keys()):\n", + " workers = shift_assignments[shift]\n", + " required = shift_requirements[shift]\n", + " total_cost = sum(worker_pay[w] for w in workers)\n", + " print(f\" {shift}: {workers} (Required: {required}, Assigned: {len(workers)}, Cost: ${total_cost})\")\n", + " \n", + " # Display assignments by worker\n", + " print(\"\\nWorker Assignments:\")\n", + " worker_assignments = {}\n", + " for (worker, shift), var in assignment_vars.items():\n", + " if var.getValue() > 0.5:\n", + " if worker not in worker_assignments:\n", + " worker_assignments[worker] = []\n", + " worker_assignments[worker].append(shift)\n", + " \n", + " for worker in sorted(worker_assignments.keys()):\n", + " shifts = worker_assignments[worker]\n", + " total_cost = len(shifts) * worker_pay[worker]\n", + " print(f\" {worker}: {shifts} ({len(shifts)} shifts, ${total_cost})\")\n", + " \n", + " return shift_assignments, worker_assignments\n", + " else:\n", + " print(f\"No optimal solution found. Status: {problem.Status.name}\")\n", + " return None, None\n", + "\n", + "shift_assignments, worker_assignments = print_solution()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a summary table of the solution\n", + "if shift_assignments:\n", + " solution_data = []\n", + " for shift in sorted(shift_assignments.keys()):\n", + " workers = shift_assignments[shift]\n", + " required = shift_requirements[shift]\n", + " assigned = len(workers)\n", + " total_cost = sum(worker_pay[w] for w in workers)\n", + " \n", + " solution_data.append({\n", + " 'Shift': shift,\n", + " 'Required': required,\n", + " 'Assigned': assigned,\n", + " 'Workers': ', '.join(workers),\n", + " 'Cost': f\"${total_cost}\"\n", + " })\n", + " \n", + " solution_df = pd.DataFrame(solution_data)\n", + " print(\"\\nSolution Summary:\")\n", + " print(solution_df.to_string(index=False))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Additional Constraints\n", + "\n", + "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit the maximum number of shifts per worker.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add constraint: each worker can work at most 4 shifts per week\n", + "max_shifts_per_worker = 4\n", + "\n", + "for worker in worker_pay.keys():\n", + " # Find all shifts this worker is available for\n", + " worker_shifts = []\n", + " for (w, shift), var in assignment_vars.items():\n", + " if w == worker:\n", + " worker_shifts.append(var)\n", + " \n", + " if worker_shifts:\n", + " # Create constraint: sum of shifts for this worker <= max_shifts_per_worker\n", + " worker_expr = LinearExpression([], [], 0.0)\n", + " for var in worker_shifts:\n", + " worker_expr += var\n", + " \n", + " constraint = problem.addConstraint(worker_expr <= max_shifts_per_worker, \n", + " name=f\"max_shifts_{worker}\")\n", + "\n", + "print(f\"Added maximum shift constraints (max {max_shifts_per_worker} shifts per worker)\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the problem again with the new constraints\n", + "print(\"\\nSolving with maximum shift constraints...\")\n", + "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", + "\n", + "\n", + "problem.solve(settings)\n", + "\n", + "print(f\"\\nSolve completed in {problem.SolveTime:.3f} seconds\")\n", + "print(f\"Solver status: {problem.Status.name}\")\n", + "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display the new solution\n", + "shift_assignments_new, worker_assignments_new = print_solution()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This notebook demonstrated how to:\n", + "\n", + "1. **Formulate a workforce optimization problem** using the cuOpt Python API\n", + "2. **Set up binary decision variables** for worker-shift assignments\n", + "3. **Define an objective function** to minimize total labor cost\n", + "4. **Add shift requirement constraints** to ensure proper staffing\n", + "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", + "6. **Add additional constraints** to limit worker shifts\n", + "7. **Analyze and compare solutions** before and after constraint modifications\n", + "\n", + "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like workforce scheduling.\n", + "\n", + "### Key Benefits of cuOpt:\n", + "- **High Performance**: GPU-accelerated solving for large-scale problems\n", + "- **Easy to Use**: Intuitive Python API similar to other optimization libraries\n", + "- **Flexible**: Support for both LP and MIP problems\n", + "- **Scalable**: Handles problems with thousands of variables and constraints efficiently\n", + "\n", + "### Problem Extensions:\n", + "This basic workforce optimization model can be extended with additional constraints such as:\n", + "- Minimum rest time between shifts\n", + "- Skill requirements for specific shifts\n", + "- Overtime cost considerations\n", + "- Worker preferences and fairness constraints\n", + "- Multi-week scheduling with carryover constraints" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## License\n", + "\n", + "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", + "SPDX-License-Identifier: MIT\n", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", + "\n", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cuopt", + "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.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 }