From 4b321f53ed765995d018274ffbf486a179532f81 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 09:46:40 -0500 Subject: [PATCH 1/8] set gpu check to all notebook --- .../trnsport_cuopt.ipynb | 34 +++++ .../Production_Planning_Example_Pulp.ipynb | 128 +++++++++++------- PuLP_integration_example/Simple_LP_pulp.ipynb | 124 +++++++++++------ .../Simple_MIP_pulp.ipynb | 34 +++++ PuLP_integration_example/Sudoku_pulp.ipynb | 34 +++++ diet_optimization/diet_optimization_lp.ipynb | 28 +++- .../diet_optimization_milp.ipynb | 28 +++- ...t_matrix_and_waypoint_graph_creation.ipynb | 35 +++++ .../intra-factory_transport.ipynb | 28 +++- .../cvrp_daily_deliveries.ipynb | 28 +++- .../cvrptw_benchmark_gehring_homberger.ipynb | 28 +++- .../cvrptw_service_team_routing.ipynb | 28 +++- .../CVaR/01_optimization_with_cufolio.ipynb | 35 +++++ .../CVaR/02_backtesting.ipynb | 35 +++++ .../CVaR/03_advanced_topics.ipynb | 35 +++++ .../cvar_portfolio_optimization.ipynb | 30 +++- .../cvrptw_benchmark_gehring_homberger.ipynb | 28 +++- .../cvrptw_service_team_routing.ipynb | 28 +++- .../linear-programming-with-datamodel.ipynb | 28 +++- .../linear-programming.ipynb | 28 +++- ...er-linear-programming-with-datamodel.ipynb | 28 +++- .../mixed-integer-linear-programming.ipynb | 28 +++- .../workforce_optimization_milp.ipynb | 30 +++- 23 files changed, 768 insertions(+), 122 deletions(-) diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index e3be0ae..aac3315 100644 --- a/GAMSPy_integration_example/trnsport_cuopt.ipynb +++ b/GAMSPy_integration_example/trnsport_cuopt.ipynb @@ -52,6 +52,40 @@ "subprocess.run(f\"unzip -o cuopt-link-release.zip -d {gams_base_path}\", shell=True, check=True)" ] }, + { + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index e3b6aec..32439dc 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -1,23 +1,10 @@ { - "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", @@ -38,38 +25,72 @@ "\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", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "!pip install pulp==3.2.0" - ], + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "id": "T2L7jTld2Qqj" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "!pip install pulp==3.2.0" + ] }, { "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" - ], + "execution_count": null, "metadata": { "collapsed": true, "id": "tFLzH53z2Qoc" }, - "execution_count": null, - "outputs": [] + "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", @@ -98,10 +119,7 @@ " 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", @@ -135,17 +153,22 @@ }, { "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." - ], - "metadata": { - "id": "OG02AqK2LpZ1" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UL0TM5pTLp_m" + }, + "outputs": [], "source": [ "\n", "# Solve the problem using CUOPT\n", @@ -157,12 +180,23 @@ "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": [] + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" } - ] -} \ No newline at end of file + }, + "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..b6222d5 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -1,23 +1,10 @@ { - "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", @@ -39,45 +26,76 @@ "\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", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "!pip install pulp==3.2.0" - ], + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "id": "QSq2W3W7ojKI" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "!pip install pulp==3.2.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", "\n", "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ], - "metadata": { - "id": "sb7vBllkojMN" - }, - "execution_count": null, - "outputs": [] + ] }, { "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" - ], - "metadata": { - "id": "h5GVfdwxPkrL" - } + ] }, { "cell_type": "code", @@ -114,16 +132,21 @@ }, { "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." - ], - "metadata": { - "id": "GIFHcgTMP9qW" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b1OShyAqqVu8" + }, + "outputs": [], "source": [ "status = prob.solve(CUOPT(msg=0))\n", "\n", @@ -133,12 +156,23 @@ "np.set_printoptions(precision=8, suppress=True)\n", "print(\"A solution x is\")\n", "print(x_vals)" - ], - "metadata": { - "id": "b1OShyAqqVu8" - }, - "execution_count": null, - "outputs": [] + ] } - ] + ], + "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..f255797 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -27,6 +27,40 @@ "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": {}, + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index 74ba17b..bd80559 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -29,6 +29,40 @@ "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": {}, + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index dc26d98..0090aa4 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -33,8 +33,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU availability\n", - "!nvidia-smi\n" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" ] }, { diff --git a/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index 8574efb..38326f4 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -33,8 +33,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU availability\n", - "!nvidia-smi\n" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" ] }, { 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..7dd5084 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,41 @@ "- **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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index 5a76813..bffaad5 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -52,8 +52,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { diff --git a/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index f225178..a1c4976 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -64,8 +64,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { diff --git a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index d40ef4a..4204ee1 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -39,8 +39,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { diff --git a/last_mile_delivery/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index 6af2a4a..930e4a8 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -68,8 +68,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPUs\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { 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..93cae40 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 @@ -28,6 +28,41 @@ "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": "d4b0528a", + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index 65ff234..359426f 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -29,6 +29,41 @@ "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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "code", "execution_count": null, 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..3cedacc 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,41 @@ "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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index cf5c407..d69d44f 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -57,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -98,8 +98,32 @@ } ], "source": [ - "# Check GPU availability\n", - "!nvidia-smi\n" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" ] }, { diff --git a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb index e013784..e3940b4 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -39,8 +39,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { diff --git a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index dba3b79..edcc92b 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -60,8 +60,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { diff --git a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index 64ba4ca..4ab9423 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -46,8 +46,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi\n", + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n", "\n", "\n" ] diff --git a/sample_lp_sever_notebooks/linear-programming.ipynb b/sample_lp_sever_notebooks/linear-programming.ipynb index c345539..892b4dd 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -46,8 +46,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { 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..87ecca3 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,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()" ] }, { diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb index 41e42a1..81fd106 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -47,8 +47,32 @@ "metadata": {}, "outputs": [], "source": [ - "# Check for GPU\n", - "!nvidia-smi\n" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" ] }, { diff --git a/workforce_optimization/workforce_optimization_milp.ipynb b/workforce_optimization/workforce_optimization_milp.ipynb index 2132daf..5da181e 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -69,8 +69,32 @@ } ], "source": [ - "# Check for GPU availability\n", - "!nvidia-smi\n" + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" ] }, { From 417249e7c4e859f6d2b60d24b36fdd2ddba5a333 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 09:51:50 -0500 Subject: [PATCH 2/8] update help info --- .../trnsport_cuopt.ipynb | 17 +- .../Production_Planning_Example_Pulp.ipynb | 413 +-- PuLP_integration_example/Simple_LP_pulp.ipynb | 365 +-- .../Simple_MIP_pulp.ipynb | 373 +-- PuLP_integration_example/Sudoku_pulp.ipynb | 515 ++-- diet_optimization/diet_optimization_lp.ipynb | 975 ++++---- .../diet_optimization_milp.ipynb | 973 ++++---- ...t_matrix_and_waypoint_graph_creation.ipynb | 17 +- .../intra-factory_transport.ipynb | 15 + .../cvrp_daily_deliveries.ipynb | 15 + .../cvrptw_benchmark_gehring_homberger.ipynb | 15 + .../cvrptw_service_team_routing.ipynb | 15 + .../CVaR/01_optimization_with_cufolio.ipynb | 17 +- .../CVaR/02_backtesting.ipynb | 17 +- .../CVaR/03_advanced_topics.ipynb | 17 +- .../cvar_portfolio_optimization.ipynb | 2209 +++++++++-------- .../cvrptw_benchmark_gehring_homberger.ipynb | 15 + .../cvrptw_service_team_routing.ipynb | 15 + .../linear-programming-with-datamodel.ipynb | 19 +- .../linear-programming.ipynb | 15 + ...er-linear-programming-with-datamodel.ipynb | 15 + .../mixed-integer-linear-programming.ipynb | 17 +- .../workforce_optimization_milp.ipynb | 1639 ++++++------ 23 files changed, 4023 insertions(+), 3680 deletions(-) diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index aac3315..6acf49e 100644 --- a/GAMSPy_integration_example/trnsport_cuopt.ipynb +++ b/GAMSPy_integration_example/trnsport_cuopt.ipynb @@ -75,15 +75,30 @@ "
\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()\n" + "check_gpu()" ] }, { diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index 32439dc..917bceb 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -1,202 +1,217 @@ { - "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." - ] - }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "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", - "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" - } + "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." + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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": { + "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", + "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 b6222d5..7268473 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -1,178 +1,193 @@ { - "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" - ] - }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "QSq2W3W7ojKI" - }, - "outputs": [], - "source": [ - "!pip install pulp==3.2.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", - "\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ] - }, - { - "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" - } + "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" + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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": { + "id": "QSq2W3W7ojKI" + }, + "outputs": [], + "source": [ + "!pip install pulp==3.2.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", + "\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" + ] + }, + { + "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 f255797..3cf5479 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -1,182 +1,197 @@ { - "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": {}, - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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": { + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index bd80559..474d3a1 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -1,253 +1,268 @@ { - "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": {}, - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "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+E2CbiGgG0G98yZM6rSpUuXVq3JHTt2yIsXLyQ6OloaN25sm88UYSbRqoWUKVNGLfEHUdthbCEVK1ZUS/5xN4Hbt29Lly5dlCsIz5A5cpO7Nad2kUrANoOrXQpffPGFTJ482eA9ZswYqV+/vkrdky1bNmO/VStwXZgji504cUIOHjwo0AOSN29eQacZxf0Ehg4dKr/88oukT59ePVPs6HT/PYt0DW3rNNMG9969e1KzZk2JiYmRHDlyKP54LURHlhOCGKTw12LIGgQ/4hIlSjihCsv0gwBikc6YMUOd0b17dzWsz4/TeSgJOELAFoMLnyk6PNCyRMBnuBOQjuLw4cOSK1cuVXF0VOEV0W6pXr26IDh3lixZVNE9e/aUsWPH2q0Gy/OTAPy1GFWC++aGwOp+qs/DI5SALQYXQ64wCgC+U7QgtcCf2q5dO7UJo4xXe7sF2Rk2btwocC0UKFBAFf/RRx/JnTt37FaF5flI4Nq1a4KkphAk7XNDJgIfVedhEU7AFoP7MsbmHwsmJFgtmLEEw46POSsCjH/79u1V8Zh5htY3xZ0EVq5caYw0gWuKQgKhQsAWg7t69Wpp1KiR+qA1aZZjx44Zm3ZMydywYYPAjYDPvHnzjLKx8vvvvxvb7IAxULhuZdOmTUon5JDCc0UhgVAhYMsoBeRl0j8STDzAcCy0bBcvXiyrVq1SrDDGErnJrBYkm4QxjYuLU/ms4FfGUDUYYviRIRiPW6VKFatV4fUDJIAYGBD0CzidZDHAKvC0CCVgi8GtXbu2Gpnwww8/yN69e6Vo0aKCuAXo9IDAAKKjCvuslpIlSyq/MYz95cuXpVKlSpI6dWo1JliX3bdvX8eT92lduPQkAN867hukWLFinl9yiwRcTsAWlwIMKvxub731loFDG1ukzMarfevWrY3vrF6ZPn26dOjQwZhsgQkYELRsMXh+xIgRVqvA6wdI4OjRo8aZ+OdJIYFQImBLCxdAYFjXrVunxruilYt0x2ihoPXrSz73YELNlCmTLFiwQDAaYffu3YLOOuhXq1Yt9ngHE7QF16pbt67RYWbB5XlJErCUgG0GV9cCsRTwcYMUL15c8KGQAAmQgB0EbHEp2FERlkECJEACbidAg+v2O0T9SIAEwoYADW7Y3EpWhARIwO0EaHDdfoeoHwmQQNgQoMENm1vJipAACbidAA2u2+8Q9SMBEggbAjS4YXMrWRESIAG3Ewh4HO7+/fvl8ePHjtZPR/TCElH/nZabN28qFZBOSMeOcEqnBw8eeBSNzAhO6wSFoBdyxrlBF+gDXaCTG/TBPTLLtm3bHI8VoVNj7dy5U86dO2dWz5F1N/3mEQTfb4mPQxvny2fgwIFx8Rfnhwz4DPAZ4DOQyDMwcuTIZG1pwC3ccePGSfny5f028ME8Yc+ePTJkyBBB9H9EAXNa0MLt3LmzIKg50no7KWi1vfPOO4YKyGrhhswIPXr0UNOohw8fbujm5AoykCBG8tSpU51UQ5WNdE9btmwx9FixYoXjLdw1a9bI559/rqbCm2NXG0ravOKm3zxauP4mLg3Y4FatWlXefPNNm3F7Fod4DBCEdTQnhvQ8yr4tpGCHvPbaa47rYw6uDp3y5cvnuE7QA+EUc+fO7QpdoM/EiRPl4cOHrtBn7ty5UMmQOnXqOB61TrsR8Fu3I161UfkkVtz0m8+QIUMSWia9m51mSbPhNyRAAiQQVAI0uEHFyYuRAAmQQNIEaHCTZsNvSIAESCCoBGhwg4qTFyMBEiCBpAnQ4CbNht+QAAmQQFAJ0OAGFScvRgIkQAJJE6DBTZoNvyEBEiCBoBIIeBxuoFo8e/ZMNm7cKJgyiCy9mLDghkkLgdYnks67e/euXLx4UY2jdXJM5tmzZ9VUXEzuQF68pk2b2p4XD/f95MmTSU5vx3hjjMemkICZgK0G98iRI9KmTRu5cOGCWQepX7++LF261PFB3h5KcSMBgX79+qkZR5gtNmnSpATfW73j6dOnghT2M2fOlLg4zC79UzADCmnvo6Oj9S5blg0bNpQbN24kWhYmCiAWAoUEzARscykgMEfjxo2VsUXa9HLlyknBggWVLps3b5ZevXqZ9eK6iwggq/GwYcMkNjbWUa0wNXnGjBnK2P7lL39RLUi8JWFKdUxMjPz444+26YfWflLG1jYlWFDIEbCthTtq1Cg1Zx0/kJUrV6rXwPigOdKoUSPVEli+fLlMmzZNkMKc4g4CEyZMEMznP3r0qEol76RW+Ic9ZcoUpQKmciO6F4zu1q1b1TP0+++/2/pP4dSpU0qX/Pnzi/eUXHyRPXt29T3/kICZgC0G98WLF8plgIIRRAU+N0iaNGlcERZPKcM/CQgcOHBAdu3alWC/Ezt27Ngh8P9D4FaAsYXUrVtX8Gr/7bffCgKt4Ji0adOq76z8ow1u2bJlXRGHwcq68trBI2CLwUUADB0rFq6ER48eyfbt21WrCQ9s6dKlg1cjXiloBBCF7YMPPlDXQxzSbt26Be3a/l4IcWu1FC5cWK+qZYkSJdQSro+rV6+qjjSPAyzY0HFi8ex+8803gn8IaFjAjwzXGdxmFBLwJmCLwb18+bJRLn4QCOto3teqVSuZPXu246HoDCW5oghgBAA+EPyTdFJy5MhhFI9OV7RstZw+fVqvqoDiWmdjpwUruoX7xRdfyOTJk40SxowZozqBly1bJtmyZTP2c4UEQMCWTjP417TAVwtjW6BAATUsDPuxDzFkKSSQFIE33nhDUqf+83EdO3aseoYwUmHRokWCTlct5paw3mfFUhvce/fuSc2aNVWnnf6nAH0wooNCAt4EbDG45iE8GJ8IvyBix6Jlosdzogf82rVr3vpxmwQUAbgROnbsqNYxDrdkyZKSM2dOFfDd/HzZ4b9FZ2+pUqWU7xYBzOFOQIsWbpdcuXIpHfE83759m3ePBDwI2GJw9UOIkrt06SLVq1dXSuDVDx0gEPi/kCeNQgJJEcDYX51JA0b2/v37amghXFJaoqKi9KplS3T2wm+7fv16GTp0qFEOhjm2a9dObcMoHzx40PiOKyQAArb4cBHhX4vu4NDbuoWLbaf9hFonLt1JIEuWLLJq1SplyDCJBkOv3nrrLaNjDx1V3s+X3TUxp6FBJx6FBMwEbDG4RYoUUf7a58+fKzeCWYFjx44Zm3oihLGDKyTw/wTweq7zfWFkS9euXQ02GIsLqVChgsAoWy2rV682Ztp9+OGHaliaLtP8PJsbE/p7LiObgC0GF2MmMVwGP5gFCxZIkyZN1Fjc77//XuWUwi1AK7hatWqRfTdY+yQJPH78WDp06KBmmVWuXFnWrVunRrVgVICeKt66deskzw/mF3iedVp1uDXKlCkjenoxWuCQQoUKqVx7wSyX1wp9ArYYXGBCllbETEASOPjcMOMMLV4tyL6bPn16vcklCXgQwNtPy5YtlUvh0KFDghle6CDTkyHQmnz//fc9zrFqo3bt2mpkwg8//CB79+6VokWLejzPcG1gJAWecQoJmAnY0mmGAjF0BsO/8ubNq8rXxhaZL2GMe/bsadaL6ySQgMCsWbOkbdu2xn5tbNHCxGu+HpZlHGDRCgwqpqfDf6xFP8/otJs3b57Y1drW5XMZGgRsa+ECB1wJeP2Dz+3SpUvqB4Kpvnb0LIfG7XCvloh+hZ53JwWv8gsXLpQBAwbI7t271RsS/LlIJ253axLPLNwaCNGIVi7e3DDqBq3fjBkzOomJZbuYgK0GFxzgNkDAGgoJBEoAwWvwcYPA4ONDIQFfCNjmUvBFGR5DAiRAAuFMgAY3nO8u60YCJOAqAjS4rrodVIYESCCcCdDghvPdZd1IgARcRYAG11W3g8qQAAmEMwEa3HC+u6wbCZCAqwjQ4LrqdlAZEiCBcCZAgxvOd5d1IwEScBWBgCc+IFi4DhriVI2QyRWCpdO6QI/r169jodK8OK2Pd2hAbDutE9hgOu7Dhw9doQv0gS7QyQ1svO8ZMqPYEf0MHJISnUHjypUrjs80hI5u+s0HlDAhfrpmnC+fgQMHxsXXlx8y4DPAZ4DPQCLPwMiRI5O1pQG3cLt37y6Ic+ukIHMqAoW899578tprrzmpiir7t99+k9GjR6uoVjVq1HBUH4QzRNZdLZUqVTKyEeh9Tiz//e9/q8DhyPzhBpk/f74gL1mfPn0cV+fLL78UBFbXggh6CO7kpOzZs0e++uoriW9wGanpndTHTb95vIFMmzbNLxwBG1zkl0JAEycFwUNgcBHusWnTpk6qospGnjYY3IYNG0qPHj0c1QfG32xwMd8fQV+cFgSfQShFN+gCFshHhvvmBn0QvNxscJF+CgF7nJSpU6cqg4sGlhsCqrvpN79z506/DS47zZx8mlk2CZBARBGgwY2o283KkgAJOEmABtdJ+iybBEggogjQ4EbU7WZlSYAEnCRAg+skfZZNAiQQUQRocCPqdrOyJEACThKgwXWSPssmARKIKAIBj8MNF0pnz56VTZs2yYMHD1QSQIzndTIJ4Pnz55U+GIyfM2dONabX6Qkm4XKvWQ9nCGDq9MaNGwWTFpDsE5OCnJ4Y5AwJkYg1uE+fPhUMLJ85c6bExWGm4p+SJ08eWbx4sURHR+tdtiyREReznaDPixcvjDLxgGI/JlSkTs0XEgMMV0KCACZytGnTJkGsivr168vSpUsdn9hhN8SI/QUPGjRIZsyYoYwtZvNgajCM282bNyUmJkZ+/PFHW+/F4MGDZfr06crYZsuWTSpXrixYPn/+XMaPH68Mrq0KsTASSCEBBJpp3LixMrapUqWScuXKScGCBdVVN2/eLL169UphCaF3ekQaXDwIU6ZMUXcL6bYRKerEiROyfv16ZXR///13GTZsmG13Ey1sPSe7evXqSp99+/apJWIgQPT3tinFgkgghQRGjRolt27dUr8pxGNAaxcuszp16qgrL1++XEVrS2ExIXV6RBpczJ+HXwlinq9et25d5TPF/jVr1hjHYNtKgf8YRh6CGBXZs2dX61h26NBBrePBpZBAqBCAWwwuA0i9evWMWCdp0qRRfRRwoSE0ZqZMmUKlSkHRMyINro7xCYKFCxf2AFmiRAm1jdikV69e9fjOqg3EPC1evLi6/Pbt2z2KQYAMCFwMFBIIFQLnzp1T7jnoC1fCo0ePZMOGDbJixQo5ffp0qFQj6HpGZKdZjhw5DJBwJ6Blq8X8MMAwFytWTH9l6RIuA/iO8ZqFDju4Fvbu3Su7du1Svtxx48ZZWj4vTgLBJIDQhVrQcClfvryY9yHC3+zZsyVz5sz6sIhYRmQL94033jB6/MeOHaseBPhRFy1aJHDmazG3hPU+q5YYJtOsWTN1eRjZCRMmKGOLHXg44WumkECoENAuMuiLRgSMbYECBZQ/V+/r3bt3qFQnaHpGpMGFGwG+UgjG4ZYsWVKNee3cubPHELG0adMGDfTLLgR/VsuWLWXJkiWC3txOnTrJZ599Jq1bt1anzZkzR9q3b++h28uux+9IwGkC5qGWaMWiEYG4w3iD1HF1Y2NjJaA0NU5XLgXlR6TBBa9JkyZJ8+bNFTo8HPfv31dDVtCa1BIVFaVXLV2uXbtWtmzZosrAUJm5c+eqsbcYDwzjC1m9erV4+3fVF/xDAi4kkCtXLkMrZPeAiwwCFx06qiHoWNu/f79aj5Q/EWtw0VG1atUq5SfFeFz0qJ48edIYiI2Wpu5As/phOHjwoFEExi2axbx94MAB81dcJwHXEsidO7ehm/fvSLdwcQA60yJJIrLT7Pbt20aLEqlnunbtatzzrVu3qvUKFSrYljEVEy606KykiW1zppmmwqXbCWA6Op5rTNwxd0RDb6QS0qInQujtcF9GZAsXCRYxvhV+USSgxBhXjAn86KOP1GQD3HTtP7XjAahVq5ZRzIgRI+TQoUPqdWv37t1iHp1Qu3Zt4ziukICbCWD2pp4ev2DBApUXDWPfEbdk4sSJSnW0gqtVq+bmagRdt4hs4eK/Kjqp4FKAccufP7+gg0xPhsArz/vvvx902EldEPPKkXgSAT4uXryoHkLdOtDnYD56lSpV9CaXJOB6AsOHDxc820+ePFEjbbyfaWQlTp8+vevrEUwFI7KFC4CzZs2Stm3bGiy1sS1TpozqoDKP1TUOsnAFIxQwSkK7DfAqBsHMnG7duqkxixYWz0uTQNAJ1KxZUw0Jy5s3r7q2fqaR+h3GuGfPnkEv0+0XjMgWLm4KXnmQshvpsfHqjocB/lzM8zb7VO26gVmzZhUM/8KDiKnHSHMOow83AlrgFBIIRQJNmjRRbjr0jVy6dEk905jqa9cIILcxi1iDq28EJhS4aVJBoUKFlG9Z68clCYQ6AbgNGjVqFOrVCIr+EetSCAo9XoQESIAE/CBAg+sHLB5KAiRAAikhQIObEno8lwRIgAT8IECD6wcsHkoCJEACKSFAg5sSejyXBEiABPwgQIPrByweSgIkQAIpIUCDmxJ6PJcESIAE/CAQ8DhchDO8e/euH0UF/1CkwYFg6bQu0EMHXUZcBqf10bpALwjSwjutE/RASD7M6nODLtAHukAnN+iDe2SWe/fuKd3M++xex7MMwfPkBkZu+s3DBvot8cGv43z5DBw4MC7+4vyQAZ8BPgN8BhJ5BkaOHJmsLaVLwe9/UTyBBEiABAIjQIMbGDeeRQIkQAJ+EwjYh4tMt+ao7n6XHIQTrl+/Lkgj/uabb7oiwAv8XevWrVMpzb2j3Aehun5dAr7Jr776yjgHISd1mhNjpwMrCEGZKVMmdc8cKD5BkXh+cN8QHtNpQZZm5P3SghCiduXV02V6L8+fP69CmDZt2lTdN+/v7d52028eSWZ1wgJfOQRscBGsG4bOSYFxa9GihcT7lwUPhNOCH0vx4sXl73//u/To0cNRdRBtzJxXCvcKgaCdFgQKgvFHvjY3CJ4f3Dc36IPwnGaDizTiiGrnpEydOlWQZw85AM2pcZzSyU2/efyzRnRBf4QuBX9o8VgSIAESSAEBGtwUwOOpJEACJOAPARpcf2jxWBIgARJIAQEa3BTA46kkQAIk4A8BGlx/aPFYEiABEkgBARrcFMDjqSRAAiTgDwEaXH9o8VgSIAESSAGBgMfhBlomBlJv2rRJEJgjZ86casB5kSJFAr0cz4tAApjUgQkUZ86cURmWa9SoIfhQSCA5AkeOHFGTpR49eiSwO3huChcunNxpQfveNoMbHyBH+vTpIzNnzvSIgISU5Ng/evRoSZ2aDe6g3dkwvRB+MG3atFGpt81VrF+/vixdutTxiQJmnbjuHgKIMvbuu+/K2rVrPZSC/Rk0aJAMGzZMUqVK5fGdFRu2WbjBgwfL9OnTlbHNli2bmv6K5fPnz2X8+PHK4FpRQV4zfAj88ssv0rhxY2Vs8eMoV66cFCxYUFVw8+bNakZU+NSWNQkmgX79+hnGNmvWrFKhQgVJkyaNsj/xUb5k7ty5wSwuyWvZYnDj4uJk2rRpSgnM579w4YLs27dPLStVqqT26++T1JRfRDyBUaNGya1bt5QbAXEi0NqFi0pPr1y+fLmKixDxoAjAgwDi1s6fP1/twxR3xGM4dOiQrFy50jguNjbWWLdyxRaD++DBAyM4d8eOHSV79uyqTlh26NBBreOHRCGBpAggSDhcBpB69eoZsTPQSkGfAFxWCEKDwDgUEjATQHwKPB+QJk2aSIYMGYz1dOnSqXUEorFDbPHhZsmSRQV1Qct2+/btHq9+CAABqVy5sh31ZRkhSuDcuXNy8+ZNpT1cCej0wLME31zZsmWldOnSIVozqm01Abid1q9fr4opU6aMUdzFixdVJhTsqFixorHfyhVbDC4qAJdBTEyM4LUvOjpahQpEOLpdu3YJfLnjxo2zsp68dogTuHz5slGDq1evSvny5cW8r1WrVoLoWpkzZzaO4woJgADepBs0aGDAOHHihBw8eFDGjBmj9uXNm1d1mhkHWLhim8HF8ItmzZrJkiVLlJGFodWCHwvC9lFIICkC5hxt+KcNKVCggNy4cUN1fGAfjC2MLoUEXkbgk08+kUWLFhmHDB06VOyKX22LDxf+EwRThrFF73KnTp3ks88+k9atW6tKz5kzR9q3by/oXKOQQGIEzM8GDCv+YcM3d/r0aSNOKzo+rl27ltjp3EcCBgF03KMfAK5OSM+ePWXs2LHG91au2GJwMfZty5Ytqh4IZowhGBh7i6DPML6Q1atXK5+c2uAfEvAiYA6m3qVLFyN7RbFixaRv377qaHSs7d+/3+tMbpKAJ4HevXuriTNwLeAtCYKECnfu3PE80IItWwwu/CVaMI7SLObtAwcOmL/iOgkYBMzpnLxf/8yZCNCZRiEBMwGMgIINwgeZULSgMw1v1hA8N4cPH9ZfWba0xeBiNocWDF43i3mbM83MZLhuJoBpmPo5ghvBLMeOHTM29UQIYwdXIp7Ahg0b1BsRXAnz5s3z4GHuGwibmWa1atUyKjlixAg16Bivf7t37/YYnVC7dm3jOK6QgJkAcnthdAsEudkw8QExFTAGd+LEiWo/WsHVqlVT6/xDApoAOuy1McWzAlcCZrh+8803oic8YDxulSpV9CmWLW0ZpYB57siKioAjGPuGHwVaK6i0FsyPt6PCujwuQ4/A8OHDBc/SkydPBCNbvJ+hIUOGSPr06UOvYtTYUgIlS5aUdu3aqT4jDCXE7Fa8TaPRpwX9AHYk7LTFpYBKYYQCspJqt4E2tpgp1K1bNw7n0XeeyyQJ1KxZU43jxrhJiH6GMHMIxhi9zRQSSIwA4rhgVqtu6Wpji5Zt//79BW/edogtLVxUBAEjMPwLP4wdO3Yo53WOHDkEboT8+fPbUVeWEQYEMDUTMxa3bt0qly5dEjxDGOITFRUVBrVjFawigCnfcEVhNAJcmZihiGcG7s48efJYVWyC69pmcHXJhQoVMnoG9T4uScAfAnAbNGrUyJ9TeCwJKALFixdXYQacwmGbS8GpCrJcEiABEnALARpct9wJ6kECJBD2BGhww/4Ws4IkQAJuIUCD65Y7QT1IgATCngANbtjfYlaQBEjALQRocN1yJ6gHCZBA2BOgwQ37W8wKkgAJuIUADa5b7gT1IAESCHsCAU98wDQ5nYzNKUpIGgjB1GA3JA/UieqGDRumAqw7xQXl6qmLWgfEG/6P//gPvenYEulxMFPMDboAAqLV4b65QR+ds03fnL/+9a/GVHi9z+6ljqZVt25dlVbc7vK9y3PTb/7x48fe6iW7HbDBxXRcnX032VIsOgCZNvGDwdx6c7xUi4pL9rK4AchCAF2cDhMII4Kpr1oQ3R7Bup0WGBX8o3aDLmABg4L75gZ9YEww5VQLQlIi1oiT8vPPPwt+Z4ULF3a8gQUObvrN37t3z+8MIwHfTaTIQY53J2XdunXSokULFZ+hadOmTqqiyoaxxdRBZLXo0aOHo/og0LI5SwLiDWAuudOC3HUIGP711187rYoqH88P7pvO6uqkUgjuZM61tWLFClsiWL2szlOnTlXPM54dc6D3l51j5Xdu+s0j43idOnX8qi59uH7h4sEkQAIkEDgBGtzA2fFMEiABEvCLAA2uX7h4MAmQAAkEToAGN3B2PJMESIAE/CJAg+sXLh5MAiRAAoEToMENnB3PJAESIAG/CNDg+oWLB5MACZBA4AQCHocbeJF/nnnr1i25cuWK2kD2VWTSpLiPwPnz51Uqcgzyzpkzp8q+jAH5lD8JHDlyRDAe89GjRwIuSMmNSQIU9xO4e/euyiKOiUp2jTF2xODGxcUJ0qIjmSQE6Ylv377t/jsUQRpiplqfPn1k5syZHtOE8c8R+0ePHu34tFMnbwdmhL377ruydu1aDzXAZ9CgQYLp3TpDrMcB3HANgX79+qnJQJikNGnSJFv0csSlsHjxYsPY2lJLFuI3gcGDBwtSSyMmQ7Zs2aRy5cpqidTk48ePVwbX74uG0Qn4sWpji4zUFSpUUNNwwWfkyJEyd+7cMKpteFUF/yzxDzE2Ntb2itlucDF3fcCAAYJ88Oapp7bXnAUmSQBvINOmTVPfV69eXQWb2bdvn1pq14/+PsmLhPEX9+/fl/nz56saYnr79evX5dChQ7Jy5Uqj1k78mI3CuZIogQkTJkh0dLQgc/iIESMEz7ndYrvBRUXxgKKFgAA4FPcRePDggQrqAs06duxoBClCsCJEiYPABx+pgtgLOjJckyZNjKAuWEdDAoIgKxR3EThw4IDs2rXLI0CQ3RraanBPnz4tn3/+uRQtWlT+9a9/2V1XlucjAUQWQxAeyPbt2z3OQgcRBC6GSBVEgkOwG3y6dOliYLh48aI8ffpUbVesWNHYzxV3EPj4449l79696gN3mROSxs5CEUXr2bNngqa9G+LX2ln3UCsLLoOYmBhZvny5eg2DawEPK1oI8OmOGzcu1KoUNH3R0m/QoIFxvRMnTsjBgwdlzJgxah/ChcJHSHEXAYTg1GE4MarECbHN4C5btky2bt0qb7/9tjRv3tyJurJMPwhgeFOzZs1kyZIlysjC0Gpp1aqVIMwi5U8Cn3zyiUdYxaFDh0qJEiWIhwQSELDFpYBeQXSUoVWL1i3F3QTgn2zZsqUythja1KlTJ5XBonXr1krxOXPmSPv27R3pdHAjObT+EW8YrhhIz549ZezYsW5UlTo5TMAWg4vXU3Q0lC1bVr788kv59NNP1Ud3LDx58kRtHz582GEcLB4EMNxpy5YtCgbcQBjihLG3GM4H4wtByh5v/676IgL/9O7dWzZu3ChwLRQoUEAR+Oijj+TOnTsRSINVfhkBW1wK+sHbv3+/4OMtSHGCTjT4Bvmq6k3H/m34I7U0btxYr6oltvWQJ/T6YphNpIl5liRcB5i4A0FnGlr+yIYCHyEaEPXr1480PKzvSwjY0sLNkSOH6vVGz7f5o4fQpE6dWu2HwaU4TwCzpbQgZ5xZzNu4b5EoGzZsELgR8Jk3b54HAp10ETs508wDDTfiCdjSwv3www8FH29Ba/b48eOCmTpnz571/prbDhGoVauWUTLGTWOIEyY87Nmzx2N0Qu3atY3jImkFHYowphg4P3HiRDVioXTp0gJDrFv/aExUqVIlkrCwrj4QsMXg+qAHD3ERAbwGN2zYUPklMba0WrVqglYvpq1qQSyMSDUoJUuWlHbt2imf9uXLl9U/I7T2zanp+/bta7gaNDMuSSAy3wl535MlgOFgyCKr3Qba2CJtd7du3WT27NnJXiOcD8DAecy6024DbWzRsu3fv7+aOhrO9WfdAiPgaAuXoxICu2l2nAU3D4Z/DR8+XAUaQtp1+OLhRuCUbFFDHJE6HKMRdu/eraaLRkVFCdwxefLkseMWsYwUEEAMDD09OwWX8ftURw2u39ryBNsJINAHet4piRPQncCJf8u9JOBJgC4FTx7cIgESIAHLCNDgWoaWFyYBEiABTwI0uJ48uEUCJEAClhGgwbUMLS9MAiRAAp4EaHA9eXCLBEiABCwjQINrGVpemARIgAQ8CdDgevLgFgmQAAlYRoAG1zK0vDAJkAAJeBKgwfXkwS0SIAESsIxAwDPNkHUXuZ2cFB3AfPDgwSpqk5O6oGzE9YVMmjTJI2W22mnzH+9piwgo3qhRI5u1SFjcpUuXBCEe3aALtDty5Ii6b27QBwHMzfLOO+8IYlc4KT///LMqHnE1MmTI4KQqqmw3/ebv3bvnN4+A7yZSnd+9e9fvAoN5wsOHD9Xlbty4Iffv3w/mpQO6ljZyeCh09taALhSEk3QwFX0ppDlC5C+nBVygmxt0AQs8Q7hvbtAH98gsiESmgweZ99u5ruP7XrlyxXHjj3q76TevG1j+3I+ADe6iRYsEASCclHXr1kmLFi0EkZuaNm3qpCqqbKQRwtx6ZGzt0aOHo/og2EyuXLkMHZC8E8FWnBbEQH711Vfl66+/dloVVT6eH9w3NwRSQisSvystyKihs0nofXYvp06dKkizhASwuG9Oi5t+8zt37pQ6der4hYQ+XL9w8WASIAESCJwADW7g7HgmCZAACfhFgAbXL1w8mARIgAQCJ0CDGzg7nkkCJEACfhGgwfULFw8mARIggcAJ0OAGzo5nkgAJkIBfBGhw/cLFg0mABEggcAIBj8P1p8hbt24JBk4nJ7lz53bFWL/k9OT3zhI4deqUGheKyS5IbFmvXj1B6nIKCSRH4NmzZ7Jx40Y5c+aMvPLKK1KjRg31Se68YH1vi8Fdvny5GjydnNKYLIBpsRQSSIwAZql1795dYmNjJS4uzjgEqco7deok06ZNE6Qpp5BAYgQwjbtNmzZy4cIFj6/r168vS5cutWWSCV0KHui54WYCSNmO2XIwtpjXX7ZsWbXENvb379/fzepTNwcJIH5H48aNlbHFP+hy5cpJwYIFlUabN2/2qUEYDPVtMbjNmzeX9evXJ/rRQUPQMnnvvfeCUSdeIwwJIN7B5MmTVc1y5swpx44dk6NHj8rJkyclKipK7Z8xY4bcuXMnDGvPKqWUwKhRowSuTbgRvvrqKxW06Pz588bUXLyF6zgNKS3rZefb4lIoVKiQ4OMt+M/y3Xffqd0ff/yxVKtWzfsQbpOAInDu3DnRgVTwWlisWDG1H/P7W7VqJZjzD6MMI1y3bl1SIwGDAIIlwWUAgb9fx11BJLZNmzYZx9mxYovBTawit2/fli5dusjz589VU5+vg4lR4j5NAG6DBg0aqM1atWrp3WppDhvodDhDD8W44QoC+Gd98+ZNpQtcCY8ePZLt27cLorPBLVW6dGnb9HTM4A4dOlTFRU2fPr16VYRfhUICSRHADwNuKW9Bq1bvz5gxo60/Hm9duO1OAghzqeXq1atSvnx5Me/DG9Ls2bMlc+bM+jDLlo4Y3OPHjwv8bRD0OhcpUsSyCvLC4UsAb0f/+Mc/BMPEIP/85z8FQwspJGAmoF1R2AdfLaRAgQKCONp4hrAPxhZG12qxpdPMuxLw16KiWbJkkUGDBnl/zW0SSJYAep2bNGmihojhYPh1R44cmex5PCDyCJiHEMKw7tq1S8VAPn36tDHuH0MNr127Zjkc2w0uKqWDTyP4c548eSyvJAsILwLbtm2TqlWrCjpdkREB7qmFCxeqHujwqilrEwwC5kD86DeqXr26uiw6Xvv27avW0bG2f//+YBT30mvY7lJYuXKl6k2GVjExMS9Vjl+SgDeBKVOmyAcffKCeIcwymzdvntHr7H0st0kABMxuphIlSnhAMWexQGea1WK7wdXDMNDBocfgWl1JXj88CKxatUr69OmjJj5gKi/SrSClEYUEXkYAfUQYfws3JtwIZsF4bi16IoTetmJpu8HF9DpIqVKlbOkVtAIar2k/AbzyDRw40JjSC4M7c+bMBIp069ZNihYtmmA/d0QuAeSFi46OFmSuxoxE+P4xFvf77783sn2jFWzHPABbDS5mAenhGHrgeuQ+Bqy5PwQwoQEzg7R8++23go+3NGvWjAbXGwq3BdPCETPhyZMnaqKMbvFqNEOGDBEMUbVabO00w49GC6M7aRJc+kLg7NmzvhzGY0ggUQI1a9ZUw7/y5s2rvod7AYJJMzDGPXv2VNtW/7G1hYsplxioTiEBfwm0bdtW8KGQQKAE4EpApLCt8SnfL126ZIT21LE4Ar2uP+fZanD9UYzHkgAJkECwCcBt4GRnva0uhWDD4/VIgARIIJQI0OCG0t2iriRAAiFNgAY3pG8flScBEgglAjS4oXS3qCsJkEBIE6DBDenbR+VJgARCiQANbijdLepKAiQQ0gRocEP69lF5EiCBUCJAgxtKd4u6kgAJhDSBgCc+IAapOeyZExSuX7+uih09erTMnz/fCRU8ytRZP2fNmqVms3h8afPGs2fPPErcuXOntG/f3mOfExtXrlwR5LNzgy6o/6FDh1S2Vjfos3fvXo9b8re//U3Spk3rsc/uDR2/olevXpIpUya7i09Qnpt+87/++msC/ZLdET/VNs6XT3ykprj4i/FDBnwG+AzwGUjkGYjPOJKsLaVLIdl/STyABEiABIJDgAY3OBx5FRIgARJIlkDAPtw1a9ZIjRo1ki3AygM2btwoHTt2VPmsGjZsaGVRPl37559/ltdff10+/fRT6dq1q0/nWHUQMpWa04kgFfTUqVOtKs7n69apU0cQWR85yNwgeH5w35AnzWnp0aOHkVUWusB/mi1bNkfVmjNnjgwYMEAOHz6s7pujysQX7qbf/J49e6R58+Z+IQnY4GbNmlWFN/OrtCAfjKy/ECyR38ppuX//vlIBnQtO64PkimZJly6d4zpBH+iFjiCn+Wg20AU6uUEf3COzZM+eXZCtwEnRHWUw/G5g5KbfPGygv+L5q/T3bB5PAiRAAiTgMwEaXJ9R8UASIAESSBkBGtyU8ePZJEACJOAzARpcn1HxQBIgARJIGQEa3JTx49kkQAIk4DMBGlyfUfFAEiABEkgZARrclPHj2SRAAiTgM4GAx+H6XIKLD0SAFwykPnPmjLzyyitqIofTkzlcjIuqhRCBU6dOqQBKGBuO8bP16tWTkiVLOlIDTODYtGmT3Lt3T3LmzCmYpFSkSBFHdHG60Ig1uEeOHJE2bdqoPPXmm1C/fn1ZunSp4wPOzTpxnQR8JfD06VPp3r27xMbGSlwcYsz8KalSpZJOnTrJtGnTxHuChT4m2Mv4oFjSp08fmTlzprx48cK4PBo32I8of94TdIyDwnQlIl0Kv/zyizRu3FgZWzyI5cqVM6Ytbt68WRCKjkICoUhg+PDhsmDBAmVsM2TIIGXLlhUsYXyxv3///rZVa/DgwTJ9+nRlbDFTrXLlymqq8vPnz2X8+PHK4NqmjEsKikiDO2rUKLl165ZyI3z11VeC1i5eezDPH7J8+XIVI9Ul94hqkIBPBNCinDx5sjoWr+7Hjh2To0ePysmTJyUqKkrtnzFjhty5c8en66XkIBh4tKYh1atXV42bffv2qWWlSpXUfv292oiQPxFncPFqA5cBBH6tpk2bqvU0adIoPxMeWgQS13PI1Zf8QwIhQODcuXOCoEUQuMuKFSum1l999VVB8CIInm8YYavlwYMHhi4IEIS4EBAsO3TooNbR6Ik0iTiDi4fy5s2b6j7DlfDo0SPZsGGDrFixQk6fPh1p95/1DSMCaFU2aNBAfWrVquVRM7gVtKBxYbUgyEzx4sVVMdu3b/coDtlHIHAxRJpYT95lRC9fvmxodPXqVSlfvryY96ElMHv2bMmcObNxHFdIIBQIwF+7fv36BKqiVav3Z8yYUUqXLp3gGCt2wGUQExOjXHTR0dHKtYA0Qrt27VK+3HHjxllRrKuvGXEGV79y4a7AVwspUKCA3LhxQ+DMxz4YWxhdCgmEOgE80//4xz8Ew8Qg//znP23LRYghls2aNZMlS5YoIwtDqwUNG8SOjjSJOJeCeagMDCsegp9++km5E+DrgmBIzbVr1yLtWWB9w4wARuM0adJEPc+oGvy68Xm3bKklWtUtW7ZUxlYPSfvss8+kdevWqnwENkfiTvPv0RbFHC4k4gxurly5DORdunRRrznYgQ6Gvn37qu/QsbZ//37jOK6QQKgRQAaLqlWrCoY5YqwrsmwjywbGwNoha9eulS1btqiiMMxy7ty5auzt4sWL1XhgfLF69Wrx9u/aoZuTZUScwTWndjenoMFN0C1crKMzjUICoUhgypQp0qhRI0ELF7PMVq1aJRgTi5amXXLw4EGjKIx5N4t5+8CBA+avwn494ny4mFKI//LwbXmPSsC4RS3Iu0UhgVAjAOOKWVx4VcdU3nXr1hmjBeysi7klDcNvFvN2pM00iziDixxR6DHF6w5m3sDHhbG433//vUycOFE9F2gFV6tWzfyMcJ0EXE8ArrCBAwcaflEYXEyr9ZZu3bpJ0aJFvXcHdds8LG3EiBFSsWJFwYQHJF40j06oXbt2UMt1+8UizuDihmD6I2ImPHnyRA0I1y1efbOGDBki6dOn15tckkBIEMCEBsyY1PLtt98KPt6CkQNWG1z8vhCkBsGhLl68qBow3r8zdOJVqVLFW72w3o44Hy7uZs2aNdXwr7x586qbC/cCBIPDYYx79uyptvmHBEKJwNmzZ12lLoaDde7c2QhQo39nmHiBVnYkDr2MyBYunkq4Ei5cuKBC2F26dMkIYafnnLvqyaUyJOADgbZt2wo+bhGkEcfwLzRiduzYIb/99pv6ncGNkD9/freoaaseEWtwQRluA/TmUkiABKwjUKhQITXm1roSQufKEelSCJ3bQ01JgATCiQANbjjdTdaFBEjA1QRocF19e6gcCZBAOBGgwQ2nu8m6kAAJuJoADa6rbw+VIwESCCcCNLjhdDdZFxIgAVcToMF19e2hciRAAuFEIOBxuAj1plNlOAXkzJkzqmgEDT9+/LhTahjlYmA3BNMZzYHOjQNsXHn8+LFHaUgk+Omnn3rsc2Lj119/VXm13KAL6o/JL/fu3XMFG9wjsyC2hzk1jvk7u9YR+wCC7A2IQ+K0uOk3b84U4zOX+EDBcb584oNiIMk9P2TAZ4DPAJ+BRJ6B+ODuydrSgFu4ixYtkr/+9a8+G3YrDty6dauakz19+nSpW7euFUX4dc3r16+rVOvDhg0TZCp1Uv744w+PwCBvv/22R5Qmp3SDHpjWiXvmBsGcftw3BMN2Wvr37++hB2LKIhmjk4I3WTzPCGjuhum4bvrNI5avzkDs6z0K2OAiD5jOyulrYcE+TsezzZcvn+O6oG46GyrCOzrNRrs3NHNzFlW9z4ll2rRpVQp6p/noumfKlEmgkxv08TauiN3s9Gu8DthfuHBhjwD9mp/dSzf95vGP2l9hp5m/xHg8CZAACQRIgAY3QHA8jQRIgAT8JUCD6y8xHk8CJEACARKgwQ0QHE8jARIgAX8J0OD6S4zHkwAJkECABGhwAwTH00iABEjAXwI0uP4S4/EkQAIkECCBgMfhBlgeTyMBEiABRwhg6rT3lHetSObMmeW1117Tm5YtaXAtQxteF75165ZcuXJFVQrpritVquSKCt69e1el4cYA/VdffdV2nZApd9OmTfLgwQMpVqyYNG3aVDJmzGi7HiwweQJI237jxo1ED3zzzTfVbLpEvwziThrcIMIM10vFxcVJmzZtVOZV1BGzn27fvu2K6vbr108WLFggPXr0kEmTJtmm09OnT6Vv374yc+ZMAR8tefLkkcWLF0t0dLTexaULCOAfc1LG1k71aHDtpB2iZcGAIM21mwSxIsaOHSuxsbGOqDVo0CCZMWOGKhv/gDC9/Mcff5SbN29KTEyM7N27V0qWLOmIbiw0IYFTp06pnYgHMXfu3AQHZM+ePcE+K3bQ4FpBNYyuiTCTAwYMkHTp0knWrFkdb9lOmDBBVqxYIUePHhUYXSfkl19+kSlTpqiiX3/9deVSgNFFYJVGjRqp0JwI+OLUPwMnmLi9TG1wy5YtKw0aNHBMXY5ScAx9aBQ8YsQIFU0Lr+5uiBaFCE27du1yzNjirqG1/+zZM3UD4VbQAWYQsQ5+QsiaNWuMY9QO/nGUgI6jW7p0afnmm2/kv/7rv+Q///M/1brZJWS1kjS4VhMO4esjMtPnn38uRYsWlX/961+uqMnHH3+sXtfxyu5UiEcEUdeCKFpmKVGihNpE6/vq1avmr7juIAHdwv3iiy8EIUJHjx4tY8aMUev6rcQO9Whw7aAcomX06tVLtdLwGo8whm4QjASoUqWK+tgxjCexOufIkcPYjYwRZtHhA7HPbJjNx3DdfgLa4CK7R82aNZWfXd/HzZs3C97g7BAaXDsoh2AZy5YtUz5JtAaaN28egjWwTuU33nhDUqf+86eDjjukWsFrKYLy48erhQZXk3B2GZ/RRkqVKqV8t4MHD1YuITzfhw8flly5cinl4G+3Y+QNO82cfRZcWTpeh9FRhlYtWrcUTwJwIyCjB36kGIeL0QgIHn7//n2PAxHYnOI8ASQGgN/WWwoWLCjt2rWT//mf/1F59pBhQ/vgvY8N1jYNbrBIhtF1kDDwp59+kqpVq8qXX35p1Ey32J48eaKSLr711luCXvpIFIz5xespOsfQuoWxxQ8Yr6tIagqJioqKRDQhVWeMm9Zix6gXGlxNm0uDwJ07d9T6/v37BR9vwfRIdKJly5YtYg0uWrSrVq0StIqOHDkiGMeJf0AffPCBwpUqVSrRHWje/LhtLwHkq9OTYj788EOPVuyxY8cMZeyYqUiDa+DmiiaAzoTEcnyh1x0zrOC/xMgFGNxIFPj6tmzZoqqOcZ1du3Y1MGAsLqRChQqOJ4BUivCPGraH6dcQvImUKVNG9IxA/NOEFCpUyJbGAw2uws0/ZgJoBeDjLXAfHD9+XE2AgO8yUgUtfGRrhSuhcuXKsm7dOkHwEwwz0qMWWrduHal4XFfv2rVrK1fPDz/8oIYUorGAeCDPnz9XuuJtBJ2f2Ge1cJSC1YR5/bAjAF9ty5YtVb0OHTqkJoSgt3v48OFqH15N33///bCrd6hWCAZ15cqVyuWj66CNLfzs8+bNE7v+QbKFq+8AlyTgB4FZs2ap6c5LlixRZ+mZZ3hdRewJPcbTj0vyUAsJwLDiTQQhGjFpBh2/GNON1q+d0d1ocC28yeF2aYxbdJMgpB7GWDohmM67cOFCNXxu9+7d6vUU/tw6derY8mrqRJ3DoUzcI3ycEhpcp8iz3LAgAL92pA6NC4sbaHMl6MO1GTiLIwESiFwCNLiRe+9ZcxIgAZsJ0ODaDJzFkQAJRC4BGtzIvfesOQmQgM0EaHBtBs7iSIAEIpcADW7k3nvWnARIwGYCNLg2A2dxJEACkUsg4HG4iCKFOeVOih6Ij2X69OmdVEWVjYytEORP0sEy1A4H/jx48MCjVCQ+dFonKAS9EObRDbpAH+gCndygD+6RWbZt26ZiNJj32b2uc4Ht3LlTzp07Z3fxCcpz028ecUX8lviZOnG+fAYOHBgXf3F+yIDPAJ8BPgOJPAMjR45M1pYG3MIdN26clC9f3m8DH8wT9uzZI0OGDBEkFqxRo0YwLx3QtdDC7dy5s/Tu3dvxtDRotb3zzjtGPerVqyeDBg0ytp1a6dGjhwrMrQO9OKWHLhcpV27duiVTp07VuxxbfvLJJ0bYRyiBdPCIQuakIMA6EokuWLBAhTR0UheU7abfPFq4/fv39wtJwAYX2QAwl91JQQAKCKZWOplrXjNAlgQIkhs6rc9vv/2m1VLLfPnyOa4TFIEByZ07tyt0gT4TJ06Uhw8fukKfuXPnQiVDEJdBp2A3dtq8ot0I+K3bEaA7ueq56TefIUOG5NRN8D07zRIg4Q4SIAESsIYADa41XHlVEiABEkhAgAY3ARLuIAESIAFrCNDgWsOVVyUBEiCBBARocBMg4Q4SIAESsIYADa41XHlVEiABEkhAgAY3ARLuIAESIAFrCAQ8DtcadXhVEkiewPnz59VU3Hv37knOnDmlYcOGUqRIkeRP5BG2EcBkkitXriRbHsZk2zm+9+zZs+rZwcQgJJFs2rQpk0gme5d4QEQSQMLIPn36yMyZM+XFixcGg1deeUXtHz16tKROzZc2A4yDK8uXL5devXolqwFmHk6aNCnZ41J6wNOnT6Vv377q2YmLw8zkPyVPnjwqy3J0dLTeZemST6eleHnxYBLANNzp06crY5stWzapXLmyYPn8+XMZP368wOBSSCAxApjWPmPGDIGxxew9zAbFP2pMx4+JiZEff/wxsdOCvo8GN+hIeUErCOCHMm3aNHXp6tWry4ULF2Tfvn1qWalSJbVff29F+bymfwSaN28u69evT/TTqFEjdbF06dLJe++959+FAzgaUdimTJmizkQYADw7J06cULrB6P7+++8ybNiwAK7s/yn04frPjGc4QAA+N/wwIB07dpTs2bOrdSw7dOggR44cUUFo1E7+cZxAoUKFBB9v2bx5s3z33XdqN4JOVatWzfuQoG/v2LFDnj17pq4Lt4KOT1G3bl3l///2228FQXpwTNq0aYNevvmCbOGaaXDdtQSyZMkixYsXV/pt377dQ0/EaoXAxUBxL4Hbt29Lly5dlAuocePGfkfaCrRmiHmspXDhwnpVLUuUKKGWf/zxh1y9etXjOys2aHCtoMprWkIALgNEG0OHDDo5PvzwQ7VcvXq18uUiZCjFvQSGDh0qeL1HsoDJkydLqlSpbFE2R44cRjlwJ5jl9OnTxqbZMBs7g7xCgxtkoLycdQQQ87hZs2aqgF27dsmECRMES0irVq1UmE61wT+uI4DYsei0gnTv3t3WYXxvvPGGMXpl7NixcvnyZdV5tmjRIoGLQ4sdBpc+XE2bS1cTwJCwli1bqgDdaBm9++67UqVKFWVwly1bJnPmzFHpchC0266Wk6uBuUw5+GsxmgSuIbsD4cONAL9/bGysYBxuyZIllR7379/3oGS1/xaFsYXrgZwbbiWwdu1aIxsCxnciWDfG5C5evFg6deqk1IZrwds4sAYtAAADk0lEQVS/69b6RJJe165dk6+//lpVuUWLFo5kjsBYX4ycgGDEC4xtwYIF1ZuR2hn/JyoqSq9atqTBtQwtLxxMAgcPHjQuhw4Xs5i3Dxw4YP6K6y4gsHLlSsEbCgRjXp0QtKxXrVole/fuVa6NpUuXysmTJ40RC3gr0h1oVupHl4KVdHntoBHAeEkt3tltzducaaYpuWepMyJnzJhR9BhcO7XD6IgtW7aoIsuWLStdu3Y1it+6datar1ChgnIzGF9YtEKDaxFYXja4BGrVqmVccMSIEVKxYkXBhAckFTSPTqhdu7ZxHFfcQQBjpCGlSpVyJCnm48eP1VhtuBIwdHDdunVKjzFjxqhJENCtdevWWFguNLiWI2YBwSBQv359NUh948aNcvHiRTVgHq1edMRoadOmjepI09tcOk/gzp07alQANEGwGCcEvlp0uMKlcOjQIcmfP7+a4KAnQyB4zvvvv2+LavTh2oKZhQSDwJIlS1Qaeu020MY2TZo00q1bN5k9e3YwiuE1gkjg6NGjxtUwOsApmTVrlrRt29YoXhvbMmXKCDpbzWN1jYMsWGEL1wKovKQ1BLJmzaqGfw0fPlwwXROp4PFDgRsBrRaK+whg+qzuMHNSO0znXbhwoQwYMEB2796t3ozgz0UqenP/gNU60uBaTZjXDzoBzNFv37590K/LC4Y/AQSvwccpoUvBKfIslwRIIOII0OBG3C1nhUmABJwiQIPrFHmWSwIkEHEEaHAj7pazwiRAAk4RoMF1ijzLJQESiDgCNLgRd8tZYRIgAacI0OA6RZ7lkgAJRByBgMfh7t+/XzBH2Uk5fPiwKh5LRJF3WpABFHLmzBnRATuc0gk5wMyCAC9O6wR9oBcCPbtBF+gDXaCTG/QxB+GBbtu2bXMk9gDK1oJnGYI0RufOndO7HVu66TePoOp+S/wskDhfPvHJ15DMnR8y4DPAZ4DPQCLPwH//938na0t9din8/PPPfhtznkACJEACkULAO19aYvX22eAiRQWFBEiABEggcQK+hHhMBXdC4qcn3Ivc7QgAkS9fPsmQIUPCA7iHBEiABCKIAPqx4HtHg1Sn8HlZ9f0yuC+7EL8jARIgARJ4OQGfXQovvwy/JQESIAESSI4ADW5yhPg9CZAACQSJAA1ukEDyMiRAAiSQHAEa3OQI8XsSIAESCBIBGtwggeRlSIAESCA5AjS4yRHi9yRAAiQQJAL/BzcGWtwVwbDYAAAAAElFTkSuQmCC)" - ] - }, - { - "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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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": { + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index 0090aa4..14fb731 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -1,483 +1,498 @@ { - "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": [ - "import subprocess\n", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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 --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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index 38326f4..64c2719 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -1,482 +1,497 @@ { - "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": [ - "import subprocess\n", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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 --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" + } + }, + "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 7dd5084..ea1d3a0 100644 --- a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb +++ b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb @@ -48,15 +48,30 @@ "
\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()\n" + "check_gpu()" ] }, { diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index bffaad5..a477af3 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -69,11 +69,26 @@ "
\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", diff --git a/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index a1c4976..7e877b3 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -81,11 +81,26 @@ "
\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", diff --git a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index 4204ee1..8ab791a 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -56,11 +56,26 @@ "
\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", diff --git a/last_mile_delivery/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index 930e4a8..44119b3 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -85,11 +85,26 @@ "
\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", 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 93cae40..d0e30c3 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 @@ -52,15 +52,30 @@ "
\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()\n" + "check_gpu()" ] }, { diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index 359426f..034f457 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -53,15 +53,30 @@ "
\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()\n" + "check_gpu()" ] }, { 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 3cedacc..a197309 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb @@ -57,15 +57,30 @@ "
\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()\n" + "check_gpu()" ] }, { diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index d69d44f..a9818d5 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -1,1118 +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": null, - "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": [ - "import subprocess\n", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\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", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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": 2, + "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": [ + "# 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": [ { - "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 e3940b4..482731f 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -56,11 +56,26 @@ "
\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", diff --git a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index edcc92b..1961d09 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -77,11 +77,26 @@ "
\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", diff --git a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index 4ab9423..cc58412 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -63,17 +63,30 @@ "
\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()\n", - "\n", - "\n" + "check_gpu()" ] }, { diff --git a/sample_lp_sever_notebooks/linear-programming.ipynb b/sample_lp_sever_notebooks/linear-programming.ipynb index 892b4dd..33e4b56 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -63,11 +63,26 @@ "
\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", 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 87ecca3..0986d75 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 @@ -64,11 +64,26 @@ "
\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", diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb index 81fd106..4f3da8f 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -64,15 +64,30 @@ "
\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()\n" + "check_gpu()" ] }, { diff --git a/workforce_optimization/workforce_optimization_milp.ipynb b/workforce_optimization/workforce_optimization_milp.ipynb index 5da181e..5b958ba 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -1,830 +1,845 @@ { - "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": null, - "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": [ - "import subprocess\n", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\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" - ] - }, + "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": null, + "metadata": {}, + "outputs": [ { - "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" - ] - }, + "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": [ + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\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": 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": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solver Configuration and Solution\n", - "\n", - "Configure the solver settings and solve the optimization problem.\n" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solution Analysis\n", - "\n", - "Let's analyze the optimal solution and create visualizations.\n" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] + "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" + ] } - ], - "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": [ + "# 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" }, - "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 } From f8004bf130f08ba6484ee68bafbd3b9a89efd340 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 10:59:12 -0500 Subject: [PATCH 3/8] update install requirements --- .../trnsport_cuopt.ipynb | 23 +- .../Production_Planning_Example_Pulp.ipynb | 417 ++-- PuLP_integration_example/Simple_LP_pulp.ipynb | 366 ++- .../Simple_MIP_pulp.ipynb | 374 ++- PuLP_integration_example/Sudoku_pulp.ipynb | 516 ++-- diet_optimization/diet_optimization_lp.ipynb | 4 +- .../diet_optimization_milp.ipynb | 4 +- .../intra-factory_transport.ipynb | 20 +- .../cvrp_daily_deliveries.ipynb | 20 +- .../cvrptw_benchmark_gehring_homberger.ipynb | 20 +- .../cvrptw_service_team_routing.ipynb | 18 +- .../CVaR/01_optimization_with_cufolio.ipynb | 20 +- .../CVaR/02_backtesting.ipynb | 20 +- .../CVaR/03_advanced_topics.ipynb | 20 +- .../cvar_portfolio_optimization.ipynb | 2209 ++++++++--------- .../cvrptw_benchmark_gehring_homberger.ipynb | 19 +- .../cvrptw_service_team_routing.ipynb | 19 +- .../linear-programming-with-datamodel.ipynb | 22 +- .../linear-programming.ipynb | 18 +- ...er-linear-programming-with-datamodel.ipynb | 18 +- .../mixed-integer-linear-programming.ipynb | 20 +- .../workforce_optimization_milp.ipynb | 1639 ++++++------ 22 files changed, 2766 insertions(+), 3040 deletions(-) diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index 6acf49e..64e0e55 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", @@ -43,7 +43,9 @@ ], "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 --upgrade -q --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade -q --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19\n", + "\n", "!pip install -q gamspy\n", "import subprocess\n", "import sys\n", @@ -75,30 +77,15 @@ "
\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()" + "check_gpu()\n" ] }, { diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index 917bceb..a100095 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -1,217 +1,206 @@ { - "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", + "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." + ] + }, + { + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, + { + "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", + "# For cuda-12\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "\n", + "# For cuda-13\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" + } }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\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": { - "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", - "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 + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/PuLP_integration_example/Simple_LP_pulp.ipynb b/PuLP_integration_example/Simple_LP_pulp.ipynb index 7268473..6982457 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -1,193 +1,179 @@ { - "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", + "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" + ] + }, + { + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QSq2W3W7ojKI" + }, + "outputs": [], + "source": [ + "!pip install pulp==3.2.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", + "\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" + } }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\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": { - "id": "QSq2W3W7ojKI" - }, - "outputs": [], - "source": [ - "!pip install pulp==3.2.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", - "\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12" - ] - }, - { - "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 + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/PuLP_integration_example/Simple_MIP_pulp.ipynb b/PuLP_integration_example/Simple_MIP_pulp.ipynb index 3cf5479..e6de08c 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -1,197 +1,183 @@ { - "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." - ] + "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": {}, + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, + { + "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 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" + } }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\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": { - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 0 + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index 474d3a1..5c0be84 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -1,268 +1,254 @@ { - "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." - ] + "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": {}, + "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=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, + { + "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 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" + } }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\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": { - "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,iVBORw0KGgoAAAANSUhEUgAAAVwAAAFZCAYAAADU25efAAAKtGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU9kWhs+96SGBQADphN6ktwBSQmgBpHcLhCRAKCEEgogNkcERHAsiIqgM6ChFwbEAMhbEgm0QsIB1QAYBdRws2EB5F1iEmXnrvbfevutkf+vPPvvsc9Y9WTsAkGlsoTAVpgCQJsgShXi706KiY2i4MYAGFEAE+sCCzckUMoKC/AFi8/7v9uE+gGb8HdOZXP/+/X81WS4vkwMAFIRwPDeTk4bwKWRMcYSiLABQxxFdZ1WWcIbvIiwvQgpEeHSGE+d4aobjZxlNmY0JC2EirAsAnsRmixIBIFkgOi2bk4jkIc2sZSHg8gUI5yHskpaWzkW4HWFDJEaI8Ex+evxf8iT+LWe8JCebnSjhub3MGt6DnylMZa/+P4/jf1taqnh+DQNkkJJEPiGIV0LO7PeUdD8JC+IDAueZz52Nn+UksU/4PHMymTHznJkayppnLtvDT5InNcB/nhP4XpIYfhYrbJ55mZ6h8yxKD5GsmyBiMuaZLVqoQZwSLtGTeCxJ/tyksMh5zuZHBEhqSwn1W4hhSnSROESyF57A231hXS/JOaRl/mXvfJZkblZSmI/kHNgL9fMEjIWcmVGS2rg8D8+FmHBJvDDLXbKWMDVIEs9L9ZbomdmhkrlZyMu5MDdIcobJbN+geQYewBP4Iw8NMIE1sAFWyBMJPLN4OVkzm2GmC1eL+IlJWTQGcuN4NJaAY7aYZmVhZQvAzP2dez3e9c/eS0gRv6ClwQBYz9ypZQsaLx+As2+Qq7h7QdMNBkAmFoCLSzhiUfachp75wCC/CjJAHigDDaADDIEpUpkdcAJuSMW+IBCEgWiwEnBAEkgDIrAKrAUbQSEoBjvAblABqsBBUAuOgROgBZwFF8FVcBN0g3vgERgAw+AlGAcfwCQEQTiIDFEhZUgT0oNMICuIDrlAnpA/FAJFQ3FQIiSAxNBaaBNUDJVAFVA1VAf9DJ2BLkLXoR7oATQIjUFvoS8wCibB8rA6rA+bw3SYAfvBYfAKOBHOgHPhAngbXA7XwEfhZvgifBO+Bw/AL+EJFEBJoRRRWihTFB3FRAWiYlAJKBFqPaoIVYaqQTWi2lCdqDuoAdQr1Gc0Fk1F09CmaCe0DzoczUFnoNejt6Ir0LXoZvRl9B30IHoc/Q1DxqhhTDCOGBYmCpOIWYUpxJRhDmNOY65g7mGGMR+wWKwi1gBrj/XBRmOTsWuwW7H7sU3YdmwPdgg7gcPhlHEmOGdcII6Ny8IV4vbijuIu4Hpxw7hPeCm8Jt4K74WPwQvw+fgyfD3+PL4XP4KfJFAIegRHQiCBS1hN2E44RGgj3CYMEyaJskQDojMxjJhM3EgsJzYSrxAfE99JSUlpSzlIBUvxpfKkyqWOS12TGpT6TJIjGZOYpOUkMWkb6QipnfSA9I5MJuuT3cgx5CzyNnId+RL5KfmTNFXaTJolzZXeIF0p3SzdK/1ahiCjJ8OQWSmTK1Mmc1LmtswrCoGiT2FS2JT1lErKGUofZUKWKmspGyibJrtVtl72uuyoHE5OX85TjitXIHdQ7pLcEBVF1aEyqRzqJuoh6hXqsDxW3kCeJZ8sXyx/TL5LflxBTsFGIUIhR6FS4ZzCgCJKUV+RpZiquF3xhOJ9xS+L1BcxFvEWbVnUuKh30UclVSU3JZ5SkVKT0j2lL8o0ZU/lFOWdyi3KT1TQKsYqwSqrVA6oXFF5pSqv6qTKUS1SPaH6UA1WM1YLUVujdlDtltqEuoa6t7pQfa/6JfVXGooabhrJGqUa5zXGNKmaLpp8zVLNC5ovaAo0Bi2VVk67TBvXUtPy0RJrVWt1aU1qG2iHa+drN2k/0SHq0HUSdEp1OnTGdTV1l+qu1W3QfahH0KPrJent0evU+6hvoB+pv1m/RX/UQMmAZZBr0GDw2JBs6GqYYVhjeNcIa0Q3SjHab9RtDBvbGicZVxrfNoFN7Ez4JvtNehZjFjssFiyuWdxnSjJlmGabNpgOmima+Zvlm7WYvTbXNY8x32neaf7NwtYi1eKQxSNLOUtfy3zLNsu3VsZWHKtKq7vWZGsv6w3WrdZvbExseDYHbPptqbZLbTfbdth+tbO3E9k12o3Z69rH2e+z76PL04PoW+nXHDAO7g4bHM46fHa0c8xyPOH4p5OpU4pTvdPoEoMlvCWHlgw5azuznaudB1xoLnEuP7oMuGq5sl1rXJ+56bhx3Q67jTCMGMmMo4zX7hbuIvfT7h+Zjsx1zHYPlIe3R5FHl6ecZ7hnhedTL22vRK8Gr3FvW+813u0+GB8/n50+fSx1FodVxxr3tfdd53vZj+QX6lfh98zf2F/k37YUXuq7dNfSxwF6AYKAlkAQyArcFfgkyCAoI+iXYGxwUHBl8PMQy5C1IZ2h1NDY0PrQD2HuYdvDHoUbhovDOyJkIpZH1EV8jPSILIkciDKPWhd1M1olmh/dGoOLiYg5HDOxzHPZ7mXDy22XFy6/v8JgRc6K6ytVVqauPBcrE8uOPRmHiYuMq4+bYgeya9gT8az4ffHjHCZnD+cl141byh3jOfNKeCMJzgklCaOJzom7EseSXJPKkl7xmfwK/ptkn+Sq5I8pgSlHUqZTI1Ob0vBpcWlnBHKCFMHldI30nPQeoYmwUDiQ4ZixO2Nc5Cc6nAllrshszZJHGqVbYkPxd+LBbJfsyuxPqyJWncyRzRHk3FptvHrL6pFcr9yf1qDXcNZ0rNVau3Ht4DrGuur10Pr49R0bdDYUbBjO886r3UjcmLLx13yL/JL895siN7UVqBfkFQx95/1dQ6F0oaiwb7PT5qrv0d/zv+/aYr1l75ZvRdyiG8UWxWXFU1s5W2/8YPlD+Q/T2xK2dW23235gB3aHYMf9na47a0tkS3JLhnYt3dVcSistKn2/O3b39TKbsqo9xD3iPQPl/uWte3X37tg7VZFUca/SvbJpn9q+Lfs+7ufu7z3gdqCxSr2quOrLj/wf+6u9q5tr9GvKDmIPZh98fijiUOdP9J/qDqscLj789YjgyEBtSO3lOvu6unq1+u0NcIO4Yezo8qPdxzyOtTaaNlY3KTYVHwfHxcdf/Bz38/0Tfic6TtJPNp7SO7XvNPV0UTPUvLp5vCWpZaA1urXnjO+ZjjanttO/mP1y5KzW2cpzCue2nyeeLzg/fSH3wkS7sP3VxcSLQx2xHY8uRV26ezn4ctcVvyvXrnpdvdTJ6Lxwzfna2euO18/coN9ouWl3s/mW7a3Tv9r+errLrqv5tv3t1m6H7raeJT3ne117L97xuHP1LuvuzXsB93ruh9/v71veN9DP7R99kPrgzcPsh5OP8h5jHhc9oTwpe6r2tOY3o9+aBuwGzg16DN56Fvrs0RBn6OXvmb9PDRc8Jz8vG9EcqRu1Gj075jXW/WLZi+GXwpeTrwr/kP1j32vD16f+dPvz1njU+PAb0Zvpt1vfKb878t7mfcdE0MTTD2kfJj8WfVL+VPuZ/rnzS+SXkclVU7ip8q9GX9u++X17PJ02PS1ki9izrQAKGXBCAgBvjwBAjgaA2g0Acdlcfz1r0Nx/glkC/4nnevBZswPgaB4AIW4ABCK+CpH0kCHdDkAQooW5AdjaWjLme+HZvn3GKEcBcNd3D4n2f9LPAv+0uZ7+L3X/0wNJ1r/5fwHU+Qmhgs/Q2gAAAFZlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA5KGAAcAAAASAAAARKACAAQAAAABAAABXKADAAQAAAABAAABWQAAAABBU0NJSQAAAFNjcmVlbnNob3QbG7RbAAAB1mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zNDU8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MzQ4PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CswaBTMAAEAASURBVHgB7Z0HkFTVtoYXSE6XNGQkP8kIl1QKQyokClOSEbC49xYgl1CCBbz7HkGBQgQErvDIeQQkK0EBSZIk5yxJEAQkKJIfzJt/+/ap0z0zTHdPn9Dd/6rqOaHPOXvt75xes8/ae6+V6n//93/jhEICJEACJGA5gdSWl8ACSIAESIAEFAEaXD4IJEACJGATARpcm0CzGBIgARKgweUzQAIkQAI2EaDBtQk0iyEBEiABGlw+AyRAAiRgEwEaXJtAsxgSIAESSOMPgjVr1siyZcukWLFikjFjRn9O5bEkQAIkEHYEHj16JBcvXpTWrVtL8+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 + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index 14fb731..0ac6a60 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -84,8 +84,8 @@ "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" + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 # For cuda 12\n", + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 # For cuda 13\n" ] }, { diff --git a/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index 64c2719..ba6b24c 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -84,8 +84,8 @@ "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" + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 # For cuda 12\n", + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 # For cuda 13\n" ] }, { diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index a477af3..10ecb14 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -69,26 +69,11 @@ "
\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", @@ -105,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "c5735277", "metadata": {}, "outputs": [], @@ -115,7 +100,8 @@ "# 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 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 7e877b3..911b431 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -81,26 +81,11 @@ "
\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", @@ -117,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "01d94bc0", "metadata": {}, "outputs": [], @@ -127,7 +112,8 @@ "# 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 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/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index 8ab791a..a81fdf0 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -56,26 +56,11 @@ "
\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", @@ -92,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "e0ce3f89", "metadata": {}, "outputs": [], @@ -102,7 +87,8 @@ "# 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 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/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index 44119b3..ed959a8 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -85,26 +85,11 @@ "
\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", @@ -131,7 +116,8 @@ "# 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 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/01_optimization_with_cufolio.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/01_optimization_with_cufolio.ipynb index d0e30c3..1719643 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 @@ -52,30 +52,15 @@ "
\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()" + "check_gpu()\n" ] }, { @@ -90,7 +75,8 @@ "# 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", + "#!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 \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" diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index 034f457..9226333 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -53,30 +53,15 @@ "
\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()" + "check_gpu()\n" ] }, { @@ -91,7 +76,8 @@ "# 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", + "#!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\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" 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 a197309..6e21d56 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb @@ -57,30 +57,15 @@ "
\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()" + "check_gpu()\n" ] }, { @@ -95,7 +80,8 @@ "# 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", + "#!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\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" diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index a9818d5..967af0b 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -1,1133 +1,1118 @@ { - "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": [ + "cells": [ { - "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", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\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": 2, - "metadata": {}, - "outputs": [ + "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" + ] + }, { - "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": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Environment Setup and Installation\n", + "\n", + "### 1.1 Install Required Dependencies\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": null, + "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": [ + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\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", - "
" + "cell_type": "code", + "execution_count": null, + "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" + ] + } ], - "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]" + "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 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "\n", + "# For CUDA 13.x systems:\n", + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19\n", + "\n", + "# Install other dependencies\n", + "!pip install numpy pandas matplotlib seaborn scipy" ] - }, - "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": [ + }, { - "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": [ + "### 1.2 Import Required Libraries\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": 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)" + ] + }, { - "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": "markdown", + "metadata": {}, + "source": [ + "### 1.3 Configure Solver Settings\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": 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" + ] + }, { - "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": [ + "### 1.4 Load S&P 500 Data\n" + ] + }, { - "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+ewc3NDQYGBjAzM0OfPn0QHBycqW3JZDKMHDkyzWWp393bt29nZzcoC65evYrOnTvDysoK2trasLe3x5AhQ/D27VuVukX9HgAVTRpSB0BEBdemTZuQnJyMXr16KZXfunULjRo1Qv/+/aGjo4N79+5h8eLF8PT0xKVLlxRJhbt376Jhw4awtbXF7NmzIZfLsWbNGjRp0gQ3b95E+fLlAQAWFhbYvn27yvZPnTqFnTt3omXLlhnG+eHDB8ydOxf29vZwcXHJ1r56eHhg9erVRfZEISeOYVb06tULbdq0USqrV69erm3vxYsXSsmup0+fYu7cuWjatGm2n0LPTzZu3Kj01k9sbCzmzp0LACpPxM2YMQNTp07Ny/DyxJfHIDdkdFwzo2PHjjAyMsKaNWswb968HI6OiIiKinnz5qF06dKIj4/HlStXsHbtWnh4eODx48fQ09PLkW0sXLgQ3bp1Q6dOnZTKX716hTdv3mDjxo0YOHBgjmzrzJkzOdLOl7y8vHDr1i3Y29tj586dGDZsWK5sJ7esWbMG5ubmKm/FNm7cGHFxcdDS0pImsK+oWLGiyrXdtGnTYGBggJ9//jlPYwkJCcG8efNQqlQpVK1aNcOHPd6/f4/GjRvD2NgYCxcuRHR0NH799Vc8evQIN2/ezLfHOyP5va/ktFWrVmHMmDFwcHDAqFGjYGNjg2fPnuGvv/7C3r174eHhgfr16yvqF/V7AFQ0MVFARNm2efNmdOjQATo6OkrlV65cUanr6OiIiRMn4ubNm6hbty4AYObMmdDV1cX169dRrFgxAEDv3r1Rrlw5TJ8+HQcPHgQA6Ovro3fv3iptbtmyBUZGRmjfvn1O7xpJrHr16ml+5rlFW1s7z7YlBU1NzUzX1dDQgIZG4Ts9yMoxkIqamhq6deuGbdu2Ye7cuYVyCCgiIsp9rVu3Rs2aNQEAAwcORLFixbB8+XIcPXpU5QGfrBBCID4+Hrq6uunWCQoKAgCYmJhkeztfyq2bmDt27IClpSV+++03dOvWDb6+voXiARE1NTWV67P8xMrKSuU8f/HixTA3N8/T838AsLGxgb+/P6ytrXH79m3UqlUr3boLFy5ETEwM7ty5g1KlSgH49EbKd999hy1btmDw4MF5FXaOye99JS0ymQybN2/O8pC+V69exdixY9GwYUOcOnVKKWk6bNgwNGjQAN26dcOTJ09gamqaw1FnT0xMDPT19aUOg4oYDj1ERNni4+ODhw8fwtXVNVP1U0+6w8PDFWWXL1+Gq6urIkkAfDpZa9KkCY4fP57mUEWp/P39ceHCBXTp0iXDk5uLFy8qTvj69++veI3181dK9+/fjxo1akBXV1dxgurn56dY7u7ujtWrVwNAmq/C/vrrr6hfvz6KFSsGXV1d1KhRAwcOHMjUcUlLZts7e/YsGjZsCBMTExgYGKB8+fKYPn26Up1Vq1ahUqVK0NPTg6mpKWrWrIldu3Yp1fHz88NPP/2keP2yUqVK2LRpU6aPoZeXF7p27Qpra2vo6OigZMmS6NmzJyIiIrJ9DIBPJ0aJiYmZrn/s2DHIZDI8fPhQUXbw4EHIZDJ06dJFqW7FihXRo0cPxe+fj9e5ZcsWfP/99wCAZs2aKfb3yyeMrly5gtq1a0NHRwcODg7Ytm3bV2P8fDzL33//HXZ2dtDV1UWTJk3w+PFjlfrnz59Ho0aNoK+vDxMTE3Ts2BHPnj1TqhMVFYWxY8fC3t4e2trasLS0xHfffYe7d+8q6nw+Pr+vry8sLCwAQHEzWiaTKZ6U+XKOgsqVK6NZs2YqscnlcpQoUQLdunVTKluxYgUqVaoEHR0dWFlZYciQIQgLC8vwuHzLZwd8utBP/Q6bmZmhZ8+eePfunVKdtOYo+PjxI/r06QMjIyOYmJigX79+ePDgQbqvnfv5+aFTp04wMDCAhYUFJk6ciJSUFABfP64BAQHo378/SpYsCW1tbdjY2KBjx44qw1t99913ePPmTa4P80VEREVH8+bNAXw6fweA5ORk/PLLL3B0dFQMvTF9+nQkJCQorZc6L9Pp06dRs2ZN6OrqYv369ZDJZIiJicHWrVsV/79zd3eHu7s7mjRpAgD4/vvvIZPJlN6wy8x5TVrSmhMgKCgIAwYMgJWVFXR0dFC1alVs3bo1S8dl165d6NatG9q1awdjY2OVc+SsyGw8crkcK1euhLOzM3R0dGBhYQE3NzelIWc2b96M5s2bw9LSEtra2nByclIZGsne3h5PnjzBv//+q/gMUo9ReuPOf+2aB/jfvEwZnfPkldevX+P7779XDGdbt25dnDhxQqlO6r7u3bsX06dPh7W1NfT19dGhQweVc8G0aGtrw9raOlPxHDx4EO3atVMkCQDA1dUV5cqVw759+7K2c5mU3e+MEALz589HyZIloaenh2bNmuHJkycq9dLqK02bNkXlypXx9OlTNGvWDHp6eihRogSWLl2qsv6bN2/QoUMH6Ovrw9LSEuPGjcPp06dV2syt68Ws+OWXXyCTybB161aVN6scHR2xdOlS+Pv7Y/369QC+fg8g1YYNGxR/S2vVqoVbt26p1Hn+/Dm6desGMzMz6OjooGbNmjh27JhSndThp/79918MHz4clpaWKFmyJIDMXe8R5ZTC98ggEeWJa9euAfj05HdakpOTER4ejsTERDx+/BgzZsyAoaEhateuraiTkJCQ5hNJenp6ivVS3z740p49eyCXy/Hjjz9mGGfFihUxb948zJo1C4MHD0ajRo0AQPFK4ZYtW9C/f3/UqlULixYtQmBgIFauXImrV6/i3r17MDExwZAhQ/DhwwecPXs2zSGQVq5ciQ4dOuDHH39EYmIi9uzZg++//x7Hjx9H27ZtM4wvLZlp78mTJ2jXrh2qVKmCefPmQVtbG97e3rh69aqinY0bN2L06NHo1q0bxowZg/j4eDx8+BD//fcffvjhBwBAYGAg6tatqxgz08LCAidPnsSAAQMQGRmJsWPHZngMExMT0apVKyQkJGDUqFGwtraGn58fjh8/jvDwcBgbG2d5/4FPN1onTZoEmUyGGjVqYMGCBV8dYqphw4aQyWS4dOkSqlSpAuBTMkpNTU3pLZfg4GA8f/483TFCGzdujNGjR+OPP/7A9OnTUbFiRQBQ/BcAvL290a1bNwwYMAD9+vXDpk2b4O7ujho1aqBSpUpf3b9t27YhKioKI0aMQHx8PFauXInmzZvj0aNHsLKyAgB4enqidevWcHBwwJw5cxAXF4dVq1ahQYMGuHv3ruKm99ChQ3HgwAGMHDkSTk5O+PjxI65cuYJnz56l+f20sLDA2rVrMWzYMHTu3FlxIz71mH2pR48emDNnDgICApQupK5cuYIPHz6gZ8+eirIhQ4YovlOjR4+Gj48P/vzzT9y7dw9Xr15N96n+b/nsFixYgJkzZ6J79+4YOHAggoODsWrVKjRu3FjxHU6LXC5H+/btcfPmTQwbNgwVKlTA0aNH0a9fvzTrp6SkoFWrVqhTpw5+/fVXeHp64rfffoOjoyOGDRv21ePatWtXPHnyBKNGjYK9vT2CgoJw9uxZvH37VimBUaNGDQCfnnqqVq1amrEQERFlxatXrwBA8XDOwIEDsXXrVnTr1g0TJkzAf//9h0WLFuHZs2c4fPiw0rovXrxAr169MGTIEAwaNAjly5fH9u3bMXDgQNSuXVvxFLWjoyMAoESJEli4cCFGjx6NWrVqZfm8JjPi4uLQtGlTeHt7Y+TIkShdujT2798Pd3d3hIeHY8yYMV9t47///oO3tzc2b94MLS0tdOnSBTt37lR56Can4xkwYAC2bNmC1q1bY+DAgUhOTsbly5dx48YNxVsga9euRaVKldChQwdoaGjgn3/+wfDhwyGXyzFixAgAwIoVKzBq1Cil4XpSj3VaMnPNk+pr5zx5ITAwEPXr10dsbCxGjx6NYsWKYevWrejQoQMOHDiAzp07K9VfsGABZDIZpkyZgqCgIKxYsQKurq64f/9+hm/AZJafnx+CgoIUn9HnateuDQ8Pj0y1Ex8fj5CQEJXytB6Q+5bvzKxZszB//ny0adMGbdq0wd27d9GyZctMP4QVFhYGNzc3dOnSBd27d8eBAwcwZcoUODs7o3Xr1gA+PdTVvHlz+Pv7Y8yYMbC2tsauXbtw4cIFpbZy63oxK2JjY3Hu3Dk0atQIpUuXTrNOjx49MHjwYBw/fhxTp0796j0A4FOyMSoqCkOGDIFMJsPSpUvRpUsXvH79WnHd8+TJEzRo0AAlSpTA1KlToa+vj3379qFTp044ePCgSl8ePnw4LCwsMGvWLMTExADI+vUe0TcRRETZMGPGDAFAREVFpbn8+vXrAoDip3z58uLChQtKdZydnUW5cuVEcnKyoiwhIUGUKlVKABAHDhxId/s1atQQNjY2IiUl5aux3rp1SwAQmzdvVipPTEwUlpaWonLlyiIuLk5Rfvz4cQFAzJo1S1E2YsQIkd6fzNjYWJV2K1euLJo3b65UbmdnJ/r16/fVeDPT3u+//y4AiODg4HTb6dixo6hUqVKG2xowYICwsbERISEhSuU9e/YUxsbGiljSO4b37t0TAMT+/fu/ul+Z8ebNG9GyZUuxdu1acezYMbFixQpRqlQpoaamJo4fP/7V9StVqiS6d++u+L169eri+++/FwDEs2fPhBBCHDp0SAAQDx48UNT78rPZv3+/AKDSZ1PrAhCXLl1SlAUFBQltbW0xYcKEDOPz8fERAISurq54//69ovy///4TAMS4ceMUZS4uLsLS0lJ8/PhRUfbgwQOhpqYm+vbtqygzNjYWI0aMyHC7/fr1E3Z2dorfg4ODBQAxe/ZslbqzZ89W6usvXrwQAMSqVauU6g0fPlwYGBgo+sjly5cFALFz506leqdOnUqz/EvZ+ex8fX2Furq6WLBggVJbjx49EhoaGkrlXx6DgwcPCgBixYoVirKUlBTRvHlzlb7er18/AUDMmzdPaTvVqlUTNWrUUPye3nENCwsTAMSyZcsyPAaptLS0xLBhwzJVl4iIKNXmzZsFAOHp6SmCg4PFu3fvxJ49e0SxYsUU5x73798XAMTAgQOV1p04caIAIM6fP68oSz3nOXXqlMq29PX10zyvvXDhQprnhpk9r0ndBx8fH0VZkyZNRJMmTRS/r1ixQgAQO3bsUJQlJiaKevXqCQMDAxEZGfnVYzVy5Ehha2sr5HK5EEKIM2fOCADi3r17SvVyMp7z588LAGL06NEq8aTGIYTqtYAQQrRq1Uo4ODgolVWqVEkpjlSpn0HqeWxWrnkye86T077cl7FjxwoA4vLly4qyqKgoUbp0aWFvb6+4Bkzd1xIlSih97vv27RMAxMqVKzMdQ3rXO58v27Ztm8qySZMmCQAiPj4+w/Y/vzZO7+fWrVuK+tn9zgQFBQktLS3Rtm1bpX41ffp0AUDpe/tlXxHiU//+cl8TEhKEtbW16Nq1q6Lst99+EwDEkSNHFGVxcXGiQoUKSm3m9PViep9RRlL/7o0ZMybDelWqVBFmZmaK39O7B5B6TVesWDERGhqqKD969KgAIP755x9FWYsWLYSzs7NS/5DL5aJ+/fqibNmyirLUz7Fhw4ZK90eEyNz1HlFO4dBDRJQtHz9+hIaGBgwMDNJc7uTkhLNnz+LIkSOYPHky9PX1VZ6UGD58OF6+fIkBAwbg6dOnePz4Mfr27Qt/f38An57OScvLly9x584d9OzZU2kC2qy6ffs2goKCMHz4cKXhi9q2bYsKFSqovNqans+fUgkLC0NERAQaNWqU7VcBM9Ne6lM/R48eTXeCVhMTE7x//z7N1x+BT6+kHjx4EO3bt4cQAiEhIYqfVq1aISIi4qv7kPoEyOnTpxEbG5uV3UxTqVKlcPr0aQwdOhTt27fHmDFjcO/ePVhYWGDChAlfXb9Ro0a4fPkygE+vaD548ACDBw+Gubm5ovzy5cswMTFB5cqVsx2nk5OT4s0K4NNT+uXLl8fr168ztX6nTp1QokQJxe+1a9dGnTp1FE8j+fv74/79+3B3d4eZmZmiXpUqVfDdd98pPbVkYmKC//77Dx8+fMj2/mSkXLlycHFxwd69exVlKSkpOHDgANq3b6/or/v374exsTG+++47pb5Uo0YNGBgYqDxd9KXsfHaHDh2CXC5H9+7dlbZpbW2NsmXLZrjNU6dOQVNTE4MGDVKUqampKZ7US8vQoUNVYs7MZ66rqwstLS1cvHjxq8MwAYCpqWmaT5sRERFlhqurKywsLGBra4uePXvCwMAAhw8fRokSJRTnEOPHj1daJ/U868vz39KlS6NVq1bfFE9Wzmsyw8PDA9bW1krzLWhqamL06NGIjo7Gv//+m+H6ycnJ2Lt3L3r06KEYSiR1qJ+dO3dmKZasxJM6rOLs2bNV2vh8SJPPrwUiIiIQEhKCJk2a4PXr19kaqiU71zzZPefJKR4eHqhduzYaNmyoKDMwMMDgwYPh6+uLp0+fKtXv27cvDA0NFb9369YNNjY2We5b6Um9Lk1rXrPUY5retevnOnbsiLNnz6r8TJo0Sanet3xnPD09kZiYiFGjRin1q7Fjx341vlQGBgZK80VoaWmhdu3aSn3g1KlTKFGiBDp06KAo09HRUTq3Br7tejE2NlbpHD/1/Dg6Olqp7Gvn11FRUQCg1EfSYmhoiMjIyEzH16NHD6X5DFKvD1OPU2hoKM6fP4/u3bsjKipKEe/Hjx/RqlUreHl5qQz/NWjQIKirqyuV5fb1HtHnmCggolxhZGQEV1dXdOzYEUuWLMGECRPQsWNHPHjwQFFn6NChmD59Onbt2oVKlSrB2dkZr169wuTJkwEg3SRE6gn814Yd+po3b94AAMqXL6+yrEKFCorlX3P8+HHUrVsXOjo6MDMzUwxBkt0xFzPTXo8ePdCgQQMMHDgQVlZW6NmzJ/bt26eUNJgyZQoMDAxQu3ZtlC1bFiNGjFAamig4OBjh4eHYsGEDLCwslH769+8P4H8T0qWndOnSGD9+PP766y+Ym5ujVatWWL16dY6ON2lmZob+/fvjxYsXeP/+fYZ1GzVqBH9/f3h7e+PatWuQyWSoV6+e0k3oy5cvo0GDBt+UZPp8bNJUpqammboJDABly5ZVKStXrpxivPqM+mbFihUREhKieBV16dKlePz4MWxtbVG7dm3MmTMnxy/kevTogatXrypOZC9evIigoCCluQK8vLwQEREBS0tLlf4UHR391b6Unc/Oy8sLQgiULVtWZZvPnj3LcJtv3ryBjY2NyhilZcqUSbN+6jjCn8vsZ66trY0lS5bg5MmTsLKyQuPGjbF06VIEBASkWV8IwYmMiYgo21avXo2zZ8/iwoULePr0KV6/fq242f/mzRuoqamp/P/O2toaJiYmKue/6Q3TkRVZOa/JbHtly5ZVOZdLHSbya+fwZ86cQXBwMGrXrg1vb294e3vDx8cHzZo1w+7du9N9COdb43n16hWKFy+udOM3LVevXoWrq6tiXHoLCwvFkEjZOcfO6jVPds95IiIiEBAQoPgJDQ3Ncqyfx5xef0ld/rkvz61lMhnKlCmjMhdUdqUmb76cxwP4NJzQ53UyUrJkSbi6uqr8ODk5KdX7lu9M6rpfHhMLC4tMT9JbsmRJlXPRL/vAmzdv4OjoqFLvy78t33K9uHTpUpVzfAAYNWqUUtnXhutMTRCkJgzSExUV9dVkwue+vCZMPb6px8nb2xtCCMycOVNlP1IThl9er6T1NzcvrveIUnGOAiLKlmLFiiE5OTnT/zPt0qUL+vTpgz179qBq1aqK8gULFmDixIl48uQJjI2N4ezsrDgRLleuXJpt7dq1C+XLl1eM5S2ly5cvo0OHDmjcuDHWrFkDGxsbaGpqYvPmzdmaEC2z7enq6uLSpUu4cOECTpw4gVOnTmHv3r1o3rw5zpw5A3V1dVSsWBEvXrzA8ePHcerUKRw8eBBr1qzBrFmzMHfuXMVFUO/evdMdlz29ces/99tvv8Hd3R1Hjx7FmTNnMHr0aCxatAg3btxQTMD0rWxtbQF8eiojozZTnzq6dOkSXr9+jerVq0NfXx+NGjXCH3/8gejoaNy7dw8LFiz4pni+fMojlRDim9rNju7du6NRo0Y4fPgwzpw5g2XLlmHJkiU4dOiQYgzRb9WjRw9MmzYN+/fvx9ixY7Fv3z4YGxvDzc1NUUcul2f4JN6XF5xfys5nJ5fLIZPJcPLkyTQ/k/SSjdmR3meeWWPHjkX79u1x5MgRnD59GjNnzsSiRYtw/vx5lYub8PBwmJubf9P2iIio6Kpdu3aaY6l/LrMJ6ZwY3z2/ST1X6d69e5rL//33XzRr1iwvQ1J49eoVWrRogQoVKmD58uWwtbWFlpYWPDw88Pvvv2c5iZEd2T3nGTNmjNIEzk2aNFGZULmgsrGxAQDFm++f8/f3h5mZWZpvGxRUOX2tk93rxb59+yq9VQIA3333HSZNmqQ0f93X/k6VKVMGGhoaePjwYbp1EhIS8OLFi6/+7fzc145T6vd14sSJ6b6Z9WViJa19yYvrPaJUTBQQUbZUqFABAODj45Opm8kJCQmQy+VpPjlgamqqdALg6emJkiVLKrbxudSJx+bNm5fpWNO7ELKzswPwaZK25s2bKy178eKFYnlGbRw8eBA6Ojo4ffq00snh5s2bMx1fdttTU1NDixYt0KJFCyxfvhwLFy7Ezz//jAsXLsDV1RUAoK+vjx49eqBHjx5ITExEly5dsGDBAkybNg0WFhYwNDRESkqKon56vnYx6ezsDGdnZ8yYMQPXrl1DgwYNsG7dOsyfPz8bR0FV6hMTX7vZXKpUKZQqVQqXL1/G69evFa9/Nm7cGOPHj8f+/fuRkpKCxo0bZ9hObj/N7eXlpVL28uVLxaRkn/fNLz1//hzm5ubQ19dXlNnY2GD48OEYPnw4goKCUL16dSxYsCDdE8es7l/p0qVRu3Zt7N27FyNHjsShQ4fQqVMnpT7q6OgIT09PNGjQIFs3FbLz2Tk6OkIIgdKlS6ebWEyPnZ0dLly4gNjYWKW3Cry9vbMce6qvHVdHR0dMmDABEyZMgJeXF1xcXPDbb79hx44dijp+fn5ITExUmjybiIgop9jZ2UEul8PLy0vp/zWBgYEIDw9XOv/NSFbOJbJ6XpOZ9h4+fAi5XK70FP/z58+VtpeWmJgYHD16FD169EC3bt1Ulo8ePRo7d+7MUqIgs/E4Ojri9OnTCA0NTfetgn/++QcJCQk4duyY0tPKaQ2nmNnPICvXPN9i8uTJSsPVZPbp9bTY2dml219Sl3/uy3NrIQS8vb0zdZ2aGSVKlICFhQVu376tsuzmzZtwcXHJke2k+pbvTOq6Xl5ecHBwUJQHBwdn+u3nzMb49OlTlTdh0zuXzs71ooODg9I+pHJycvrq9evn9PX10axZM5w/fx5v3rxJs8/v27cPCQkJaNeunaLsW68JU2PX1NTMUrxpyer1HlF2ceghIsqWevXqAYDKyVJ4eDiSkpJU6v/1118A8NUM/d69e3Hr1i2MHTs2zaFhUp+q/+GHHzIda+pJVHh4uFJ5zZo1YWlpiXXr1im9Rnry5Ek8e/YMbdu2/Wob6urqkMlkSElJUZT5+vriyJEjmY4vO+2l9Spv6glq6r58/PhRabmWlhacnJwghEBSUhLU1dXRtWtXHDx4EI8fP1ZpLzg4WPHv9PY/MjISycnJSmXOzs5QU1NL89Xcr/l8m6n8/PywadMmVKlSRfE0T0YaNWqE8+fP4+bNm4qbzS4uLjA0NMTixYuhq6v71bdR0tvfnHLkyBGl8Shv3ryJ//77T3GiZ2NjAxcXF2zdulUphsePH+PMmTNo06YNgE9zBXyZfLO0tETx4sUzPP6pN8azsn89evTAjRs3sGnTJoSEhCgNOwR8etIlJSUFv/zyi8q6ycnJmdpWVj+7Ll26QF1dHXPnzlV5wkkIofId+FyrVq2QlJSEjRs3KsrkcjlWr1791TjTk95xjY2NVbyWnsrR0RGGhoYqn9OdO3cAAPXr1892HEREROlJPYdYsWKFUvny5csBQOn8NyP6+vqZPo/I7HlNZrVp0wYBAQFK8yclJydj1apVMDAwQJMmTdJd9/Dhw4iJicGIESPQrVs3lZ927drh4MGDWTqPzWw8Xbt2hRACc+fOVWkj9Twm9Qnlz89rIiIi0nxoKLOfQVaueb5F6s3b1J9vefu7TZs2uHnzJq5fv64oi4mJwYYNG2Bvb68yVM+2bduUhpU5cOAA/P39c/QmateuXXH8+HG8e/dOUXbu3Dm8fPkS33//fY5tB/i274yrqys0NTWxatUqpX705Xf+W7Vq1Qp+fn44duyYoiw+Pl7p3BrI+evF7JoxYwaEEHB3d1eZT8LHxweTJ0+GjY0NhgwZoij/1mtCS0tLNG3aFOvXr0/zbZS0rn2/lN3rPaLs4hsFRJQtDg4OqFy5Mjw9PfHTTz8pyi9evIjRo0ejW7duKFu2LBITE3H58mUcOnQINWvWVHrK5NKlS5g3bx5atmyJYsWK4caNG9i8eTPc3NwwZswYlW2mpKRg7969qFu3LhwdHTMdq6OjI0xMTLBu3ToYGhpCX18fderUQenSpbFkyRL0798fTZo0Qa9evRAYGIiVK1fC3t4e48aNU7SReqI7evRotGrVCurq6ujZsyfatm2L5cuXw83NDT/88AOCgoKwevVqlClTJsNXG9OT2fbmzZuHS5cuoW3btrCzs0NQUBDWrFmDkiVLKt7OaNmyJaytrdGgQQNYWVnh2bNn+PPPP9G2bVvFcFGLFy/GhQsXUKdOHQwaNAhOTk4IDQ3F3bt34enpqUhIpHcMHzx4gJEjR+L7779HuXLlkJycjO3btyuSEKnmzJmDuXPn4sKFC2jatGm6+z958mTFK9fFixeHr68v1q9fj5iYGKxcuTJTx7BRo0bYuXMnZDKZ4lioq6ujfv36OH36NJo2bQotLa0M23BxcYG6ujqWLFmCiIgIaGtrKya5ywllypRBw4YNMWzYMCQkJGDFihUoVqyYYn4OAFi2bBlat26NevXqYcCAAYiLi8OqVatgbGyMOXPmAPg0jmbJkiXRrVs3VK1aFQYGBvD09MStW7fw22+/pbt9XV1dODk5Ye/evShXrhzMzMxQuXLlDCd47t69OyZOnIiJEyfCzMxM5amYJk2aYMiQIVi0aBHu37+Pli1bQlNTE15eXti/fz9WrlyZ5pN7n8vqZ+fo6Ij58+dj2rRp8PX1RadOnWBoaAgfHx8cPnwYgwcPxsSJE9PcVqdOnVC7dm1MmDAB3t7eqFChAo4dO6bo89l5gii945qcnIwWLVqge/fucHJygoaGBg4fPozAwED07NlTqY2zZ8+iVKlSXx1rlYiIKDuqVq2Kfv36YcOGDQgPD0eTJk1w8+ZNbN26FZ06dcr0k/Q1atSAp6cnli9fjuLFi6N06dKoU6dOuvUzc16TWYMHD8b69evh7u6OO3fuwN7eHgcOHMDVq1exYsWKDIdF3blzJ4oVK5ZuQr5Dhw7YuHEjTpw4gS5duuRoPM2aNUOfPn3wxx9/wMvLC25ubpDL5bh8+TKaNWuGkSNHomXLltDS0kL79u0xZMgQREdHY+PGjbC0tFS50VijRg2sXbsW8+fPR5kyZWBpaanyxgDw6WnmzF7z5BdTp07F7t270bp1a4wePRpmZmbYunUrfHx8cPDgQZUHyszMzNCwYUP0798fgYGBWLFiBcqUKaMysW5a/vzzT4SHhysmiv3nn38U86KNGjVKMRnv9OnTsX//fjRr1gxjxoxBdHQ0li1bBmdnZ8X8bjkpu98ZCwsLTJw4EYsWLUK7du3Qpk0b3Lt3DydPnszRoS2HDBmCP//8E7169cKYMWNgY2ODnTt3KiZ3Tj2XPn/+fKauF3Nb48aN8euvv2L8+PGoUqUK3N3dYWNjg+fPn2Pjxo2Qy+Xw8PBQehMmvXsAWbF69Wo0bNgQzs7OGDRoEBwcHBAYGIjr16/j/fv3SnM4piW713tE2SaIiLJp+fLlwsDAQMTGxirKvL29Rd++fYWDg4PQ1dUVOjo6olKlSmL27NkiOjpaaX1vb2/RsmVLYW5uLrS1tUWFChXEokWLREJCQprbO3XqlAAg/vjjjyzHevToUeHk5CQ0NDQEALF582bFsr1794pq1aoJbW1tYWZmJn788Ufx/v17pfWTk5PFqFGjhIWFhZDJZOLzP59///23KFu2rGIfNm/eLGbPni2+/BNrZ2cn+vXr99VYM9PeuXPnRMeOHUXx4sWFlpaWKF68uOjVq5d4+fKlos769etF48aNRbFixYS2trZwdHQUkyZNEhEREUrbCwwMFCNGjBC2trZCU1NTWFtbixYtWogNGzZ89Ri+fv1a/PTTT8LR0VHo6OgIMzMz0axZM+Hp6am07oQJE4RMJhPPnj3LcN937dolGjduLCwsLISGhoYwNzcXnTt3Fnfu3PnqcUv15MkTAUBUrFhRqXz+/PkCgJg5c6bKOml9Nhs3bhQODg5CXV1dABAXLlxQ1G3btq1KG02aNBFNmjTJMDYfHx8BQCxbtkz89ttvwtbWVmhra4tGjRqJBw8eqNT39PQUDRo0ELq6usLIyEi0b99ePH36VLE8ISFBTJo0SVStWlUYGhoKfX19UbVqVbFmzRqldvr16yfs7OyUyq5duyZq1KghtLS0BAAxe/ZsIYRIs++matCggQAgBg4cmO4+btiwQdSoUUPo6uoKQ0ND4ezsLCZPniw+fPiQ4bERInufnRBCHDx4UDRs2FDo6+sLfX19UaFCBTFixAjx4sWLDI9BcHCw+OGHH4ShoaEwNjYW7u7u4urVqwKA2LNnj9K6+vr6KttN61ildVxDQkLEiBEjRIUKFYS+vr4wNjYWderUEfv27VNaNyUlRdjY2IgZM2Z89VgRERF9afPmzQKAuHXrVob1kpKSxNy5c0Xp0qWFpqamsLW1FdOmTRPx8fFK9dI75xFCiOfPn4vGjRsLXV1dAUBxHnXhwgUBQOzfv19lna+d13y+Dz4+PoqytM6xAgMDRf/+/YW5ubnQ0tISzs7OSuf3aQkMDBQaGhqiT58+6daJjY0Venp6onPnzrkST3Jysli2bJmoUKGC0NLSEhYWFqJ169ZK57rHjh0TVapUETo6OsLe3l4sWbJEbNq0SSWOgIAA0bZtW2FoaCgAKGJK/QxSz11TZeaaJyvnPDmpUqVKKsf01atXolu3bsLExETo6OiI2rVri+PHjyvVSd3X3bt3i2nTpglLS0uhq6sr2rZtK968eZOpbdvZ2QkAaf58fryFEOLx48eiZcuWQk9PT5iYmIgff/xRBAQEZGo7AMSIESPSXJbedze735mUlBQxd+5cYWNjI3R1dUXTpk3F48ePVa550uorTZo0EZUqVVKJMa1z6devX4u2bdsKXV1dYWFhISZMmCAOHjwoAIgbN24o6mTmejGzvryWz6pLly6Jjh07CnNzc6GpqSlKlSolBg0aJHx9fVXqpncP4PNrurTiS72uSvXq1SvRt29fYW1tLTQ1NUWJEiVEu3btxIEDBxR10usDmb3eI8opMiEkmHmRiAqFiIgIODg4YOnSpRgwYIDU4VA+Vrt2bdjZ2WH//v1ShyIpX19flC5dGsuWLUv3SXeS1pEjR9C5c2dcuXIFDRo0yPNt//DDD3j16lWmhtkiIiIiKsouXryIZs2aYf/+/V99c5XyxooVKzBu3Di8f/8eJUqUkDocIsoizlFARNlmbGyMyZMnY9myZZDL5VKHQ/lUZGQkHjx4kKUJqInywpfjk6akpGDVqlUwMjJC9erV8zyeJUuWYOTIkUwSEBEREVG+9+W5dHx8PNavX4+yZcsySUBUQHGOAiL6JlOmTMGUKVOkDoPyMSMjI060RPnSqFGjEBcXh3r16iEhIQGHDh3CtWvXsHDhQujq6uZ5PJ9P2EdERERElJ916dIFpUqVgouLCyIiIrBjxw48f/4cO3fulDo0IsomJgqIiIioSGrevDl+++03HD9+HPHx8ShTpgxWrVqFkSNHSh0aEREREVG+1qpVK/z111/YuXMnUlJS4OTkhD179qBHjx5Sh0ZE2cQ5CoiIiIiIiIiIiIiIijDOUUBEREREREREREREVIQxUUBEREREREREREREVIRxjoI0yOVyfPjwAYaGhpDJZFKHQ0REREREnxFCICoqCsWLF4eaGp99IiIiIiL6VkwUpOHDhw+wtbWVOgwiIiIiIsrAu3fvULJkSanDICIiIiIq8JgoSIOhoSGATxceRkZGEkdTuMnlcgQHB8PCwoJPg1GmsM9QdrDfUFaxz1B2sN/kncjISNja2irO24mIiIiI6NswUZCG1OGGjIyMmCjIZXK5HPHx8TAyMuIFNWUK+wxlB/sNZRX7DGUH+03e4zChREREREQ5g1cwRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFRERERERERERERERFGBMFlGO8vLxQv359lCtXDrVq1cKTJ09U6ly/fh0uLi5wcXFBpUqVMHToUCQkJKS5bMiQIYplAPD333+jbNmycHR0xKBBg5CUlJRn+0ZERERERERERERUWDFRQDlmyJAhGDx4MF6+fIkpU6bA3d1dpU7VqlVx69Yt3L9/H48ePUJQUBC2bNmS7rI1a9YAAHx8fDBz5kxcvnwZ3t7eCAwMxIYNG/Jw74iIiIiIiIiIiIgKJyYKKEcEBQXh9u3b6N27NwCga9euePfuHby9vZXq6enpQVNTEwCQmJiIuLg4yGSyry47cOAAOnToAGtra8hkMgwdOhS7d+/Oq90jIiIiIiIiIiIiKrSYKKAc8e7dO9jY2EBDQwMAIJPJUKpUKbx9+1alrq+vL6pWrQpzc3MYGxsrvXnw5bLhw4cDAN6+fQs7OztFPXt7+zTbJiIiIiIiIiIiIqKsYaKA8py9vT0ePHiAgIAAJCQkwMPDI91lhw4dkjBSIiIiIiIiIiIiosKPiQLKEba2tvD390dycjIAQAiBt2/folSpUumuY2BggB49eqSZDDAwMEDPnj2xc+dOAECpUqXw5s0bxXJfX98M2yYiIiIiIiIiIiKizGGigHKEpaUlqlevjh07dgAADh48iJIlS6JMmTJK9by9vZGUlATg0zwER44cQcWKFdNcdvjwYVSpUgXApzkPjh07hoCAAAghsG7dOvTs2TOvdo+IiIiIiIiIiIio0GKigHLM+vXrsX79epQrVw6LFy/G5s2bAQADBw7EsWPHAADnz59HtWrVULVqVVSrVg1WVlYYN25custmzpwJAHBwcMDcuXPRoEEDlClTBhYWFhgyZIg0O0pERERERERERERUiMiEEELqIPKbyMhIGBsbIyIiAkZGRlKHU6jJ5XIEBQXB0tISamrMW9HXsc9QdrDfUFaxz1B2sN/kHZ6vExERERHlLF7BEBEREREREREREREVYUwUEBEREREREREREREVYUwUEBEREREREREREREVYRpSB0AZs596QuoQcpUaBCqaCjwLk0EOmdTh5ArfxW2lDoGIiIiIiIiIiIgoXXyjgIiIiIiIiIiIiIioCGOigIiIiIiIiIiIiIioCGOigIiIiIiIiIiIiIioCGOigIiIiIiIiIiIKIuaNm0KbW1tGBgYwNDQEJUqVcL+/fsBAL6+vpDJZAgPD1fU37hxI0xNTXHx4kUAgEwmg62tLeLj4xV1jhw5Ant7e6XtPH78GN27d4elpSUMDAzg6OgId3d3PHr0KLd3kYiKECYKiEgyXl5eqF+/PsqVK4datWrhyZMnKnWuX78OFxcXuLi4oFKlShg6dCgSEhIAAOfPn0ft2rXh5OSESpUqYfLkyZDL5QCA6OhotGrVCubm5jAxMcnL3SIiIiIiIqIiYsmSJYiOjkZkZCSWLl2KH3/8EW/evEmz3s8//wxPT080bdpUUR4XF4dVq1al2/6dO3cU18337t1DdHQ0bt26hcaNG+PkyZO5sUtEVEQxUUBEkhkyZAgGDx6Mly9fYsqUKXB3d1epU7VqVdy6dQv379/Ho0ePEBQUhC1btgAATE1NsWfPHjx9+hR37tzBtWvXsG3bNgCApqYmpkyZAk9PzzzcIyIiIiIiIiqKZDIZ2rZtCxMTE7x48UJp2ZQpU/Dnn3/i0qVLqFGjhtKy6dOnY9GiRUpvHnxuwoQJ6NWrF+bPn48SJUoAAMzMzPDTTz9h8uTJubIvRFQ0MVFARJIICgrC7du30bt3bwBA165d8e7dO3h7eyvV09PTg6amJgAgMTERcXFxkMlkAIBq1arBwcEBAKCjowMXFxf4+voCALS1tdG8eXO+TUBERERERES5Ti6X4+jRo4iLi4OLi4uifOjQoTh8+DCuXr2KChUqqKzXvHlz1KpVC0uWLFFZFhsbi8uXL6NHjx65GToREQAmCohIIu/evYONjQ00NDQAfHr6olSpUnj79q1KXV9fX1StWhXm5uYwNjZO882DgIAAHDhwAO3atcvt0ImIiIiIiIgAANOmTYOJiQn09fXRpUsXzJgxA5aWlorlHh4eaNeuHUqVKpVuG4sXL8aqVavw4cMHpfKwsDDI5XIUL15cUbZ582aYmJjA0NAQderUyfkdIqIii4kCIsr37O3t8eDBAwQEBCAhIQEeHh5KyyMjI9G+fXtMnjwZNWvWlChKIiIiIiIiKmpShw2Ki4vDixcvsHXrVqxfv16x/J9//sH27dvx888/p9tGtWrV0KFDB8ydO1ep3NTUFGpqakoJhP79+yM8PByrVq1SzN9HRJQTmCggIknY2trC398fycnJAAAhBN6+fZvhUxYGBgbo0aMHDh06pCiLioqCm5sbOnbsiPHjx+d63ERERERERERpKVOmDNq0aYPjx48ryqpWrYrz589j48aNmDp1arrrzp8/Hzt27MDLly8VZXp6emjQoAH27duXq3ETEQFMFBCRRCwtLVG9enXs2LEDAHDw4EGULFkSZcqUUarn7e2NpKQkAJ/mKDhy5AgqVqwIAIiOjoabmxvc3NwwY8aMvN0BIiIiIiIios/4+vrCw8MDzs7OSuXOzs64cOECNm/enO4ExA4ODvjpp5+wdOlSpfJff/0VO3fuxKxZsxRvFkRERODu3bu5sxNEVGQxUUBEklm/fj3Wr1+PcuXKYfHixdi8eTMAYODAgTh27BgA4Pz586hWrRqqVq2KatWqwcrKCuPGjQMArFy5Ejdv3sShQ4fg4uICFxcXLFiwQNF+lSpVUK9ePURGRqJkyZLo06dP3u8kERERERERFVpTpkyBgYEBDAwM0LBhQ7i6umLWrFkq9SpVqoSLFy9i+/btmDBhQpptzZw5E4mJiUpltWvXxtWrV/HkyRNUqVIFhoaGqFGjBsLDw7F9+/Zc2SciKppkQgghdRD5TWRkJIyNjREREQEjIyNJY7GfekLS7ec2NQhUNBV4FiaDHDKpw8kVvovbSh1CoSKXyxEUFARLS0uoqTHXSZnDfkNZxT5D2cF+k3fy0/k6EREREVFhwCsYIiIiIiIiIiIiIqIijIkCIiIiIiIiIiIiIqIijIkCIiIiIiIiIiIiIqIiTEPqAIgo5xXmuS2KwrwWAOe2ICIiIiIiIiKivMM3CoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiChfadq0KWQyGTw9PZXKly1bBplMhrFjxwIAZDIZbG1tER8fr6hz5MgR2NvbK3739/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": [ - "
" + "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" ] - }, - "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" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Data Preprocessing and Return Calculation\n" + ] + }, { - "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" - ] + "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" + ] + }, + { + "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" + ] + }, + { + "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": [ + { + "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": [ + { + "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": [ + { + "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": [ + { + "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" + ] + }, + { + "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" + }, + "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": [ - "# 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" }, - "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 + "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 482731f..6d81e9f 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -56,26 +56,11 @@ "
\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", @@ -101,7 +86,9 @@ "\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 install --upgrade --extra-index-url https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "\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\n" ] }, { diff --git a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index 1961d09..080860c 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -77,26 +77,11 @@ "
\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", @@ -122,7 +107,9 @@ "\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 install --upgrade --extra-index-url https://pypi.nvidia.com cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", + "\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-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index cc58412..c2f2886 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -63,30 +63,17 @@ "
\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()" + "check_gpu()\n", + "\n", + "\n" ] }, { @@ -108,7 +95,8 @@ "\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 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\n" ] }, { diff --git a/sample_lp_sever_notebooks/linear-programming.ipynb b/sample_lp_sever_notebooks/linear-programming.ipynb index 33e4b56..60beb0e 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -63,26 +63,11 @@ "
\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", @@ -108,7 +93,8 @@ "\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 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\n" ] }, { 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 0986d75..f8d18ce 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 @@ -64,26 +64,11 @@ "
\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", @@ -109,7 +94,8 @@ "\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 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\n" ] }, { diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb index 4f3da8f..0de6a2f 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -64,30 +64,15 @@ "
\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()" + "check_gpu()\n" ] }, { @@ -110,7 +95,8 @@ "\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 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\n" ] }, { diff --git a/workforce_optimization/workforce_optimization_milp.ipynb b/workforce_optimization/workforce_optimization_milp.ipynb index 5b958ba..8696519 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -1,845 +1,830 @@ { - "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": null, - "metadata": {}, - "outputs": [ + "cells": [ { - "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": [ - "import subprocess\n", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\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": 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": [ + "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)." + ] + }, { - "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": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Environment Setup\n", + "\n", + "First, let's check if we have a GPU available and install necessary dependencies.\n" + ] + }, { - "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": [ + "cell_type": "code", + "execution_count": null, + "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": [ + "import subprocess\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{output.splitlines()[2]}
\n", + "
\n", + " \"\"\"))\n", + " except Exception:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\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", + " \"\"\"))\n", + "\n", + "check_gpu()\n" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import Required Libraries\n" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Data Setup\n", + "\n", + "Define the shift requirements, worker pay rates, and availability constraints.\n" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "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" + ] + }, { - "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": [ + "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" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "No optimal solution found. Status: Infeasible\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" } - ], - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } From f720e5e5c79e311b04299e90a270c6b3fbd58169 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 13:23:07 -0500 Subject: [PATCH 4/8] address review comments --- .../trnsport_cuopt.ipynb | 25 +- .../Production_Planning_Example_Pulp.ipynb | 423 ++-- PuLP_integration_example/Simple_LP_pulp.ipynb | 369 +-- .../Simple_MIP_pulp.ipynb | 377 +-- PuLP_integration_example/Sudoku_pulp.ipynb | 519 ++-- diet_optimization/diet_optimization_lp.ipynb | 8 +- .../diet_optimization_milp.ipynb | 8 +- ...t_matrix_and_waypoint_graph_creation.ipynb | 8 +- .../intra-factory_transport.ipynb | 23 +- .../cvrp_daily_deliveries.ipynb | 23 +- .../cvrptw_benchmark_gehring_homberger.ipynb | 23 +- .../cvrptw_service_team_routing.ipynb | 23 +- .../CVaR/01_optimization_with_cufolio.ipynb | 25 +- .../CVaR/02_backtesting.ipynb | 25 +- .../CVaR/03_advanced_topics.ipynb | 25 +- .../cvar_portfolio_optimization.ipynb | 2211 +++++++++-------- .../cvrptw_benchmark_gehring_homberger.ipynb | 23 +- .../cvrptw_service_team_routing.ipynb | 23 +- .../linear-programming-with-datamodel.ipynb | 27 +- .../linear-programming.ipynb | 23 +- ...er-linear-programming-with-datamodel.ipynb | 23 +- .../mixed-integer-linear-programming.ipynb | 25 +- .../workforce_optimization_milp.ipynb | 1641 ++++++------ 23 files changed, 3122 insertions(+), 2778 deletions(-) diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index 64e0e55..831c571 100644 --- a/GAMSPy_integration_example/trnsport_cuopt.ipynb +++ b/GAMSPy_integration_example/trnsport_cuopt.ipynb @@ -65,27 +65,44 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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()\n" + "check_gpu()" ] }, { diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index a100095..b92d1f6 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -1,206 +1,223 @@ { - "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." - ] - }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "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", - "# For cuda-12\n", - "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", - "\n", - "# For cuda-13\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" - } + "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." + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "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": { + "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", + "# For cuda-12\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "\n", + "# For cuda-13\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 6982457..223a6a7 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -1,179 +1,196 @@ { - "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" - ] - }, - { - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "QSq2W3W7ojKI" - }, - "outputs": [], - "source": [ - "!pip install pulp==3.2.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", - "\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" - } + "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" + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "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": { + "id": "QSq2W3W7ojKI" + }, + "outputs": [], + "source": [ + "!pip install pulp==3.2.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", + "\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 e6de08c..c199884 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -1,183 +1,200 @@ { - "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": {}, - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "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 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" - } + "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", + "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": { + "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 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 5c0be84..0c190e2 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -1,254 +1,271 @@ { - "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": {}, - "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=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "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 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" - } + "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", + "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": { + "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 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 0ac6a60..b990eb9 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -38,14 +38,16 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\n", + " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", diff --git a/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index ba6b24c..93d15ef 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -38,14 +38,16 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\n", + " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", 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 ea1d3a0..24bfdbb 100644 --- a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb +++ b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb @@ -36,14 +36,16 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\n", + " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index 10ecb14..117a410 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -57,23 +57,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", diff --git a/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index 911b431..8d884a8 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -69,23 +69,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", diff --git a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index a81fdf0..7c66b3a 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -44,23 +44,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", diff --git a/last_mile_delivery/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index ed959a8..db3c47f 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -73,23 +73,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", 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 1719643..a7bbe80 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 @@ -40,27 +40,44 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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()\n" + "check_gpu()" ] }, { diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index 9226333..d6bee68 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -41,27 +41,44 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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()\n" + "check_gpu()" ] }, { 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 6e21d56..8248388 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb @@ -45,27 +45,44 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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()\n" + "check_gpu()" ] }, { diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index 967af0b..fb46e48 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -1,1118 +1,1135 @@ { - "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": [ - { - "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", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "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 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", - "\n", - "# For CUDA 13.x systems:\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19\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", + "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": [ { - "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": [ + "# 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 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "\n", + "# For CUDA 13.x systems:\n", + "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19\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": [ { - "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/v77b0RERMDY2FhRfvXqVWhoaODWrVtISkqCpqam0jJ1dXXUq1cv09uxs7NDXFycUju5wcPDA6tXr87zZEGXLl1QpkwZxe/R0dEYNmwYOnfujC5duijKrayscj2WwYMHo3jx4ujduzdKlSqFR48e4c8//4SHhwfu3r0LXV1dRd2TJ0+iU6dOaNq0KVatWoVHjx5h/vz5CAoKwtq1a3M91twwY8YMTJ06VeowiIiIiIiIiHIUEwVElKFXr16hT58+cHBwwKVLl2BhYaFYNmbMGDRq1Ah9+vTBw4cP4eDgkGPbVVdXh7q6eo61l5u6desGc3NzAMDQoUPRtWtXHDp0CDdu3MjSze4vyeVyJCYmZlgnKCgIAGBiYpLt7XwpJ5MzDRs2xMaNG3Ht2jW0bt1aUX716lV0794du3btwp07d1C3bl3FsitXrqBKlSowNDTM9HZkMhl0dHRyLO78pkqVKqhSpYri95CQEAwbNgxVqlRB79698zSWAwcOqCSsatSogX79+mHnzp0YOHCgonzixImoUqUKzpw5Aw2NT6ccRkZGWLhwIcaMGYMKFSrkZeg5QkNDQ7EvRERERERERIUFhx4iogwtW7YMsbGx2LBhg1KSAADMzc2xfv16xMTEYOnSpYry1GFSnj9/ju7du8PIyAjFihXDmDFjEB8fr6gnk8kQExODrVu3KoZOSR0bP615Auzt7dGuXTtcvHgRNWvWhK6uLpydnRVD8hw6dAjOzs7Q0dFBjRo1cO/ePaV4Hz58CHd3dzg4OEBHRwfW1tb46aef8PHjxxw9Zs2bNwcA+Pj4AABiYmIwYcIE2NraQltbG+XLl8evv/4KIYTSejKZDCNHjsTOnTtRqVIlaGtrY926dYrjPnfuXMVxmjNnDpo2bYp+/foBAGrVqqV0/ABg//79qFGjBnR1dWFubo7evXvDz8/vq/GnNUfB69ev8f3338PMzAx6enqoW7cuTpw48dW2GjZsCOBTYiBVfHw87t69iy5dusDBwUFpWXBwMF6+fKlYDwD8/Pzw008/wcrKCtra2qhUqRI2bdqktJ305ijYv38/nJycoKOjg8qVK+Pw4cMZjk+/YcMGODo6QltbG7Vq1cKtW7cUy9zd3bF69WoAUBruJ9WePXtQo0YNGBoawsjICM7Ozli5cuVXj1FOOn/+PBo1agR9fX2YmJigY8eOePbsmVKdzH4/05PW8EapbxR9vq2nT5/i6dOnGDx4sNKN9eHDh0MIke6QQt9qzZo1iu9P8eLFMWLEiEzN2xEeHg53d3cYGxvDxMQE/fr1S3O9tOYoSP3uHjlyBJUrV1b001OnTqmsn/r3S0dHB46Ojli/fn2abZ49exYNGzaEiYkJDAwMUL58eUyfPj1Lx4KIiIiIiIgos/hIHBFl6J9//oG9vT0aNWqU5vLGjRvD3t4+zZvG3bt3h729PRYtWoQbN27gjz/+QFhYGLZt2wYA2L59OwYOHIjatWtj8ODBAABHR8cM4/H29sYPP/yAIUOGoHfv3vj111/Rvn17rFu3DtOnT8fw4cMBAIsWLUL37t2VhtE5e/YsXr9+jf79+8Pa2hpPnjzBhg0b8OTJE9y4cSPHJih99eoVAKBYsWIQQqBDhw64cOECBgwYABcXF5w+fRqTJk2Cn58ffv/9d6V1z58/j3379mHkyJEwNzdH1apVsXbtWpVhZqpUqYIGDRqgfPny2LBhA+bNm4fSpUsrjt+WLVvQv39/1KpVC4sWLUJgYCBWrlyJq1ev4t69e1l6AyEwMBD169dHbGwsRo8ejWLFimHr1q3o0KEDDhw4kOGwUw4ODihevDiuXLmiKLt16xYSExNRv3591K9fH1evXsWECRMAANeuXQPwvwRDYGAg6tatq7gRa2FhgZMnT2LAgAGIjIzE2LFj0932iRMn0KNHDzg7O2PRokUICwvDgAEDUKJEiTTr79q1C1FRURgyZAhkMhmWLl2KLl264PXr19DU1MSQIUPw4cMHnD17Ftu3b1da9+zZs+jVqxdatGiBJUuWAPh00/zq1asYM2bM1w9yDvD09ETr1q3h4OCAOXPmIC4uDqtWrUKDBg1w9+5dleTI176fWREQEAAAijdrACgSdTVr1lSqW7x4cZQsWVIlkZeeqKgohISEqJQnJCSolM2ZMwdz586Fq6srhg0bhhcvXmDt2rW4desWrl69mu7QVEIIdOzYEVeuXMHQoUNRsWJFHD58WJGIy4wrV67g0KFDGD58OAwNDfHHH3+ga9euePv2LYoVKwbg0zFxc3ODjY0N5s6di5SUFMybN08lCfvkyRO0a9cOVapUwbx586CtrQ1vb2+lpBoRERERERFRjhJEROkIDw8XAETHjh0zrNehQwcBQERGRgohhJg9e7YAIDp06KBUb/jw4QKAePDggaJMX19f9OvXT6XNzZs3CwDCx8dHUWZnZycAiGvXrinKTp8+LQAIXV1d8ebNG0X5+vXrBQBx4cIFRVlsbKzKdnbv3i0AiEuXLmW47bSk7ueLFy9EcHCw8PHxEevXrxfa2trCyspKxMTEiCNHjggAYv78+UrrduvWTchkMuHt7a0oAyDU1NTEkydPlOoGBwcLAGL27NnpHqdbt24pyhITE4WlpaWoXLmyiIuLU5QfP35cABCzZs1S2YfP2dnZKX0mY8eOFQDE5cuXFWVRUVGidOnSwt7eXqSkpGR4nL7//nuhq6srEhMThRBCLFq0SJQuXVoIIcSaNWuEpaWlou7EiRMFAOHn5yeEEGLAgAHCxsZGhISEKLXZs2dPYWxsrPhMfXx8BACxefNmRR1nZ2dRsmRJERUVpSi7ePGiACDs7OwUZanrFitWTISGhirKjx49KgCIf/75R1E2YsQIleMlhBBjxowRRkZGIjk5OcNjkVPS6hMuLi7C0tJSfPz4UVH24MEDoaamJvr27asoy8r3M7MGDBgg1NXVxcuXLxVly5YtEwDE27dvVerXqlVL1K1bN8M2L1y4IABk+KOvr6+oHxQUJLS0tETLli2V+uSff/4pAIhNmzYpyvr166fUB1K/p0uXLlWUJScni0aNGqn0q7S+MwCElpaW0vf5wYMHAoBYtWqVoqx9+/ZCT09P0b+FEMLLy0toaGgotfn7778LACI4ODjDY0RERERERESUUzj0EBGlKyoqCgC+OlZ86vLIyEil8hEjRij9PmrUKACfJoTNLicnJ6Vx/+vUqQPg03A/pUqVUil//fq1ouzzSVbj4+MREhKiGBv/7t272Y6pfPnysLCwQOnSpTFkyBCUKVMGJ06cgJ6eHjw8PKCuro7Ro0crrTNhwgQIIXDy5Eml8iZNmsDJySnbsQDA7du3ERQUhOHDhyuN29+2bVtUqFAhU0MGfc7DwwO1a9dWGg7IwMAAgwcPhq+vL54+fZrh+g0bNkRcXBzu3LkD4NMwRPXr1wcANGjQAEFBQfDy8lIsK126NIoXLw4hBA4ePIj27dtDCIGQkBDFT6tWrRAREZHu5/bhwwc8evQIffv2hYGBgaK8SZMmcHZ2TnOdHj16wNTUVPF76ls0n/eh9JiYmCAmJgZnz579at3c4O/vj/v378Pd3R1mZmaK8ipVquC7775L8zuXU9/PXbt24e+//8aECRNQtmxZRXlcXByAtOe80NHRUSz/mlmzZuHs2bMqPy1btlSq5+npicTERIwdO1ZpMu5BgwbByMgow37v4eEBDQ0NDBs2TFGmrq6uOCaZ4erqqvRGVJUqVWBkZKToPykpKfD09ESnTp1QvHhxRb0yZcoozd8B/G/OkaNHj0Iul2c6BiIiIiIiIqLsYqKAiNKVmgBITRikJ72Ewuc3DYFPwwqpqakpzTuQVZ8nAwDA2NgYAGBra5tmeVhYmKIsNDQUY8aMgZWVFXR1dRU39wEgIiIi2zEdPHgQZ8+excWLF+Ht7Y3Hjx+jRo0aAIA3b96gePHiKsemYsWKiuWfS43nW6S2Wb58eZVlFSpUUNlmZtpLq6309uFLn89TIITAtWvX0KBBAwBA5cqVYWRkhKtXryI+Ph537txR1A8ODkZ4eLhifozPf/r37w/gf5M5pxUz8Okm7JfSKgNU+1Zq0uDzPpSe4cOHo1y5cmjdujVKliyJn376Kc3x6b8UHByMgIAAxU90dPRX10lLRp95xYoVERISgpiYGKXynPh+Xr58GQMGDECrVq2wYMECpWWpibm0hgiKj49XStxlxNnZGa6urio/NjY2SvXSOwZaWlpwcHDIsJ++efMGNjY2SkmltNrKyJf9B/jUh1L7T1BQEOLi4jLVJ3v06IEGDRpg4MCBsLKyQs+ePbFv3z4mDYiIiIiIiCjXcI4CIkqXsbExbGxs8PDhwwzrPXz4ECVKlICRkVGG9XJiDgB1dfUslYvPJgzu3r07rl27hkmTJsHFxQUGBgaQy+Vwc3P7phtwjRs3Vhqb/Vtk9uZpQVK1alUYGhriypUraNOmDUJDQxVvFKipqaFOnTq4cuUKHB0dkZiYqEgUpH4mvXv3Tnes+CpVquRYnJnpQ+mxtLTE/fv3cfr0aZw8eRInT57E5s2b0bdvX2zdujXd9WrVqqV0A3v27NmYM2dOlmPPCVn9fj548AAdOnRA5cqVceDAAaUJiwEobuT7+/urJPL8/f1Ru3btbws4n/mW/vMlXV1dXLp0CRcuXMCJEydw6tQp7N27F82bN8eZM2fS3RYRERERERFRdvGNAiLKULt27eDj46M0Ge3nLl++DF9fX7Rr105lWepwMqm8vb0hl8uVJlXNqQmEvyYsLAznzp3D1KlTMXfuXHTu3BnfffcdHBwccnW7dnZ2+PDhg8pbGc+fP1cs/5qsHqPUNl+8eKGy7MWLF5na5pftpdVWZvdBXV0ddevWxdWrV3HlyhUYGRkpDf+TOqFx6kStqYkCCwsLGBoaIiUlJc0nyl1dXWFpaZluzMCnPveltMoyK6PPQktLC+3bt8eaNWvw6tUrDBkyBNu2bctwezt37lQaTqdv377Ziiujz/z58+cwNzeHvr6+Unlmvp/pefXqFdzc3GBpaQkPDw+VJ/EBwMXFBcCnobA+9+HDB7x//16xPKekdwwSExPh4+OTYT+1s7ODv7+/yhsdaR3P7LK0tISOjk6m+6SamhpatGiB5cuX4+nTp1iwYAHOnz+PCxcu5FhMRERERERERKmYKCCiDE2aNAm6uroYMmQIPn78qLQsNDQUQ4cOhZ6eHiZNmqSy7urVq5V+X7VqFQAojcetr6+P8PDwnA/8C6lP4H75dO+KFStydbtt2rRBSkoK/vzzT6Xy33//HTKZTGVs8rTo6ekBQKaPU82aNWFpaYl169YpDfty8uRJPHv2DG3bts38DuDTPty8eRPXr19XlMXExGDDhg2wt7fP1JwKDRs2RHBwMDZv3ow6deoojSFfv359vHjxAkePHkWxYsUUQxqpq6uja9euOHjwIB4/fqzSZnBwcLrbK168OCpXroxt27Yp3fz9999/8ejRo0ztd1pSb7Z/+Vl8+d1QU1NTvO2Q1tA7qRo0aKCU+Mhu4srGxgYuLi7YunWrUmyPHz/GmTNn0KZNG5V1MvP9TEtAQABatmwJNTU1nD59GhYWFmnWq1SpEipUqIANGzYgJSVFUb527VrIZDJ069Yts7uXKa6urtDS0sIff/yh9D3/+++/ERERkWG/b9OmDZKTk7F27VpFWUpKiuKY5AR1dXW4urriyJEj+PDhg6Lc29tbZa6S0NBQlfVTEysZ9SciIiIiIiKi7OLQQ0SUobJly2Lr1q348ccf4ezsjAEDBqB06dLw9fXF33//jZCQEOzevVtpEs9UPj4+6NChA9zc3HD9+nXs2LEDP/zwA6pWraqoU6NGDXh6emL58uUoXrw4SpcurZiIOCcZGRmhcePGWLp0KZKSklCiRAmcOXMGPj4+Ob6tz7Vv3x7NmjXDzz//DF9fX1StWhVnzpzB0aNHMXbs2DSP25d0dXXh5OSEvXv3oly5cjAzM0PlypVRuXLlNOtrampiyZIl6N+/P5o0aYJevXohMDAQK1euhL29PcaNG5elfZg6dSp2796N1q1bY/To0TAzM8PWrVvh4+ODgwcPKt30T0/qWwLXr19XGVqnbt26kMlkuHHjBtq3b6/01P7ixYtx4cIF1KlTB4MGDYKTkxNCQ0Nx9+5deHp6pnlDNdXChQvRsWNHNGjQAP3790dYWBj+/PNPVK5cOdtzAaTOPTF69Gi0atUK6urq6NmzJwYOHIjQ0FA0b94cJUuWxJs3b7Bq1Sq4uLgoEh+5bdmyZWjdujXq1auHAQMGIC4uDqtWrYKxsXGawxll5vuZFjc3N7x+/RqTJ0/GlStXlN42srKywnfffacUU4cOHdCyZUv07NkTjx8/xp9//omBAwfm+HGxsLDAtGnTMHfuXLi5uaFDhw548eIF1qxZg1q1aqF3797prtu+fXs0aNAAU6dOha+vL5ycnHDo0KFvmrskLXPmzMGZM2fQoEEDDBs2TJFErFy5Mu7fv6+oN2/ePFy6dAlt27aFnZ0dgoKCsGbNGpQsWVJpUnEiIiIiIiKiHCOIiDLh4cOHolevXsLGxkZoamoKa2tr0atXL/Ho0SOVurNnzxYAxNOnT0W3bt2EoaGhMDU1FSNHjhRxcXFKdZ8/fy4aN24sdHV1BQDRr18/IYQQmzdvFgCEj4+Poq6dnZ1o27atyvYAiBEjRiiV+fj4CABi2bJlirL379+Lzp07CxMTE2FsbCy+//578eHDBwFAzJ49W1EvrW2nJXU/g4ODM6wXFRUlxo0bJ4oXLy40NTVF2bJlxbJly4RcLv/qfqS6du2aqFGjhtDS0lKKNzXWW7duqayzd+9eUa1aNaGtrS3MzMzEjz/+KN6/f5/mPnzOzs5O8TmkevXqlejWrZswMTEROjo6onbt2uL48eMZ7vfnYmJihIaGhgAgzpw5o7K8SpUqAoBYsmSJyrLAwEAxYsQIYWtrq+h7LVq0EBs2bFDUSf28N2/erLTunj17RIUKFYS2traoXLmyOHbsmOjatauoUKGCyrqf95VUX/aN5ORkMWrUKGFhYSFkMpni2B04cEC0bNlSWFpaCi0tLVGqVCkxZMgQ4e/vn+ljlBXBwcEqsQkhhKenp2jQoIHQ1dUVRkZGon379uLp06dKdbLy/UwLgHR/mjRpolL/8OHDwsXFRWhra4uSJUuKGTNmiMTExK9u58KFCwKA2L9/f5rL+/XrJ/T19VXK//zzT1GhQgWhqakprKysxLBhw0RYWJjKunZ2dkplHz9+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/G5s2bAQADBw7EsWPHAADnz59HtWrVULVqVVSrVg1WVlYYN25custmzpwJAHBwcMDcuXPRoEEDlClTBhYWFhgyZIg0O0pERERERERERERUiMiEEELqIPKbyMhIGBsbIyIiAkZGRlKHU6jJ5XIEBQXB0tISamrMW9HXsc9QdrDfUFaxz1B2sN/kHZ6vExERERHlLF7BEBEREREREREREREVYUwUEBEREREREREREREVYUwUEBEREREREREREREVYRpSB0AZs596QuoQcpUaBCqaCjwLk0EOmdTh5ArfxW2lDoGIiIiIiIiIiIgoXXyjgIiIiIiIiIiIiIioCGOigIiIiIiIiIiIiIioCGOigIiIiIiIiIiIiIioCGOigIiIiIiIiIiIKIuaNm0KbW1tGBgYwNDQEJUqVcL+/fsBAL6+vpDJZAgPD1fU37hxI0xNTXHx4kUAgEwmg62tLeLj4xV1jhw5Ant7e6XtPH78GN27d4elpSUMDAzg6OgId3d3PHr0KLd3kYiKECYKiEgyXl5eqF+/PsqVK4datWrhyZMnKnWuX78OFxcXuLi4oFKlShg6dCgSEhIAAOfPn0ft2rXh5OSESpUqYfLkyZDL5QCA6OhotGrVCubm5jAxMcnL3SIiIiIiIqIiYsmSJYiOjkZkZCSWLl2KH3/8EW/evEmz3s8//wxPT080bdpUUR4XF4dVq1al2/6dO3cU18337t1DdHQ0bt26hcaNG+PkyZO5sUtEVEQxUUBEkhkyZAgGDx6Mly9fYsqUKXB3d1epU7VqVdy6dQv379/Ho0ePEBQUhC1btgAATE1NsWfPHjx9+hR37tzBtWvXsG3bNgCApqYmpkyZAk9PzzzcIyIiIiIiIiqKZDIZ2rZtCxMTE7x48UJp2ZQpU/Dnn3/i0qVLqFGjhtKy6dOnY9GiRUpvHnxuwoQJ6NWrF+bPn48SJUoAAMzMzPDTTz9h8uTJubIvRFQ0MVFARJIICgrC7du30bt3bwBA165d8e7dO3h7eyvV09PTg6amJgAgMTERcXFxkMlkAIBq1arBwcEBAKCjowMXFxf4+voCALS1tdG8eXO+TUBERERERES5Ti6X4+jRo4iLi4OLi4uifOjQoTh8+DCuXr2KChUqqKzXvHlz1KpVC0uWLFFZFhsbi8uXL6NHjx65GToREQAmCohIIu/evYONjQ00NDQAfHr6olSpUnj79q1KXV9fX1StWhXm5uYwNjZO882DgIAAHDhwAO3atcvt0ImIiIiIiIgAANOmTYOJiQn09fXRpUsXzJgxA5aWlorlHh4eaNeuHUqVKpVuG4sXL8aqVavw4cMHpfKwsDDI5XIUL15cUbZ582aYmJjA0NAQderUyfkdIqIii4kCIsr37O3t8eDBAwQEBCAhIQEeHh5KyyMjI9G+fXtMnjwZNWvWlChKIiIiIiIiKmpShw2Ki4vDixcvsHXrVqxfv16x/J9//sH27dvx888/p9tGtWrV0KFDB8ydO1ep3NTUFGpqakoJhP79+yM8PByrVq1SzN9HRJQTmCggIknY2trC398fycnJAAAhBN6+fZvhUxYGBgbo0aMHDh06pCiLioqCm5sbOnbsiPHjx+d63ERERERERERpKVOmDNq0aYPjx48ryqpWrYrz589j48aNmDp1arrrzp8/Hzt27MDLly8VZXp6emjQoAH27duXq3ETEQFMFBCRRCwtLVG9enXs2LEDAHDw4EGULFkSZcqUUarn7e2NpKQkAJ/mKDhy5AgqVqwIAIiOjoabmxvc3NwwY8aMvN0BIiIiIiIios/4+vrCw8MDzs7OSuXOzs64cOECNm/enO4ExA4ODvjpp5+wdOlSpfJff/0VO3fuxKxZsxRvFkRERODu3bu5sxNEVGQxUUBEklm/fj3Wr1+PcuXKYfHixdi8eTMAYODAgTh27BgA4Pz586hWrRqqVq2KatWqwcrKCuPGjQMArFy5Ejdv3sShQ4fg4uICFxcXLFiwQNF+lSpVUK9ePURGRqJkyZLo06dP3u8kERERERERFVpTpkyBgYEBDAwM0LBhQ7i6umLWrFkq9SpVqoSLFy9i+/btmDBhQpptzZw5E4mJiUpltWvXxtWrV/HkyRNUqVIFhoaGqFGjBsLDw7F9+/Zc2SciKppkQgghdRD5TWRkJIyNjREREQEjIyNJY7GfekLS7ec2NQhUNBV4FiaDHDKpw8kVvovbSh1CoSKXyxEUFARLS0uoqTHXSZnDfkNZxT5D2cF+k3fy0/k6EREREVFhwCsYIiIiIiIiIiIiIqIijIkCIiIiIiIiIiIiIqIijIkCIiIiIiIiIiIiIqIiTEPqAIgo5xXmuS2KwrwWAOe2ICIiIiIiIiKivMM3CoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiIiIijAmCoiIiIiIiIiIiChfadq0KWQyGTw9PZXKly1bBplMhrFjxwIAZDIZbG1tER8fr6hz5MgR2NvbK3739/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 6d81e9f..52ddac5 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -44,23 +44,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", diff --git a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index 080860c..06013c7 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -65,23 +65,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", diff --git a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index c2f2886..6751962 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -51,29 +51,44 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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()\n", - "\n", - "\n" + "check_gpu()" ] }, { diff --git a/sample_lp_sever_notebooks/linear-programming.ipynb b/sample_lp_sever_notebooks/linear-programming.ipynb index 60beb0e..b60a5f6 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -51,23 +51,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", 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 f8d18ce..cc089e3 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 @@ -52,23 +52,40 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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", diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb index 0de6a2f..ac5ed65 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -52,27 +52,44 @@ "\n", "def check_gpu():\n", " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\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", - "
{output.splitlines()[2]}
\n", + "
{gpu_info}
\n", "
\n", " \"\"\"))\n", - " except Exception:\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()\n" + "check_gpu()" ] }, { diff --git a/workforce_optimization/workforce_optimization_milp.ipynb b/workforce_optimization/workforce_optimization_milp.ipynb index 8696519..c056185 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -1,830 +1,847 @@ { - "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": null, - "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": [ - "import subprocess\n", - "from IPython.display import display, HTML\n", - "\n", - "def check_gpu():\n", - " try:\n", - " output = subprocess.check_output(\"nvidia-smi\", shell=True).decode()\n", - " display(HTML(f\"\"\"\n", - "
\n", - "

✅ GPU is enabled

\n", - "
{output.splitlines()[2]}
\n", - "
\n", - " \"\"\"))\n", - " except Exception:\n", - " display(HTML(\"\"\"\n", - "
\n", - "

⚠️ GPU not detected!

\n", - "

This notebook requires a GPU runtime.

\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", - " \"\"\"))\n", - "\n", - "check_gpu()\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 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": 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" - ] - }, + "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": null, + "metadata": {}, + "outputs": [ { - "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" - ] - }, + "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": [ + "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": 3, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solver Configuration and Solution\n", - "\n", - "Configure the solver settings and solve the optimization problem.\n" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solution Analysis\n", - "\n", - "Let's analyze the optimal solution and create visualizations.\n" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] - }, + "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": [ { - "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" - ] + "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" + ] } - ], - "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": [ + "# 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" }, - "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 } From 11d5fbb790478f085e89ea6198ec5917f8e9d5db Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 13:52:26 -0500 Subject: [PATCH 5/8] Addres reviews and also handle generic colab issues --- .../trnsport_cuopt.ipynb | 25 ++++++++---------- .../Production_Planning_Example_Pulp.ipynb | 16 ++++++------ PuLP_integration_example/Simple_LP_pulp.ipynb | 15 ++++++----- .../Simple_MIP_pulp.ipynb | 15 ++++++----- PuLP_integration_example/Sudoku_pulp.ipynb | 15 ++++++----- diet_optimization/diet_optimization_lp.ipynb | 19 ++++++++------ .../diet_optimization_milp.ipynb | 19 ++++++++------ ...t_matrix_and_waypoint_graph_creation.ipynb | 11 +++++--- .../intra-factory_transport.ipynb | 18 ++++++------- .../cvrp_daily_deliveries.ipynb | 18 ++++++------- .../cvrptw_benchmark_gehring_homberger.ipynb | 18 ++++++------- .../cvrptw_service_team_routing.ipynb | 18 ++++++------- .../CVaR/01_optimization_with_cufolio.ipynb | 25 ++++++++---------- .../CVaR/02_backtesting.ipynb | 25 ++++++++---------- .../CVaR/03_advanced_topics.ipynb | 25 ++++++++---------- .../cvar_portfolio_optimization.ipynb | 26 ++++++++----------- .../cvrptw_benchmark_gehring_homberger.ipynb | 20 +++++++------- .../cvrptw_service_team_routing.ipynb | 20 +++++++------- .../linear-programming-with-datamodel.ipynb | 19 +++++++------- .../linear-programming.ipynb | 19 +++++++------- ...er-linear-programming-with-datamodel.ipynb | 19 +++++++------- .../mixed-integer-linear-programming.ipynb | 19 +++++++------- .../workforce_optimization_milp.ipynb | 19 ++++++++------ 23 files changed, 226 insertions(+), 217 deletions(-) diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index 831c571..80d16f4 100644 --- a/GAMSPy_integration_example/trnsport_cuopt.ipynb +++ b/GAMSPy_integration_example/trnsport_cuopt.ipynb @@ -42,16 +42,10 @@ } ], "source": [ - "# remove -q to debug issues with pip installs\n", - "!pip install --upgrade -q --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", - "#!pip install --upgrade -q --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19\n", - "\n", - "!pip install -q gamspy\n", - "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)" + "# 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" ] }, { @@ -61,20 +55,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index b92d1f6..22034a8 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -34,20 +34,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -99,11 +102,8 @@ "outputs": [], "source": [ "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\n", - "# For cuda-12\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", - "\n", - "# For cuda-13\n", "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" ] }, diff --git a/PuLP_integration_example/Simple_LP_pulp.ipynb b/PuLP_integration_example/Simple_LP_pulp.ipynb index 223a6a7..fcfbab9 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -35,20 +35,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -98,8 +101,8 @@ }, "outputs": [], "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\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/PuLP_integration_example/Simple_MIP_pulp.ipynb b/PuLP_integration_example/Simple_MIP_pulp.ipynb index c199884..a26059e 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -34,20 +34,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -98,8 +101,8 @@ }, "outputs": [], "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\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/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index 0c190e2..a043b43 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -36,20 +36,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -99,8 +102,8 @@ }, "outputs": [], "source": [ - "# # Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", - "\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/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index b990eb9..f439c52 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -34,20 +34,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -84,10 +87,10 @@ "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 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 # For cuda 12\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 # For cuda 13\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/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index 93d15ef..ba380a3 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -34,20 +34,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -84,10 +87,10 @@ "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 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 # For cuda 12\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19 # For cuda 13\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/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb index 24bfdbb..57bd5f2 100644 --- a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb +++ b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb @@ -32,20 +32,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index 117a410..e6e8814 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -53,20 +53,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -112,12 +115,9 @@ "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 cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\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/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index 8d884a8..943d25b 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -65,20 +65,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -124,12 +127,9 @@ "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 cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\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/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index 7c66b3a..f0046b6 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -40,20 +40,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -99,12 +102,9 @@ "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 cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\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/last_mile_delivery/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index db3c47f..d7fff74 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -69,20 +69,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -128,12 +131,9 @@ "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 cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\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/01_optimization_with_cufolio.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/01_optimization_with_cufolio.ipynb index a7bbe80..9c88fb5 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 @@ -36,20 +36,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -87,16 +90,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 --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 \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/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index d6bee68..be1dcc7 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -37,20 +37,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -88,16 +91,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 --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\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 8248388..6047ecf 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb @@ -41,20 +41,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -92,16 +95,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 --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\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 fb46e48..2f68a41 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -99,20 +99,23 @@ ], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -172,17 +175,10 @@ } ], "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 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", - "\n", - "# For CUDA 13.x systems:\n", - "# !pip install --upgrade --extra-index-url https://pypi.nvidia.com --user cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19\n", - "\n", - "# Install other dependencies\n", - "!pip install numpy pandas matplotlib seaborn scipy" + "# 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/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb index 52ddac5..2ab7503 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -40,20 +40,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -99,13 +102,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 cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", - "\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\n" + "#!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/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index 06013c7..5577373 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -61,20 +61,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -120,13 +123,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 cuopt-server-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19 cuopt-sh-client\n", - "\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" + "#!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-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index 6751962..5fdb829 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -47,20 +47,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -106,12 +109,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 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\n" + "#!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 b60a5f6..0da8add 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -47,20 +47,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -106,12 +109,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 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\n" + "#!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 cc089e3..3165329 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 @@ -48,20 +48,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -107,12 +110,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 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\n" + "#!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 ac5ed65..bcfebc9 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -48,20 +48,23 @@ "outputs": [], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -108,12 +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 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\n" + "#!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 c056185..53b862e 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -70,20 +70,23 @@ ], "source": [ "import subprocess\n", + "import html\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", + " 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}
\n", + "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", - " except (subprocess.CalledProcessError, FileNotFoundError, IndexError) as e:\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", "

⚠️ GPU not detected!

\n", @@ -120,10 +123,10 @@ "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" + "# 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" ] }, { From 76686a7b262ee4bfeb997082b0b5bf8c980db843 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 13:57:05 -0500 Subject: [PATCH 6/8] Update Pulp version --- PuLP_integration_example/Production_Planning_Example_Pulp.ipynb | 2 +- PuLP_integration_example/Simple_LP_pulp.ipynb | 2 +- PuLP_integration_example/Simple_MIP_pulp.ipynb | 2 +- PuLP_integration_example/Sudoku_pulp.ipynb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb index 22034a8..0b81745 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -89,7 +89,7 @@ }, "outputs": [], "source": [ - "!pip install pulp==3.2.0" + "!pip install pulp==3.3.0" ] }, { diff --git a/PuLP_integration_example/Simple_LP_pulp.ipynb b/PuLP_integration_example/Simple_LP_pulp.ipynb index fcfbab9..3f8dbec 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -90,7 +90,7 @@ }, "outputs": [], "source": [ - "!pip install pulp==3.2.0" + "!pip install pulp==3.3.0" ] }, { diff --git a/PuLP_integration_example/Simple_MIP_pulp.ipynb b/PuLP_integration_example/Simple_MIP_pulp.ipynb index a26059e..9bf2eeb 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -89,7 +89,7 @@ }, "outputs": [], "source": [ - "pip install pulp==3.2.0" + "pip install pulp==3.3.0" ] }, { diff --git a/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index a043b43..cf21191 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -91,7 +91,7 @@ }, "outputs": [], "source": [ - " !pip install pulp==3.2.0" + " !pip install pulp==3.3.0" ] }, { From f309e2e24416836987174765787ef58119116337 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 16 Oct 2025 14:48:16 -0500 Subject: [PATCH 7/8] address review comments --- .../trnsport_cuopt.ipynb | 2 + .../Production_Planning_Example_Pulp.ipynb | 2 + PuLP_integration_example/Simple_LP_pulp.ipynb | 2 + .../Simple_MIP_pulp.ipynb | 4 +- PuLP_integration_example/Sudoku_pulp.ipynb | 2 + diet_optimization/diet_optimization_lp.ipynb | 2 + .../diet_optimization_milp.ipynb | 2 + ...t_matrix_and_waypoint_graph_creation.ipynb | 2 + .../intra-factory_transport.ipynb | 2 + .../cvrp_daily_deliveries.ipynb | 2 + .../cvrptw_benchmark_gehring_homberger.ipynb | 2 + .../cvrptw_service_team_routing.ipynb | 2 + .../CVaR/01_optimization_with_cufolio.ipynb | 2 + .../CVaR/02_backtesting.ipynb | 2 + .../CVaR/03_advanced_topics.ipynb | 2 + .../cvar_portfolio_optimization.ipynb | 2 + .../cvrptw_benchmark_gehring_homberger.ipynb | 2 + .../cvrptw_service_team_routing.ipynb | 2 + .../linear-programming-with-datamodel.ipynb | 2 + .../linear-programming.ipynb | 2 + ...er-linear-programming-with-datamodel.ipynb | 2 + .../mixed-integer-linear-programming.ipynb | 2 + .../workforce_optimization_milp.ipynb | 387 ++---------------- 23 files changed, 81 insertions(+), 352 deletions(-) diff --git a/GAMSPy_integration_example/trnsport_cuopt.ipynb b/GAMSPy_integration_example/trnsport_cuopt.ipynb index 80d16f4..200fea7 100644 --- a/GAMSPy_integration_example/trnsport_cuopt.ipynb +++ b/GAMSPy_integration_example/trnsport_cuopt.ipynb @@ -71,6 +71,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -98,6 +99,7 @@ " \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 0b81745..6c28a1a 100644 --- a/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb +++ b/PuLP_integration_example/Production_Planning_Example_Pulp.ipynb @@ -50,6 +50,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -77,6 +78,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/PuLP_integration_example/Simple_LP_pulp.ipynb b/PuLP_integration_example/Simple_LP_pulp.ipynb index 3f8dbec..59c6c8a 100644 --- a/PuLP_integration_example/Simple_LP_pulp.ipynb +++ b/PuLP_integration_example/Simple_LP_pulp.ipynb @@ -51,6 +51,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -78,6 +79,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/PuLP_integration_example/Simple_MIP_pulp.ipynb b/PuLP_integration_example/Simple_MIP_pulp.ipynb index 9bf2eeb..25038dc 100644 --- a/PuLP_integration_example/Simple_MIP_pulp.ipynb +++ b/PuLP_integration_example/Simple_MIP_pulp.ipynb @@ -50,6 +50,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -77,6 +78,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] @@ -89,7 +91,7 @@ }, "outputs": [], "source": [ - "pip install pulp==3.3.0" + "!pip install pulp==3.3.0" ] }, { diff --git a/PuLP_integration_example/Sudoku_pulp.ipynb b/PuLP_integration_example/Sudoku_pulp.ipynb index cf21191..994f6eb 100644 --- a/PuLP_integration_example/Sudoku_pulp.ipynb +++ b/PuLP_integration_example/Sudoku_pulp.ipynb @@ -52,6 +52,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -79,6 +80,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index f439c52..7b9a732 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -50,6 +50,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -77,6 +78,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/diet_optimization/diet_optimization_milp.ipynb b/diet_optimization/diet_optimization_milp.ipynb index ba380a3..1905fbe 100644 --- a/diet_optimization/diet_optimization_milp.ipynb +++ b/diet_optimization/diet_optimization_milp.ipynb @@ -50,6 +50,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -77,6 +78,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] 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 57bd5f2..7882780 100644 --- a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb +++ b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb @@ -48,6 +48,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -75,6 +76,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/intra-factory_transport/intra-factory_transport.ipynb b/intra-factory_transport/intra-factory_transport.ipynb index e6e8814..89c699e 100644 --- a/intra-factory_transport/intra-factory_transport.ipynb +++ b/intra-factory_transport/intra-factory_transport.ipynb @@ -69,6 +69,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -96,6 +97,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index 943d25b..60d0476 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -81,6 +81,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -108,6 +109,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb index f0046b6..dd1b531 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -56,6 +56,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -83,6 +84,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/last_mile_delivery/cvrptw_service_team_routing.ipynb b/last_mile_delivery/cvrptw_service_team_routing.ipynb index d7fff74..29532d0 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -85,6 +85,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -112,6 +113,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] 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 9c88fb5..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 @@ -52,6 +52,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -79,6 +80,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb index be1dcc7..c67d637 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/02_backtesting.ipynb @@ -53,6 +53,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -80,6 +81,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] 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 6047ecf..a069016 100644 --- a/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb +++ b/portfolio_optimization/cuFOLIO_portfolio_optimization/CVaR/03_advanced_topics.ipynb @@ -57,6 +57,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -84,6 +85,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index 2f68a41..bda7d89 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -115,6 +115,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -142,6 +143,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb index 2ab7503..a5d02b7 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -56,6 +56,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -83,6 +84,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb index 5577373..ecc1844 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -77,6 +77,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -104,6 +105,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb index 5fdb829..4b74155 100644 --- a/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb +++ b/sample_lp_sever_notebooks/linear-programming-with-datamodel.ipynb @@ -63,6 +63,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -90,6 +91,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/sample_lp_sever_notebooks/linear-programming.ipynb b/sample_lp_sever_notebooks/linear-programming.ipynb index 0da8add..d3042f6 100644 --- a/sample_lp_sever_notebooks/linear-programming.ipynb +++ b/sample_lp_sever_notebooks/linear-programming.ipynb @@ -63,6 +63,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -90,6 +91,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] 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 3165329..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 @@ -64,6 +64,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -91,6 +92,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb index bcfebc9..d0d103d 100644 --- a/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb +++ b/sample_lp_sever_notebooks/mixed-integer-linear-programming.ipynb @@ -64,6 +64,7 @@ "
{gpu_info_escaped}
\n", "
\n", " \"\"\"))\n", + " return True\n", " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", " display(HTML(\"\"\"\n", "
\n", @@ -91,6 +92,7 @@ " \n", "
\n", " \"\"\"))\n", + " return False\n", "\n", "check_gpu()" ] diff --git a/workforce_optimization/workforce_optimization_milp.ipynb b/workforce_optimization/workforce_optimization_milp.ipynb index 53b862e..2d4b161 100644 --- a/workforce_optimization/workforce_optimization_milp.ipynb +++ b/workforce_optimization/workforce_optimization_milp.ipynb @@ -31,62 +31,23 @@ "cell_type": "code", "execution_count": null, "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" - ] - } - ], + "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", + " 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", - " gpu_info_escaped = html.escape(gpu_info)\n", " display(HTML(f\"\"\"\n", "
\n", "

✅ GPU is enabled

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

⚠️ GPU not detected!

\n", @@ -123,10 +84,10 @@ "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" + "# 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" ] }, { @@ -138,61 +99,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", @@ -212,19 +121,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of shifts: 14\n", - "Number of workers: 7\n", - "Number of available assignments: 73\n" - ] - } - ], + "outputs": [], "source": [ "# Number of workers required for each shift\n", "shift_requirements = {\n", @@ -273,42 +172,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Create DataFrames for better visualization\n", "shifts_df = pd.DataFrame(list(shift_requirements.items()), columns=['Shift', 'Required Workers'])\n", @@ -334,18 +200,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Create the optimization problem\n", "problem = Problem(\"workforce_optimization\")\n", @@ -364,17 +221,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Objective function set: minimize total labor cost\n" - ] - } - ], + "outputs": [], "source": [ "# Create objective function: minimize total labor cost\n", "objective_expr = LinearExpression([], [], 0.0)\n", @@ -391,18 +240,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Add constraints: assign exactly the required number of workers to each shift\n", "constraint_names = []\n", @@ -440,17 +280,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Solver configured with 60-second time limit\n" - ] - } - ], + "outputs": [], "source": [ "# Configure solver settings\n", "settings = SolverSettings()\n", @@ -463,57 +295,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Solve the problem\n", "print(\"Solving workforce optimization problem...\")\n", @@ -539,44 +323,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "def print_solution():\n", " \"\"\"Print the optimal solution in a readable format\"\"\"\n", @@ -624,33 +373,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Create a summary table of the solution\n", "if shift_assignments:\n", @@ -685,17 +410,9 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Added maximum shift constraints (max 4 shifts per worker)\n" - ] - } - ], + "outputs": [], "source": [ "# Add constraint: each worker can work at most 4 shifts per week\n", "max_shifts_per_worker = 4\n", @@ -721,33 +438,9 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Solve the problem again with the new constraints\n", "print(\"\\nSolving with maximum shift constraints...\")\n", @@ -763,17 +456,9 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "No optimal solution found. Status: Infeasible\n" - ] - } - ], + "outputs": [], "source": [ "# Display the new solution\n", "shift_assignments_new, worker_assignments_new = print_solution()\n" @@ -842,7 +527,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.11" + "version": "3.12.12" } }, "nbformat": 4, From 2a61579cfd4a5c7205f795214ac03b1447a0d422 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 17 Oct 2025 15:47:19 -0500 Subject: [PATCH 8/8] remove --user usage --- README.md | 2 +- .../cost_matrix_and_waypoint_graph_creation.ipynb | 2 +- last_mile_delivery/cvrp_daily_deliveries.ipynb | 2 +- last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb | 2 +- last_mile_delivery/cvrptw_service_team_routing.ipynb | 2 +- .../cvrptw_benchmark_gehring_homberger.ipynb | 2 +- .../cvrptw_service_team_routing.ipynb | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e8b0baf..02dc5e4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ docker pull nvidia/cuopt:25.10.* 3. Run the examples: ```bash -docker run -it --rm --gpus all --network=host -v $(pwd):/workspace -w /workspace nvidia/cuopt:25.05.* /bin/bash -c "pip install --user -r requirements.txt; jupyter-notebook" +docker run -it --rm --gpus all --network=host -v $(pwd):/workspace -w /workspace nvidia/cuopt:25.05.* /bin/bash -c "pip install -r requirements.txt; jupyter-notebook" ``` 4. Open your browser with the link provided in the terminal, and you can see the notebooks. 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 7882780..523c536 100644 --- a/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb +++ b/intra-factory_transport/cost_matrix_and_waypoint_graph_creation.ipynb @@ -89,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/last_mile_delivery/cvrp_daily_deliveries.ipynb b/last_mile_delivery/cvrp_daily_deliveries.ipynb index 60d0476..acf3141 100644 --- a/last_mile_delivery/cvrp_daily_deliveries.ipynb +++ b/last_mile_delivery/cvrp_daily_deliveries.ipynb @@ -143,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 dd1b531..848c480 100644 --- a/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb +++ b/last_mile_delivery/cvrptw_benchmark_gehring_homberger.ipynb @@ -118,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 29532d0..ef0b628 100644 --- a/last_mile_delivery/cvrptw_service_team_routing.ipynb +++ b/last_mile_delivery/cvrptw_service_team_routing.ipynb @@ -146,7 +146,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install --user -q matplotlib scipy " + "!pip install -q matplotlib scipy " ] }, { diff --git a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb index a5d02b7..6c6c5cd 100644 --- a/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb +++ b/routing_optimization_over_server/cvrptw_benchmark_gehring_homberger.ipynb @@ -118,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 ecc1844..ce50377 100644 --- a/routing_optimization_over_server/cvrptw_service_team_routing.ipynb +++ b/routing_optimization_over_server/cvrptw_service_team_routing.ipynb @@ -139,7 +139,7 @@ "outputs": [], "source": [ "#Install notebook dependencies\n", - "!pip install --user -q matplotlib scipy pandas numpy" + "!pip install -q matplotlib scipy pandas numpy" ] }, {